Browse Source

counter: add mpl-stack example (solita needs fix)

ngundotra 3 years ago
parent
commit
db49ce2c0a

+ 16 - 0
basics/counter/mpl-stack/.solitarc.js

@@ -0,0 +1,16 @@
+// @ts-check
+const path = require('path');
+const programDir = path.join(__dirname);
+const idlDir = path.join(__dirname, 'idl');
+const sdkDir = path.join(__dirname, 'ts', 'generated');
+const binaryInstallDir = path.join(__dirname, 'target', 'solita');
+
+module.exports = {
+    idlGenerator: 'shank',
+    programName: 'counter_mpl_stack',
+    programId: 'Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS',
+    idlDir,
+    sdkDir,
+    binaryInstallDir,
+    programDir,
+};

+ 17 - 0
basics/counter/mpl-stack/Cargo.toml

@@ -0,0 +1,17 @@
+[package]
+name = "counter-mpl-stack"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib", "lib"]
+
+[features]
+no-entrypoint = []
+cpi = ["no-entrypoint"]
+default = []
+
+[dependencies]
+borsh = "0.9"
+shank = "0.0.8"
+solana-program = "1.10.38"

+ 13 - 0
basics/counter/mpl-stack/README.md

@@ -0,0 +1,13 @@
+# Counter: MPL Stack
+
+This example program is written using Solana native using MPL stack.
+
+
+## Setup
+
+1. Build the program with `cargo build-bpf`
+2. Compile the idl with `shank build`
+3. Build the typescript SDK with `yarn solita`
+ - Temporarily, we have to modify line 58 in ts/generated/accounts/Counter.ts 
+ to `const accountInfo = await connection.getAccountInfo(address, { commitment: "confirmed" });` in order to allow the tests to pass. In the future versions of Solita, this will be fixed.
+4. Run tests with `yarn test`

+ 42 - 0
basics/counter/mpl-stack/idl/counter_mpl_stack.json

@@ -0,0 +1,42 @@
+{
+  "version": "0.1.0",
+  "name": "counter_mpl_stack",
+  "instructions": [
+    {
+      "name": "Increment",
+      "accounts": [
+        {
+          "name": "counter",
+          "isMut": true,
+          "isSigner": false,
+          "desc": "Counter account to increment"
+        }
+      ],
+      "args": [],
+      "discriminant": {
+        "type": "u8",
+        "value": 0
+      }
+    }
+  ],
+  "accounts": [
+    {
+      "name": "Counter",
+      "type": {
+        "kind": "struct",
+        "fields": [
+          {
+            "name": "count",
+            "type": "u64"
+          }
+        ]
+      }
+    }
+  ],
+  "metadata": {
+    "origin": "shank",
+    "address": "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS",
+    "binaryVersion": "0.0.8",
+    "libVersion": "0.0.8"
+  }
+}

+ 33 - 0
basics/counter/mpl-stack/idl/counter_solana_native.json

@@ -0,0 +1,33 @@
+{
+  "version": "0.1.0",
+  "name": "counter_solana_native",
+  "instructions": [
+    {
+      "name": "Increment",
+      "accounts": [],
+      "args": [],
+      "discriminant": {
+        "type": "u8",
+        "value": 0
+      }
+    }
+  ],
+  "accounts": [
+    {
+      "name": "Counter",
+      "type": {
+        "kind": "struct",
+        "fields": [
+          {
+            "name": "count",
+            "type": "u64"
+          }
+        ]
+      }
+    }
+  ],
+  "metadata": {
+    "origin": "shank",
+    "address": "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"
+  }
+}

+ 6 - 0
basics/counter/mpl-stack/jest.config.js

@@ -0,0 +1,6 @@
+module.exports = {
+    preset: 'ts-jest/presets/default',
+    testEnvironment: 'node',
+    testTimeout: 100000,
+    resolver: "ts-jest-resolver",
+};

+ 30 - 0
basics/counter/mpl-stack/package.json

@@ -0,0 +1,30 @@
+{
+  "name": "counter-mpl-stack",
+  "version": "0.1.0",
+  "description": "Counter program written using MPL tooling",
+  "main": "index.js",
+  "author": "ngundotra",
+  "license": "Apache-2.0",
+  "private": false,
+  "scripts": {
+    "start-validator": "solana-test-validator --reset --quiet --bpf-program Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS ./target/deploy/counter_solana_native.so",
+    "run-tests": "jest tests --detectOpenHandles",
+    "test": "start-server-and-test start-validator http://localhost:8899/health run-tests"
+  },
+  "devDependencies": {
+    "@types/bn.js": "^5.1.1",
+    "@types/jest": "^29.0.0",
+    "chai": "^4.3.6",
+    "jest": "^29.0.2",
+    "start-server-and-test": "^1.14.0",
+    "ts-jest": "^28.0.8",
+    "ts-jest-resolver": "^2.0.0",
+    "ts-node": "^10.9.1",
+    "typescript": "^4.8.2"
+  },
+  "dependencies": {
+    "@metaplex-foundation/beet": "^0.6.1",
+    "@metaplex-foundation/solita": "^0.15.2",
+    "@solana/web3.js": "^1.56.2"
+  }
+}

+ 67 - 0
basics/counter/mpl-stack/src/lib.rs

@@ -0,0 +1,67 @@
+use borsh::{BorshDeserialize, BorshSerialize};
+use shank::ShankInstruction;
+use solana_program::{
+    account_info::{next_account_info, AccountInfo},
+    declare_id,
+    entrypoint::ProgramResult,
+    msg,
+    program::{invoke, invoke_signed},
+    program_error::ProgramError,
+    pubkey::Pubkey,
+    system_instruction,
+};
+
+mod state;
+use state::*;
+
+declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
+
+#[cfg(not(feature = "no-entrypoint"))]
+use solana_program::entrypoint;
+
+#[cfg(not(feature = "no-entrypoint"))]
+entrypoint!(process_instruction);
+
+#[derive(ShankInstruction, BorshDeserialize, BorshSerialize)]
+pub enum Instruction {
+    #[account(0, writable, name = "counter", desc = "Counter account to increment")]
+    Increment,
+}
+
+pub fn process_instruction(
+    _program_id: &Pubkey,
+    accounts: &[AccountInfo],
+    instruction_data: &[u8],
+) -> ProgramResult {
+    let (instruction_discriminant, instruction_data_inner) = instruction_data.split_at(1);
+    match instruction_discriminant[0] {
+        0 => {
+            msg!("Instruction: Increment");
+            process_increment_counter(accounts, instruction_data_inner)?;
+        }
+        _ => {
+            msg!("Error: unknown instruction")
+        }
+    }
+    Ok(())
+}
+
+pub fn process_increment_counter(
+    accounts: &[AccountInfo],
+    instruction_data: &[u8],
+) -> Result<(), ProgramError> {
+    let account_info_iter = &mut accounts.iter();
+
+    let counter_account = next_account_info(account_info_iter)?;
+    assert!(
+        counter_account.is_writable,
+        "Counter account must be writable"
+    );
+
+    let mut counter = Counter::try_from_slice(&counter_account.try_borrow_mut_data()?)?;
+    counter.count += 1;
+    counter.serialize(&mut *counter_account.data.borrow_mut())?;
+
+    msg!("Counter state incremented to {:?}", counter.count);
+    Ok(())
+}

+ 7 - 0
basics/counter/mpl-stack/src/state.rs

@@ -0,0 +1,7 @@
+use borsh::{BorshDeserialize, BorshSerialize};
+use shank::ShankAccount;
+
+#[derive(ShankAccount, BorshSerialize, BorshDeserialize, Debug, Clone)]
+pub struct Counter {
+    pub count: u64,
+}

+ 119 - 0
basics/counter/mpl-stack/tests/counter.test.ts

@@ -0,0 +1,119 @@
+import {
+    Connection,
+    Keypair,
+    LAMPORTS_PER_SOL,
+    Transaction,
+    TransactionInstruction,
+    sendAndConfirmTransaction,
+    SystemProgram
+} from '@solana/web3.js';
+import {
+    bignum
+} from '@metaplex-foundation/beet';
+import { assert } from 'chai';
+import { BN } from 'bn.js';
+
+import {
+    createIncrementInstruction,
+    Counter,
+    PROGRAM_ID,
+} from '../ts';
+
+function convertBignumToNumber(bignum: bignum): number {
+    return new BN(bignum).toNumber();
+}
+
+describe("Counter Solana Native", () => {
+    const connection = new Connection("http://localhost:8899");
+
+    it("Test allocate counter + increment tx", async () => {
+        // Randomly generate our wallet
+        const payerKeypair = Keypair.generate();
+        const payer = payerKeypair.publicKey;
+
+        // Randomly generate the account key 
+        // to sign for setting up the Counter state
+        const counterKeypair = Keypair.generate();
+        const counter = counterKeypair.publicKey;
+
+        // Airdrop our wallet 1 Sol
+        await connection.requestAirdrop(payer, LAMPORTS_PER_SOL);
+
+        // Create a TransactionInstruction to interact with our counter program
+        const allocIx: TransactionInstruction = SystemProgram.createAccount({
+            fromPubkey: payer,
+            newAccountPubkey: counter,
+            lamports: await connection.getMinimumBalanceForRentExemption(Counter.byteSize),
+            space: Counter.byteSize,
+            programId: PROGRAM_ID
+        })
+        const incrementIx: TransactionInstruction = createIncrementInstruction({ counter });
+        let tx = new Transaction().add(allocIx).add(incrementIx);
+
+        // Explicitly set the feePayer to be our wallet (this is set to first signer by default)
+        tx.feePayer = payer;
+
+        // Fetch a "timestamp" so validators know this is a recent transaction
+        tx.recentBlockhash = (await connection.getLatestBlockhash('confirmed')).blockhash;
+
+        // Send transaction to network (local network)
+        await sendAndConfirmTransaction(
+            connection,
+            tx,
+            [payerKeypair, counterKeypair],
+            { skipPreflight: true, commitment: 'confirmed' }
+        );
+
+        // Get the counter account info from network
+        let count = (await Counter.fromAccountAddress(connection, counter)).count;
+        assert((new BN(count)).toNumber() === 1, "Expected count to have been 1");
+        console.log(`[alloc+increment] count is: ${count}`);
+    });
+    it("Test allocate tx and increment tx", async () => {
+        const payerKeypair = Keypair.generate();
+        const payer = payerKeypair.publicKey;
+
+        const counterKeypair = Keypair.generate();
+        const counter = counterKeypair.publicKey;
+
+        await connection.requestAirdrop(payer, LAMPORTS_PER_SOL);
+
+        // Check allocate tx
+        const allocIx: TransactionInstruction = SystemProgram.createAccount({
+            fromPubkey: payer,
+            newAccountPubkey: counter,
+            lamports: await connection.getMinimumBalanceForRentExemption(Counter.byteSize),
+            space: Counter.byteSize,
+            programId: PROGRAM_ID
+        })
+        let tx = new Transaction().add(allocIx);
+        tx.feePayer = payer;
+        tx.recentBlockhash = (await connection.getLatestBlockhash('confirmed')).blockhash;
+        await sendAndConfirmTransaction(
+            connection,
+            tx,
+            [payerKeypair, counterKeypair],
+            { skipPreflight: true, commitment: 'confirmed' }
+        );
+
+        let count = (await Counter.fromAccountAddress(connection, counter)).count;
+        assert(convertBignumToNumber(count) === 0, "Expected count to have been 0");
+        console.log(`[allocate] count is: ${count}`);
+
+        // Check increment tx
+        const incrementIx: TransactionInstruction = createIncrementInstruction({ counter });
+        tx = new Transaction().add(incrementIx);
+        tx.feePayer = payer;
+        tx.recentBlockhash = (await connection.getLatestBlockhash('confirmed')).blockhash;
+        await sendAndConfirmTransaction(
+            connection,
+            tx,
+            [payerKeypair],
+            { skipPreflight: true, commitment: 'confirmed' }
+        );
+
+        count = (await Counter.fromAccountAddress(connection, counter)).count;
+        assert(convertBignumToNumber(count) === 1, "Expected count to have been 1");
+        console.log(`[increment] count is: ${count}`);
+    })
+})

+ 156 - 0
basics/counter/mpl-stack/ts/generated/accounts/Counter.ts

@@ -0,0 +1,156 @@
+/**
+ * This code was GENERATED using the solita package.
+ * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality.
+ *
+ * See: https://github.com/metaplex-foundation/solita
+ */
+
+import * as beet from '@metaplex-foundation/beet'
+import * as web3 from '@solana/web3.js'
+import * as beetSolana from '@metaplex-foundation/beet-solana'
+
+/**
+ * Arguments used to create {@link Counter}
+ * @category Accounts
+ * @category generated
+ */
+export type CounterArgs = {
+  count: beet.bignum
+}
+/**
+ * Holds the data for the {@link Counter} Account and provides de/serialization
+ * functionality for that data
+ *
+ * @category Accounts
+ * @category generated
+ */
+export class Counter implements CounterArgs {
+  private constructor(readonly count: beet.bignum) { }
+
+  /**
+   * Creates a {@link Counter} instance from the provided args.
+   */
+  static fromArgs(args: CounterArgs) {
+    return new Counter(args.count)
+  }
+
+  /**
+   * Deserializes the {@link Counter} from the data of the provided {@link web3.AccountInfo}.
+   * @returns a tuple of the account data and the offset up to which the buffer was read to obtain it.
+   */
+  static fromAccountInfo(
+    accountInfo: web3.AccountInfo<Buffer>,
+    offset = 0
+  ): [Counter, number] {
+    return Counter.deserialize(accountInfo.data, offset)
+  }
+
+  /**
+   * Retrieves the account info from the provided address and deserializes
+   * the {@link Counter} from its data.
+   *
+   * @throws Error if no account info is found at the address or if deserialization fails
+   */
+  static async fromAccountAddress(
+    connection: web3.Connection,
+    address: web3.PublicKey
+  ): Promise<Counter> {
+    const accountInfo = await connection.getAccountInfo(address, { commitment: "confirmed" });
+    if (accountInfo == null) {
+      throw new Error(`Unable to find Counter account at ${address}`)
+    }
+    return Counter.fromAccountInfo(accountInfo, 0)[0]
+  }
+
+  /**
+   * Provides a {@link web3.Connection.getProgramAccounts} config builder,
+   * to fetch accounts matching filters that can be specified via that builder.
+   *
+   * @param programId - the program that owns the accounts we are filtering
+   */
+  static gpaBuilder(
+    programId: web3.PublicKey = new web3.PublicKey(
+      'Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS'
+    )
+  ) {
+    return beetSolana.GpaBuilder.fromStruct(programId, counterBeet)
+  }
+
+  /**
+   * Deserializes the {@link Counter} from the provided data Buffer.
+   * @returns a tuple of the account data and the offset up to which the buffer was read to obtain it.
+   */
+  static deserialize(buf: Buffer, offset = 0): [Counter, number] {
+    return counterBeet.deserialize(buf, offset)
+  }
+
+  /**
+   * Serializes the {@link Counter} into a Buffer.
+   * @returns a tuple of the created Buffer and the offset up to which the buffer was written to store it.
+   */
+  serialize(): [Buffer, number] {
+    return counterBeet.serialize(this)
+  }
+
+  /**
+   * Returns the byteSize of a {@link Buffer} holding the serialized data of
+   * {@link Counter}
+   */
+  static get byteSize() {
+    return counterBeet.byteSize
+  }
+
+  /**
+   * Fetches the minimum balance needed to exempt an account holding
+   * {@link Counter} data from rent
+   *
+   * @param connection used to retrieve the rent exemption information
+   */
+  static async getMinimumBalanceForRentExemption(
+    connection: web3.Connection,
+    commitment?: web3.Commitment
+  ): Promise<number> {
+    return connection.getMinimumBalanceForRentExemption(
+      Counter.byteSize,
+      commitment
+    )
+  }
+
+  /**
+   * Determines if the provided {@link Buffer} has the correct byte size to
+   * hold {@link Counter} data.
+   */
+  static hasCorrectByteSize(buf: Buffer, offset = 0) {
+    return buf.byteLength - offset === Counter.byteSize
+  }
+
+  /**
+   * Returns a readable version of {@link Counter} properties
+   * and can be used to convert to JSON and/or logging
+   */
+  pretty() {
+    return {
+      count: (() => {
+        const x = <{ toNumber: () => number }>this.count
+        if (typeof x.toNumber === 'function') {
+          try {
+            return x.toNumber()
+          } catch (_) {
+            return x
+          }
+        }
+        return x
+      })(),
+    }
+  }
+}
+
+/**
+ * @category Accounts
+ * @category generated
+ */
+export const counterBeet = new beet.BeetStruct<Counter, CounterArgs>(
+  [['count', beet.u64]],
+  Counter.fromArgs,
+  'Counter'
+)

+ 5 - 0
basics/counter/mpl-stack/ts/generated/accounts/index.ts

@@ -0,0 +1,5 @@
+export * from './Counter'
+
+import { Counter } from './Counter'
+
+export const accountProviders = { Counter }

+ 19 - 0
basics/counter/mpl-stack/ts/generated/index.ts

@@ -0,0 +1,19 @@
+import { PublicKey } from '@solana/web3.js'
+export * from './accounts'
+export * from './instructions'
+
+/**
+ * Program address
+ *
+ * @category constants
+ * @category generated
+ */
+export const PROGRAM_ADDRESS = 'Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS'
+
+/**
+ * Program public key
+ *
+ * @category constants
+ * @category generated
+ */
+export const PROGRAM_ID = new PublicKey(PROGRAM_ADDRESS)

+ 62 - 0
basics/counter/mpl-stack/ts/generated/instructions/Increment.ts

@@ -0,0 +1,62 @@
+/**
+ * This code was GENERATED using the solita package.
+ * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality.
+ *
+ * See: https://github.com/metaplex-foundation/solita
+ */
+
+import * as beet from '@metaplex-foundation/beet'
+import * as web3 from '@solana/web3.js'
+
+/**
+ * @category Instructions
+ * @category Increment
+ * @category generated
+ */
+export const IncrementStruct = new beet.BeetArgsStruct<{
+  instructionDiscriminator: number
+}>([['instructionDiscriminator', beet.u8]], 'IncrementInstructionArgs')
+/**
+ * Accounts required by the _Increment_ instruction
+ *
+ * @property [_writable_] counter Counter account to increment
+ * @category Instructions
+ * @category Increment
+ * @category generated
+ */
+export type IncrementInstructionAccounts = {
+  counter: web3.PublicKey
+}
+
+export const incrementInstructionDiscriminator = 0
+
+/**
+ * Creates a _Increment_ instruction.
+ *
+ * @param accounts that will be accessed while the instruction is processed
+ * @category Instructions
+ * @category Increment
+ * @category generated
+ */
+export function createIncrementInstruction(
+  accounts: IncrementInstructionAccounts,
+  programId = new web3.PublicKey('Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS')
+) {
+  const [data] = IncrementStruct.serialize({
+    instructionDiscriminator: incrementInstructionDiscriminator,
+  })
+  const keys: web3.AccountMeta[] = [
+    {
+      pubkey: accounts.counter,
+      isWritable: true,
+      isSigner: false,
+    },
+  ]
+
+  const ix = new web3.TransactionInstruction({
+    programId,
+    keys,
+    data,
+  })
+  return ix
+}

+ 1 - 0
basics/counter/mpl-stack/ts/generated/instructions/index.ts

@@ -0,0 +1 @@
+export * from './Increment'

+ 1 - 0
basics/counter/mpl-stack/ts/index.ts

@@ -0,0 +1 @@
+export * from './generated';