From c69a7bb529c36109eaa8d594ee1ff85f431d5c3e Mon Sep 17 00:00:00 2001 From: 0xzrf Date: Sat, 11 Oct 2025 12:17:18 +0530 Subject: [PATCH 1/2] feat: Added pinocchio counter program --- Cargo.toml | 3 +- basics/counter/pinocchio/package.json | 22 ++++ basics/counter/pinocchio/program/Cargo.toml | 14 +++ .../counter/pinocchio/program/src/helpers.rs | 19 +++ .../src/instructions/create_counter.rs | 68 ++++++++++ .../src/instructions/increment_counter.rs | 60 +++++++++ .../pinocchio/program/src/instructions/mod.rs | 20 +++ basics/counter/pinocchio/program/src/lib.rs | 16 +++ .../pinocchio/program/src/processor.rs | 23 ++++ .../pinocchio/program/src/states/counter.rs | 12 ++ .../pinocchio/program/src/states/mod.rs | 2 + basics/counter/pinocchio/test/counter.test.ts | 118 ++++++++++++++++++ .../counter/pinocchio/tests/counter.test.ts | 118 ++++++++++++++++++ basics/counter/pinocchio/tests/package.json | 22 ++++ .../pinocchio/tests/test/counter.test.ts | 118 ++++++++++++++++++ basics/counter/pinocchio/tests/tsconfig.json | 10 ++ basics/counter/pinocchio/tsconfig.json | 10 ++ 17 files changed, 654 insertions(+), 1 deletion(-) create mode 100644 basics/counter/pinocchio/package.json create mode 100644 basics/counter/pinocchio/program/Cargo.toml create mode 100644 basics/counter/pinocchio/program/src/helpers.rs create mode 100644 basics/counter/pinocchio/program/src/instructions/create_counter.rs create mode 100644 basics/counter/pinocchio/program/src/instructions/increment_counter.rs create mode 100644 basics/counter/pinocchio/program/src/instructions/mod.rs create mode 100644 basics/counter/pinocchio/program/src/lib.rs create mode 100644 basics/counter/pinocchio/program/src/processor.rs create mode 100644 basics/counter/pinocchio/program/src/states/counter.rs create mode 100644 basics/counter/pinocchio/program/src/states/mod.rs create mode 100644 basics/counter/pinocchio/test/counter.test.ts create mode 100644 basics/counter/pinocchio/tests/counter.test.ts create mode 100644 basics/counter/pinocchio/tests/package.json create mode 100644 basics/counter/pinocchio/tests/test/counter.test.ts create mode 100644 basics/counter/pinocchio/tests/tsconfig.json create mode 100644 basics/counter/pinocchio/tsconfig.json diff --git a/Cargo.toml b/Cargo.toml index 12a30c4d0..e6966769b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "basics/counter/native/program", "basics/counter/anchor/programs/counter_anchor", "basics/counter/mpl-stack", + "basics/counter/pinocchio/program", "basics/create-account/native/program", "basics/create-account/anchor/programs/create-system-account", "basics/cross-program-invocation/anchor/programs/*", @@ -35,7 +36,7 @@ members = [ "tokens/token-2022/non-transferable/native/program", "tokens/token-2022/default-account-state/native/program", "tokens/token-2022/transfer-fee/native/program", - "tokens/token-2022/multiple-extensions/native/program", + "tokens/token-2022/multiple-extensions/native/program", "basics/counter/pinocchio", "basics/counter/pinocchio/pinocchio-counter", ] resolver = "2" diff --git a/basics/counter/pinocchio/package.json b/basics/counter/pinocchio/package.json new file mode 100644 index 000000000..414610d47 --- /dev/null +++ b/basics/counter/pinocchio/package.json @@ -0,0 +1,22 @@ +{ + "scripts": { + "test": "pnpm ts-mocha -p ./tsconfig.json -t 1000000 ./test/counter.test.ts", + "build-and-test": "cargo build-sbf --manifest-path=./program/Cargo.toml --sbf-out-dir=./tests/fixtures && pnpm test", + "build": "cargo build-sbf --manifest-path=./program/Cargo.toml --sbf-out-dir=./program/target/so", + "deploy": "solana program deploy ./program/target/so/hello_solana_program_pinocchio.so" + }, + "dependencies": { + "@solana/web3.js": "^1.47.3" + }, + "devDependencies": { + "@types/bn.js": "^5.1.0", + "@types/chai": "^4.3.1", + "@types/mocha": "^9.1.1", + "@types/node": "^22.15.2", + "chai": "^4.3.4", + "mocha": "^9.0.3", + "solana-bankrun": "^0.3.0", + "ts-mocha": "^10.0.0", + "typescript": "^4.3.5" + } +} diff --git a/basics/counter/pinocchio/program/Cargo.toml b/basics/counter/pinocchio/program/Cargo.toml new file mode 100644 index 000000000..6116565fb --- /dev/null +++ b/basics/counter/pinocchio/program/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "pinocchio-counter" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] + +[dependencies] +pinocchio = "0.9.2" +pinocchio-log = "0.5.1" +pinocchio-pubkey = "0.3.0" +bytemuck = {version = "1.23.2", features = ["derive"]} +pinocchio-system = "0.3.0" diff --git a/basics/counter/pinocchio/program/src/helpers.rs b/basics/counter/pinocchio/program/src/helpers.rs new file mode 100644 index 000000000..ce8a4ea5b --- /dev/null +++ b/basics/counter/pinocchio/program/src/helpers.rs @@ -0,0 +1,19 @@ +use bytemuck::{Pod, Zeroable}; +use pinocchio::{account_info::AccountInfo, msg, program_error::ProgramError}; + +pub fn load(account: &AccountInfo) -> Result<&mut T, ProgramError> +where + T: Pod + Zeroable, +{ + let data = unsafe { account.borrow_mut_data_unchecked() }; + + bytemuck::try_from_bytes_mut::(data).map_err(|_| ProgramError::InvalidAccountData) +} + +pub fn require(condition: bool, err: ProgramError, msg: &'static str) -> Result<(), ProgramError> { + if !condition { + msg!(msg); + return Err(err); + } + Ok(()) +} diff --git a/basics/counter/pinocchio/program/src/instructions/create_counter.rs b/basics/counter/pinocchio/program/src/instructions/create_counter.rs new file mode 100644 index 000000000..8d84ffcb5 --- /dev/null +++ b/basics/counter/pinocchio/program/src/instructions/create_counter.rs @@ -0,0 +1,68 @@ +use crate::{load, require, states::Counter}; +use { + pinocchio::{ + account_info::AccountInfo, + instruction::{Seed, Signer}, + program_error::ProgramError, + pubkey::{find_program_address, pubkey_eq, Pubkey}, + sysvars::{rent::Rent, Sysvar}, + ProgramResult, + }, + pinocchio_log::log, + pinocchio_system::instructions::CreateAccount, +}; +pub fn process_create_counter(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + log!("Counter Instruction: CreateCounter"); + validate(program_id, accounts)?; + + if let [creator, counter_pda, _system_program] = accounts { + let counter_seeds = &[Counter::PREFIX]; + + let (expected_counter, bump) = find_program_address(counter_seeds, program_id); + + require( + pubkey_eq(&expected_counter, counter_pda.key()), + ProgramError::IncorrectProgramId, + "Validation Error: Seed constraints violated", + )?; + let curve_bump: &[u8] = &[bump]; + let seeds = [Seed::from(Counter::PREFIX), Seed::from(curve_bump)]; + let signer = Signer::from(&seeds); + + CreateAccount { + from: creator, + lamports: Rent::get()?.minimum_balance(Counter::SIZE), + owner: program_id, + space: Counter::SIZE as u64, + to: counter_pda, + } + .invoke_signed(&[signer])?; + + let counter = load::(counter_pda)?; + + counter.count = 0; + + Ok(()) + } else { + Err(ProgramError::NotEnoughAccountKeys) + } +} +pub fn validate(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + if let [creator, counter_pda, _system_program] = accounts { + require( + creator.is_signer(), + ProgramError::MissingRequiredSignature, + "Validation Error: Creator must be a signer", + )?; + + require( + counter_pda.is_writable(), + ProgramError::InvalidAccountData, + "Validation Error: Counter program Writable priviledge escalated", + )?; + + Ok(()) + } else { + Err(ProgramError::NotEnoughAccountKeys) + } +} diff --git a/basics/counter/pinocchio/program/src/instructions/increment_counter.rs b/basics/counter/pinocchio/program/src/instructions/increment_counter.rs new file mode 100644 index 000000000..29d021130 --- /dev/null +++ b/basics/counter/pinocchio/program/src/instructions/increment_counter.rs @@ -0,0 +1,60 @@ +use crate::{load, require, states::Counter}; +use { + pinocchio::{ + account_info::AccountInfo, + program_error::ProgramError, + pubkey::{find_program_address, pubkey_eq, Pubkey}, + ProgramResult, + }, + pinocchio_log::log, +}; + +pub fn process_increment_counter(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + log!("Counter Instruction: IncrementCounter"); + validate(program_id, accounts)?; + + if let [_user, counter_pda] = accounts { + let counter = load::(counter_pda)?; + + counter.count += 1; + Ok(()) + } else { + Err(ProgramError::NotEnoughAccountKeys) + } +} + +pub fn validate(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + if let [user, counter_pda] = accounts { + require( + user.is_signer(), + ProgramError::MissingRequiredSignature, + "Validation Error: User must be a signer", + )?; + + let counter_seeds = &[Counter::PREFIX]; + + let (expected_counter, _) = find_program_address(counter_seeds, program_id); + + require( + pubkey_eq(&expected_counter, counter_pda.key()), + ProgramError::IncorrectProgramId, + "Validation Error: Seed constraints violated", + )?; + + require( + counter_pda.data_len() == Counter::SIZE, + ProgramError::UninitializedAccount, + "Validation Error: Counter isn't initialized yet", + )?; + + require( + counter_pda.is_writable(), + ProgramError::InvalidAccountData, + "Validation Error: Counter program Writable priviledge escalated", + )?; + + Ok(()) + } else { + Err(ProgramError::NotEnoughAccountKeys) + } +} diff --git a/basics/counter/pinocchio/program/src/instructions/mod.rs b/basics/counter/pinocchio/program/src/instructions/mod.rs new file mode 100644 index 000000000..63c8dedb3 --- /dev/null +++ b/basics/counter/pinocchio/program/src/instructions/mod.rs @@ -0,0 +1,20 @@ +use pinocchio::program_error::ProgramError; +pub mod create_counter; +pub mod increment_counter; +#[repr(u8)] +pub enum CounterInstructions { + CreateCounter, + Increment, +} + +impl TryFrom<&u8> for CounterInstructions { + type Error = ProgramError; + + fn try_from(value: &u8) -> Result { + match *value { + 0 => Ok(CounterInstructions::CreateCounter), + 1 => Ok(CounterInstructions::Increment), + _ => Err(ProgramError::InvalidInstructionData), + } + } +} diff --git a/basics/counter/pinocchio/program/src/lib.rs b/basics/counter/pinocchio/program/src/lib.rs new file mode 100644 index 000000000..70c494607 --- /dev/null +++ b/basics/counter/pinocchio/program/src/lib.rs @@ -0,0 +1,16 @@ +#![no_std] +use pinocchio::{no_allocator, nostd_panic_handler, program_entrypoint}; +use pinocchio_pubkey::declare_id; + +pub mod helpers; +pub mod instructions; +pub mod processor; +pub mod states; +pub use helpers::*; +use processor::*; + +program_entrypoint!(process_instructions); +nostd_panic_handler!(); +no_allocator!(); + +declare_id!("8TpdLD58VBWsdzxRi2yRcmKJD9UcE2GuUrBwsyCwpbUN"); diff --git a/basics/counter/pinocchio/program/src/processor.rs b/basics/counter/pinocchio/program/src/processor.rs new file mode 100644 index 000000000..16c21ff98 --- /dev/null +++ b/basics/counter/pinocchio/program/src/processor.rs @@ -0,0 +1,23 @@ +use crate::instructions::{ + create_counter::process_create_counter, increment_counter::process_increment_counter, + CounterInstructions, +}; +use pinocchio::{ + account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey, ProgramResult, +}; + +pub fn process_instructions( + program_id: &Pubkey, + accounts: &[AccountInfo], + ix_data: &[u8], +) -> ProgramResult { + let (disc, _) = ix_data + .split_first() + .ok_or(ProgramError::InvalidInstructionData)?; + + match CounterInstructions::try_from(disc)? { + CounterInstructions::CreateCounter => process_create_counter(program_id, accounts)?, + CounterInstructions::Increment => process_increment_counter(program_id, accounts)?, + } + Ok(()) +} diff --git a/basics/counter/pinocchio/program/src/states/counter.rs b/basics/counter/pinocchio/program/src/states/counter.rs new file mode 100644 index 000000000..990ce4bd9 --- /dev/null +++ b/basics/counter/pinocchio/program/src/states/counter.rs @@ -0,0 +1,12 @@ +use bytemuck::{Pod, Zeroable}; + +#[repr(C)] +#[derive(Clone, Copy, Zeroable, Pod)] +pub struct Counter { + pub count: u64, +} + +impl Counter { + pub const PREFIX: &[u8] = b"counter"; + pub const SIZE: usize = core::mem::size_of::(); +} diff --git a/basics/counter/pinocchio/program/src/states/mod.rs b/basics/counter/pinocchio/program/src/states/mod.rs new file mode 100644 index 000000000..90e5744c7 --- /dev/null +++ b/basics/counter/pinocchio/program/src/states/mod.rs @@ -0,0 +1,2 @@ +pub mod counter; +pub use counter::*; diff --git a/basics/counter/pinocchio/test/counter.test.ts b/basics/counter/pinocchio/test/counter.test.ts new file mode 100644 index 000000000..297c25f15 --- /dev/null +++ b/basics/counter/pinocchio/test/counter.test.ts @@ -0,0 +1,118 @@ +import { + PublicKey, + Transaction, + TransactionInstruction, + SystemProgram, +} from "@solana/web3.js"; +import { assert } from "chai"; +import { test } from "mocha"; +import { ProgramTestContext, start } from "solana-bankrun"; + +describe("Pinocchio-Counter", () => { + const programId = new PublicKey( + "8TpdLD58VBWsdzxRi2yRcmKJD9UcE2GuUrBwsyCwpbUN", + ); + + test("Test Create counter ix works", async () => { + const context = await start( + [{ name: "program/target/deploy/pinocchio_counter", programId }], + [], + ); + + const client = context.banksClient; + const payer = context.payer; + const blockhash = context.lastBlockhash; + + const counter = PublicKey.findProgramAddressSync( + [Buffer.from("counter")], + programId, + )[0]; + + console.log( + "Payer::", + payer.publicKey.toString(), + "\nCounter", + counter.toString(), + ); + const ixs = [ + new TransactionInstruction({ + programId, + keys: [ + { pubkey: payer.publicKey, isSigner: true, isWritable: true }, + { pubkey: counter, isSigner: false, isWritable: true }, + { + pubkey: SystemProgram.programId, + isSigner: false, + isWritable: false, + }, + ], + data: Buffer.from([0]), + }), + ]; + + const tx = new Transaction(); + tx.recentBlockhash = blockhash; + tx.add(...ixs); + tx.sign(payer); + + await client.simulateTransaction(tx); + await client.processTransaction(tx); + }); + + test("Test Increment counter ix works", async () => { + const counter = PublicKey.findProgramAddressSync( + [Buffer.from("counter")], + programId, + )[0]; + + let counterData = Buffer.alloc( + 8, + Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 0]), // The count value being 0 + ); + + const context = await start( + [{ name: "pinocchio_counter", programId }], + [ + { + address: counter, + info: { + lamports: 1000_0000, + owner: programId, + executable: false, + data: counterData, // TODO + }, + }, + ], + ); + + const client = context.banksClient; + const payer = context.payer; + const blockhash = context.lastBlockhash; + + const ixs = [ + new TransactionInstruction({ + programId, + keys: [ + { pubkey: payer.publicKey, isSigner: true, isWritable: true }, + { pubkey: counter, isSigner: false, isWritable: true }, + ], + data: Buffer.from([1]), + }), + ]; + + const tx = new Transaction(); + tx.recentBlockhash = blockhash; + tx.add(...ixs); + tx.sign(payer); + + await client.simulateTransaction(tx); + await client.processTransaction(tx); + + const counterAcc = await client.getAccount(counter); + + assert( + counterAcc.data.toString() == + Uint8Array.from([1, 0, 0, 0, 0, 0, 0, 0]).toString(), + ); + }); +}); diff --git a/basics/counter/pinocchio/tests/counter.test.ts b/basics/counter/pinocchio/tests/counter.test.ts new file mode 100644 index 000000000..9439978a0 --- /dev/null +++ b/basics/counter/pinocchio/tests/counter.test.ts @@ -0,0 +1,118 @@ +import { + PublicKey, + Transaction, + TransactionInstruction, + SystemProgram, +} from "@solana/web3.js"; +import { assert } from "chai"; +import { test } from "mocha"; +import { ProgramTestContext, start } from "solana-bankrun"; + +describe("Pinocchio-Counter", () => { + const programId = new PublicKey( + "8TpdLD58VBWsdzxRi2yRcmKJD9UcE2GuUrBwsyCwpbUN", + ); + + test("Test Create counter ix works", async () => { + const context = await start( + [{ name: "../program/target/deploy/pinocchio_counter", programId }], + [], + ); + + const client = context.banksClient; + const payer = context.payer; + const blockhash = context.lastBlockhash; + + const counter = PublicKey.findProgramAddressSync( + [Buffer.from("counter")], + programId, + )[0]; + + console.log( + "Payer::", + payer.publicKey.toString(), + "\nCounter", + counter.toString(), + ); + const ixs = [ + new TransactionInstruction({ + programId, + keys: [ + { pubkey: payer.publicKey, isSigner: true, isWritable: true }, + { pubkey: counter, isSigner: false, isWritable: true }, + { + pubkey: SystemProgram.programId, + isSigner: false, + isWritable: false, + }, + ], + data: Buffer.from([0]), + }), + ]; + + const tx = new Transaction(); + tx.recentBlockhash = blockhash; + tx.add(...ixs); + tx.sign(payer); + + await client.simulateTransaction(tx); + await client.processTransaction(tx); + }); + + test("Test Increment counter ix works", async () => { + const counter = PublicKey.findProgramAddressSync( + [Buffer.from("counter")], + programId, + )[0]; + + let counterData = Buffer.alloc( + 8, + Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 0]), // The count value being 0 + ); + + const context = await start( + [{ name: "pinocchio_counter", programId }], + [ + { + address: counter, + info: { + lamports: 1000_0000, + owner: programId, + executable: false, + data: counterData, // TODO + }, + }, + ], + ); + + const client = context.banksClient; + const payer = context.payer; + const blockhash = context.lastBlockhash; + + const ixs = [ + new TransactionInstruction({ + programId, + keys: [ + { pubkey: payer.publicKey, isSigner: true, isWritable: true }, + { pubkey: counter, isSigner: false, isWritable: true }, + ], + data: Buffer.from([1]), + }), + ]; + + const tx = new Transaction(); + tx.recentBlockhash = blockhash; + tx.add(...ixs); + tx.sign(payer); + + await client.simulateTransaction(tx); + await client.processTransaction(tx); + + const counterAcc = await client.getAccount(counter); + + assert( + counterAcc.data.toString() == + Uint8Array.from([1, 0, 0, 0, 0, 0, 0, 0]).toString(), + ); + }); +}); diff --git a/basics/counter/pinocchio/tests/package.json b/basics/counter/pinocchio/tests/package.json new file mode 100644 index 000000000..db883bcbf --- /dev/null +++ b/basics/counter/pinocchio/tests/package.json @@ -0,0 +1,22 @@ +{ + "scripts": { + "test": "pnpm ts-mocha -p ./tsconfig.json -t 1000000 ./tests/counter.test.ts", + "build-and-test": "cargo build-sbf --manifest-path=./program/Cargo.toml --sbf-out-dir=./tests/fixtures && pnpm test", + "build": "cargo build-sbf --manifest-path=./program/Cargo.toml --sbf-out-dir=./program/target/so", + "deploy": "solana program deploy ./program/target/so/hello_solana_program_pinocchio.so" + }, + "dependencies": { + "@solana/web3.js": "^1.47.3" + }, + "devDependencies": { + "@types/bn.js": "^5.1.0", + "@types/chai": "^4.3.1", + "@types/mocha": "^9.1.1", + "@types/node": "^22.15.2", + "chai": "^4.3.4", + "mocha": "^9.0.3", + "solana-bankrun": "^0.3.0", + "ts-mocha": "^10.0.0", + "typescript": "^4.3.5" + } +} diff --git a/basics/counter/pinocchio/tests/test/counter.test.ts b/basics/counter/pinocchio/tests/test/counter.test.ts new file mode 100644 index 000000000..9439978a0 --- /dev/null +++ b/basics/counter/pinocchio/tests/test/counter.test.ts @@ -0,0 +1,118 @@ +import { + PublicKey, + Transaction, + TransactionInstruction, + SystemProgram, +} from "@solana/web3.js"; +import { assert } from "chai"; +import { test } from "mocha"; +import { ProgramTestContext, start } from "solana-bankrun"; + +describe("Pinocchio-Counter", () => { + const programId = new PublicKey( + "8TpdLD58VBWsdzxRi2yRcmKJD9UcE2GuUrBwsyCwpbUN", + ); + + test("Test Create counter ix works", async () => { + const context = await start( + [{ name: "../program/target/deploy/pinocchio_counter", programId }], + [], + ); + + const client = context.banksClient; + const payer = context.payer; + const blockhash = context.lastBlockhash; + + const counter = PublicKey.findProgramAddressSync( + [Buffer.from("counter")], + programId, + )[0]; + + console.log( + "Payer::", + payer.publicKey.toString(), + "\nCounter", + counter.toString(), + ); + const ixs = [ + new TransactionInstruction({ + programId, + keys: [ + { pubkey: payer.publicKey, isSigner: true, isWritable: true }, + { pubkey: counter, isSigner: false, isWritable: true }, + { + pubkey: SystemProgram.programId, + isSigner: false, + isWritable: false, + }, + ], + data: Buffer.from([0]), + }), + ]; + + const tx = new Transaction(); + tx.recentBlockhash = blockhash; + tx.add(...ixs); + tx.sign(payer); + + await client.simulateTransaction(tx); + await client.processTransaction(tx); + }); + + test("Test Increment counter ix works", async () => { + const counter = PublicKey.findProgramAddressSync( + [Buffer.from("counter")], + programId, + )[0]; + + let counterData = Buffer.alloc( + 8, + Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 0]), // The count value being 0 + ); + + const context = await start( + [{ name: "pinocchio_counter", programId }], + [ + { + address: counter, + info: { + lamports: 1000_0000, + owner: programId, + executable: false, + data: counterData, // TODO + }, + }, + ], + ); + + const client = context.banksClient; + const payer = context.payer; + const blockhash = context.lastBlockhash; + + const ixs = [ + new TransactionInstruction({ + programId, + keys: [ + { pubkey: payer.publicKey, isSigner: true, isWritable: true }, + { pubkey: counter, isSigner: false, isWritable: true }, + ], + data: Buffer.from([1]), + }), + ]; + + const tx = new Transaction(); + tx.recentBlockhash = blockhash; + tx.add(...ixs); + tx.sign(payer); + + await client.simulateTransaction(tx); + await client.processTransaction(tx); + + const counterAcc = await client.getAccount(counter); + + assert( + counterAcc.data.toString() == + Uint8Array.from([1, 0, 0, 0, 0, 0, 0, 0]).toString(), + ); + }); +}); diff --git a/basics/counter/pinocchio/tests/tsconfig.json b/basics/counter/pinocchio/tests/tsconfig.json new file mode 100644 index 000000000..8c20b2236 --- /dev/null +++ b/basics/counter/pinocchio/tests/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "types": ["mocha", "chai", "node"], + "typeRoots": ["./node_modules/@types"], + "lib": ["es2015"], + "module": "commonjs", + "target": "es6", + "esModuleInterop": true + } +} diff --git a/basics/counter/pinocchio/tsconfig.json b/basics/counter/pinocchio/tsconfig.json new file mode 100644 index 000000000..8c20b2236 --- /dev/null +++ b/basics/counter/pinocchio/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "types": ["mocha", "chai", "node"], + "typeRoots": ["./node_modules/@types"], + "lib": ["es2015"], + "module": "commonjs", + "target": "es6", + "esModuleInterop": true + } +} From 2e54ff1c7b6de4e48636627285620a5a41bccb62 Mon Sep 17 00:00:00 2001 From: 0xzrf Date: Sun, 12 Oct 2025 14:05:08 +0530 Subject: [PATCH 2/2] feat: Added pinocchio version of transfer-sol program example --- Cargo.toml | 1 + basics/transfer-sol/pinocchio/package.json | 22 ++++ .../transfer-sol/pinocchio/program/Cargo.toml | 13 +++ .../program/src/instructions/cpi_transfer.rs | 39 +++++++ .../pinocchio/program/src/instructions/mod.rs | 19 ++++ .../transfer-sol/pinocchio/program/src/lib.rs | 12 ++ .../pinocchio/program/src/processor.rs | 22 ++++ .../pinocchio/test/transferSol.test.ts | 106 ++++++++++++++++++ basics/transfer-sol/pinocchio/tsconfig.json | 10 ++ 9 files changed, 244 insertions(+) create mode 100644 basics/transfer-sol/pinocchio/package.json create mode 100644 basics/transfer-sol/pinocchio/program/Cargo.toml create mode 100644 basics/transfer-sol/pinocchio/program/src/instructions/cpi_transfer.rs create mode 100644 basics/transfer-sol/pinocchio/program/src/instructions/mod.rs create mode 100644 basics/transfer-sol/pinocchio/program/src/lib.rs create mode 100644 basics/transfer-sol/pinocchio/program/src/processor.rs create mode 100644 basics/transfer-sol/pinocchio/test/transferSol.test.ts create mode 100644 basics/transfer-sol/pinocchio/tsconfig.json diff --git a/Cargo.toml b/Cargo.toml index e6966769b..e6481ec1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ members = [ "basics/repository-layout/anchor/programs/*", "basics/transfer-sol/native/program", "basics/transfer-sol/anchor/programs/*", + "basics/transfer-sol/pinocchio/program", "tokens/token-2022/mint-close-authority/native/program", "tokens/token-2022/non-transferable/native/program", "tokens/token-2022/default-account-state/native/program", diff --git a/basics/transfer-sol/pinocchio/package.json b/basics/transfer-sol/pinocchio/package.json new file mode 100644 index 000000000..4a36de844 --- /dev/null +++ b/basics/transfer-sol/pinocchio/package.json @@ -0,0 +1,22 @@ +{ + "scripts": { + "test": "pnpm ts-mocha -p ./tsconfig.json -t 1000000 ./test/transferSol.test.ts", + "build-and-test": "cargo build-sbf --manifest-path=./program/Cargo.toml --sbf-out-dir=./tests/fixtures && pnpm test", + "build": "cargo build-sbf --manifest-path=./program/Cargo.toml --sbf-out-dir=./program/target/so", + "deploy": "solana program deploy ./program/target/so/hello_solana_program_pinocchio.so" + }, + "dependencies": { + "@solana/web3.js": "^1.47.3" + }, + "devDependencies": { + "@types/bn.js": "^5.1.0", + "@types/chai": "^4.3.1", + "@types/mocha": "^9.1.1", + "@types/node": "^22.15.2", + "chai": "^4.3.4", + "mocha": "^9.0.3", + "solana-bankrun": "^0.3.0", + "ts-mocha": "^10.0.0", + "typescript": "^4.3.5" + } +} diff --git a/basics/transfer-sol/pinocchio/program/Cargo.toml b/basics/transfer-sol/pinocchio/program/Cargo.toml new file mode 100644 index 000000000..a66a34172 --- /dev/null +++ b/basics/transfer-sol/pinocchio/program/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "pinocchio-transfer-sol" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] + +[dependencies] +bytemuck = {version = "1.24.0", features = ["derive"]} +pinocchio = "0.9.2" +pinocchio-pubkey = "0.3.0" +pinocchio-system = "0.3.0" diff --git a/basics/transfer-sol/pinocchio/program/src/instructions/cpi_transfer.rs b/basics/transfer-sol/pinocchio/program/src/instructions/cpi_transfer.rs new file mode 100644 index 000000000..959e8982c --- /dev/null +++ b/basics/transfer-sol/pinocchio/program/src/instructions/cpi_transfer.rs @@ -0,0 +1,39 @@ +use bytemuck::{Pod, Zeroable}; +use { + pinocchio::{ + account_info::AccountInfo, msg, program_error::ProgramError, pubkey::Pubkey, ProgramResult, + }, + pinocchio_system::instructions::Transfer, +}; +#[repr(C)] +#[derive(Pod, Zeroable, Clone, Copy)] +pub struct CpiTransferArgs { + amount: u64, +} + +pub fn process_cpi_transfer( + _program_id: &Pubkey, + accounts: &[AccountInfo], + ix_data: &[u8], +) -> ProgramResult { + msg!("TransferSol Instruction: CpiTransfer"); + if let [sender, receiver, _system_program] = accounts { + let mut aligned_ix_buf = [0u8; core::mem::size_of::()]; // putting raw ix_data will fail since it started at index 1 of the original instruction_data, so this new allocation is required + + aligned_ix_buf.copy_from_slice(ix_data); + + let params = bytemuck::try_from_bytes::(&aligned_ix_buf) + .map_err(|_| ProgramError::InvalidInstructionData)?; + + // sol_log_64(params.amount, 0, 0, 0, 0); + + Transfer { + from: sender, + lamports: params.amount, + to: receiver, + } + .invoke()?; + } + + Ok(()) +} diff --git a/basics/transfer-sol/pinocchio/program/src/instructions/mod.rs b/basics/transfer-sol/pinocchio/program/src/instructions/mod.rs new file mode 100644 index 000000000..4390b5ca6 --- /dev/null +++ b/basics/transfer-sol/pinocchio/program/src/instructions/mod.rs @@ -0,0 +1,19 @@ +use pinocchio::program_error::ProgramError; + +pub mod cpi_transfer; + +#[repr(u8)] +pub enum TransferSolInstructions { + CpiTransfer, +} + +impl TryFrom<&u8> for TransferSolInstructions { + type Error = ProgramError; + + fn try_from(value: &u8) -> Result { + match *value { + 0 => Ok(TransferSolInstructions::CpiTransfer), + _ => Err(ProgramError::InvalidInstructionData), + } + } +} diff --git a/basics/transfer-sol/pinocchio/program/src/lib.rs b/basics/transfer-sol/pinocchio/program/src/lib.rs new file mode 100644 index 000000000..3bddf98fb --- /dev/null +++ b/basics/transfer-sol/pinocchio/program/src/lib.rs @@ -0,0 +1,12 @@ +#![no_std] +use pinocchio::{no_allocator, nostd_panic_handler, program_entrypoint}; +use pinocchio_pubkey::declare_id; +mod processor; +use processor::process_instruction; +mod instructions; + +declare_id!("8TpdLD58VBWsdzxRi2yRcmKJD9UcE2GuUrBwsyCwpbUN"); + +program_entrypoint!(process_instruction); +no_allocator!(); +nostd_panic_handler!(); diff --git a/basics/transfer-sol/pinocchio/program/src/processor.rs b/basics/transfer-sol/pinocchio/program/src/processor.rs new file mode 100644 index 000000000..def480214 --- /dev/null +++ b/basics/transfer-sol/pinocchio/program/src/processor.rs @@ -0,0 +1,22 @@ +use crate::instructions::{cpi_transfer::*, TransferSolInstructions}; +use pinocchio::{ + account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey, ProgramResult, +}; + +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + ix_data: &[u8], +) -> ProgramResult { + let (disc, ix_args) = ix_data + .split_first() + .ok_or(ProgramError::InvalidInstructionData)?; + + match TransferSolInstructions::try_from(disc)? { + TransferSolInstructions::CpiTransfer => { + process_cpi_transfer(program_id, accounts, ix_args)? + } + } + + Ok(()) +} diff --git a/basics/transfer-sol/pinocchio/test/transferSol.test.ts b/basics/transfer-sol/pinocchio/test/transferSol.test.ts new file mode 100644 index 000000000..e14436938 --- /dev/null +++ b/basics/transfer-sol/pinocchio/test/transferSol.test.ts @@ -0,0 +1,106 @@ +import { + PublicKey, + Transaction, + TransactionInstruction, + Keypair, + SystemProgram, + LAMPORTS_PER_SOL, +} from "@solana/web3.js"; +import pkg from "@solana/web3.js"; +import { assert } from "chai"; +import { test } from "mocha"; +import { ProgramTestContext, start } from "solana-bankrun"; +import BN from "bn.js"; +describe("Transfer Sol Tests", () => { + const programId = new PublicKey( + "8TpdLD58VBWsdzxRi2yRcmKJD9UcE2GuUrBwsyCwpbUN", + ); + + test("Test Cpi Transfer Works", async () => { + const context = await start( + [{ programId, name: "./program/target/deploy/pinocchio_transfer_sol" }], + [], + ); + + const client = context.banksClient; + const payer = context.payer; + const blockhash = context.lastBlockhash; + + const param = { amount: LAMPORTS_PER_SOL }; + + const ix_data_buffer = Buffer.concat([ + Uint8Array.from([0]), + Uint8Array.from(new BN(param.amount).toArray("le", 8)), + ]); + const sender = Keypair.generate(); + const receiver = PublicKey.unique(); + + const initialCredit: any = { + fromPubkey: payer.publicKey, + lamports: 1.5 * LAMPORTS_PER_SOL, + toPubkey: sender.publicKey, + }; + + const ix1 = [SystemProgram.transfer(initialCredit)]; + + const tx1 = new Transaction(); + tx1.recentBlockhash = blockhash; + tx1.add(...ix1); + + tx1.sign(payer); + + await client.simulateTransaction(tx1); + await client.processTransaction(tx1); + const ix2 = [ + new TransactionInstruction({ + programId, + keys: [ + { pubkey: sender.publicKey, isSigner: true, isWritable: true }, + { pubkey: receiver, isSigner: false, isWritable: true }, + { + pubkey: SystemProgram.programId, + isSigner: false, + isWritable: false, + }, + ], + data: ix_data_buffer, + }), + ]; + + const tx2 = new Transaction(); + tx2.recentBlockhash = blockhash; + tx2.add(...ix2); + tx2.feePayer = payer.publicKey; + tx2.sign(sender, payer); + + const senderBalanceBefore = await client.getBalance( + sender.publicKey, + "finalized", + ); + await client.simulateTransaction(tx2, "finalized"); + const processedTxn = await client.processTransaction(tx2); + + assert( + processedTxn.logMessages[0].startsWith(`Program ${programId.toString()}`), + ); + + assert( + processedTxn.logMessages[processedTxn.logMessages.length - 1] == + `Program ${programId.toString()} success`, + ); + + const senderBalanceAfter = await client.getBalance( + sender.publicKey, + "finalized", + ); + const receiverBalanceAfter = await client.getBalance(receiver, "finalized"); + + assert(receiverBalanceAfter.toString() == param.amount.toString()); + + assert( + new BN(senderBalanceBefore) + .sub(new BN(senderBalanceAfter)) + .eq(new BN(param.amount)), + ); + }); +}); diff --git a/basics/transfer-sol/pinocchio/tsconfig.json b/basics/transfer-sol/pinocchio/tsconfig.json new file mode 100644 index 000000000..8c20b2236 --- /dev/null +++ b/basics/transfer-sol/pinocchio/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "types": ["mocha", "chai", "node"], + "typeRoots": ["./node_modules/@types"], + "lib": ["es2015"], + "module": "commonjs", + "target": "es6", + "esModuleInterop": true + } +}