浏览代码

counter: add native program example

ngundotra 3 年之前
父节点
当前提交
540c9af3e2

+ 16 - 0
basics/counter/native/Cargo.toml

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

+ 14 - 0
basics/counter/native/README.md

@@ -0,0 +1,14 @@
+# Counter: Solana Native
+
+This example program is written in Solana using only the Solana toolsuite.
+
+## Setup
+
+1. Build the program with `cargo build-bpf`
+2. Run tests + local validator with `yarn test`
+
+## Debugging
+
+1. Start test validator with `yarn start-validator`
+2. Start listening to program logs with `solana config set -ul && solana logs`
+3. Run tests with `yarn run-tests`

+ 6 - 0
basics/counter/native/jest.config.js

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

+ 28 - 0
basics/counter/native/package.json

@@ -0,0 +1,28 @@
+{
+  "name": "counter-solana-native",
+  "version": "0.1.0",
+  "description": "Counter program written using only Solana 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": {
+    "@solana/web3.js": "^1.56.2"
+  }
+}

+ 60 - 0
basics/counter/native/src/lib.rs

@@ -0,0 +1,60 @@
+use borsh::{BorshDeserialize, BorshSerialize};
+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);
+
+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(())
+}

+ 6 - 0
basics/counter/native/src/state.rs

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

+ 123 - 0
basics/counter/native/tests/counter.test.ts

@@ -0,0 +1,123 @@
+import {
+    Connection,
+    Keypair,
+    LAMPORTS_PER_SOL,
+    Transaction,
+    TransactionInstruction,
+    sendAndConfirmTransaction,
+    SystemProgram
+} from '@solana/web3.js';
+import { assert } from 'chai';
+
+import {
+    createIncrementInstruction,
+    deserializeCounterAccount,
+    Counter,
+    COUNTER_ACCOUNT_SIZE,
+    PROGRAM_ID,
+} from '../ts';
+
+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_ACCOUNT_SIZE),
+            space: COUNTER_ACCOUNT_SIZE,
+            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
+        const counterAccountInfo = await connection.getAccountInfo(counter, { commitment: "confirmed" });
+        assert(counterAccountInfo, "Expected counter account to have been created");
+
+        // Deserialize the counter & check count has been incremented
+        const counterAccount = deserializeCounterAccount(counterAccountInfo.data);
+        assert(counterAccount.count.toNumber() === 1, "Expected count to have been 1");
+        console.log(`[alloc+increment] count is: ${counterAccount.count.toNumber()}`);
+    });
+    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_ACCOUNT_SIZE),
+            space: COUNTER_ACCOUNT_SIZE,
+            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 counterAccountInfo = await connection.getAccountInfo(counter, { commitment: "confirmed" });
+        assert(counterAccountInfo, "Expected counter account to have been created");
+
+        let counterAccount = deserializeCounterAccount(counterAccountInfo.data);
+        assert(counterAccount.count.toNumber() === 0, "Expected count to have been 0");
+        console.log(`[allocate] count is: ${counterAccount.count.toNumber()}`);
+
+        // 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' }
+        );
+
+        counterAccountInfo = await connection.getAccountInfo(counter, { commitment: "confirmed" });
+        assert(counterAccountInfo, "Expected counter account to have been created");
+
+        counterAccount = deserializeCounterAccount(counterAccountInfo.data);
+        assert(counterAccount.count.toNumber() === 1, "Expected count to have been 1");
+        console.log(`[increment] count is: ${counterAccount.count.toNumber()}`);
+    })
+})

+ 17 - 0
basics/counter/native/ts/accounts/counter.ts

@@ -0,0 +1,17 @@
+import * as BN from 'bn.js';
+
+export type Counter = {
+    count: BN
+}
+
+export const COUNTER_ACCOUNT_SIZE = 8;
+
+export function deserializeCounterAccount(data: Buffer): Counter {
+    if (data.byteLength !== 8) {
+        throw Error("Need exactly 8 bytes to deserialize counter")
+    }
+
+    return {
+        count: new BN(data, 'le')
+    }
+}

+ 1 - 0
basics/counter/native/ts/accounts/index.ts

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

+ 5 - 0
basics/counter/native/ts/index.ts

@@ -0,0 +1,5 @@
+import { PublicKey } from '@solana/web3.js';
+export * from './instructions';
+export * from './accounts';
+
+export const PROGRAM_ID = new PublicKey("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

+ 25 - 0
basics/counter/native/ts/instructions/createIncrementInstruction.ts

@@ -0,0 +1,25 @@
+import { PublicKey, TransactionInstruction } from '@solana/web3.js';
+import { PROGRAM_ID } from '../';
+
+export type IncrementInstructionAccounts = {
+    counter: PublicKey,
+}
+export type IncrementInstructionArgs = {
+}
+
+export function createIncrementInstruction(
+    accounts: IncrementInstructionAccounts,
+    args: IncrementInstructionArgs
+): TransactionInstruction {
+    return new TransactionInstruction({
+        programId: PROGRAM_ID,
+        keys: [
+            {
+                pubkey: accounts.counter,
+                isSigner: false,
+                isWritable: true
+            }
+        ],
+        data: Buffer.from([0x0])
+    })
+}

+ 1 - 0
basics/counter/native/ts/instructions/index.ts

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