25 min read
Overview
Pinocchio is a lightweight, zero-dependency library for writing Solana programs built for developers who want maximum performance without the overhead of larger frameworks.
In this guide, you’ll use Pinocchio to build a Vault program that lets users deposit and withdraw SOL to and from a Vault PDA. Along the way, you’ll use Shank, Codama, and the Solana Kit SDK to handle IDL generation, client scaffolding, and end-to-end testing.
What You Will Do
- Set up a Solana program with Pinocchio
- Develop a vault program with two instruction handlers:
DepositandWithdraw. Only the depositor can withdraw the funds from the vault. - Add Shank attributes to your program and generate an IDL
- Create a TypeScript client with Codama using the IDL
- Write a TS client using
@solana/kitfor end-to-end testing
What You Will Need
This guide assumes you have a basic understanding of Solana Programming, Rust, and TypeScript. You can also refer to our existing Solana guides to learn more:
- Solana Fundamentals Reference Guide
- How to Send Transactions with Solana Kit
- How to Create Custom Program Clients in Solana Kit with Codama
Before you begin, make sure you have the following installed:
| Dependency | Version |
|---|---|
| Rust | 1.90.0 |
| solana-cli | 3.0.6 |
| Node | 24.8.0 |
| shank | 0.4 |
| @solana/kit | 3.0.3 |
| tsx | 4.20.6 |
| codama | 1.3.7 |
What is Pinocchio?
Pinocchio is a zero-dependency Rust framework for writing Solana programs that prioritizes performance and precise control. Unlike higher-level frameworks, it avoids unnecessary abstractions and boilerplate to keep programs fast and efficient.
Pinocchio programs instruct the Rust compiler not to include the standard library by default, using the #![no_std] attribute.
This approach makes them compatible with Solana’s lightweight on-chain runtime, where programs execute deterministically without an operating system, filesystem, or threads. To support this environment, Pinocchio relies on Rust’s minimal core library and its own utilities to provide just what’s needed to run safely and efficiently.
Benefits
This “bare-metal” approach keeps Pinocchio lean and efficient, resulting in several practical advantages:
- Smaller binaries: With fewer abstractions, Pinocchio programs deploy faster and cost less since deployment costs scale with binary size.
- Lower compute unit (CU) usage: Zero-copy access to account data eliminates expensive serialization and deserialization, saving compute units. This is critical in programs that operate close to Solana’s compute limits.
- No external dependencies: It’s pure Rust by default, similar to embedded-systems development where you only bring what’s essential.
Trade-offs
However, these benefits come with trade-offs:
- Manual setup: You’re responsible for writing the instruction dispatcher, account validation, and data layout. There’s no autogenerated boilerplate or macro scaffolding.
- More responsibility: You must explicitly check ownership, signer status, and instruction flow.
- Extra tooling: Because Pinocchio doesn’t generate an IDL automatically, you’ll need tools like Shank or Codama to generate one.
Pinocchio vs Anchor
Anchor and Pinocchio both make it easier to build Solana programs, but they’re designed with very different philosophies in mind.
Anchor prioritizes developer productivity and team scalability. It uses attribute macros, autogenerated boilerplate, and built-in IDL generation to simplify setup and enforce best practices. For many teams, this means faster onboarding, cleaner conventions, and less manual validation. The trade-off is heavier abstraction, which can lead to larger binaries and higher compute usage.
Pinocchio, on the other hand, is built for developers who value control and performance over convenience. It takes a minimalist approach: no standard library, no dependency bloat, and direct access to account data through zero-copy patterns. This design produces smaller binaries and tighter compute budgets, but it also means you’ll need to write instructions, validate accounts, and generate your IDL manually.
Despite being lower level, many developers find Pinocchio surprisingly approachable. For example, checking if an account has signed a transaction in Anchor requires a Signer type inside a #[derive(Accounts)] struct, while in Pinocchio, it’s just an if statement.
Pinocchio is designed with performance and fine-grained control as its top priorities, even if that means sacrificing some developer conveniences.
| Factor | Pinocchio | Anchor |
|---|---|---|
| Design goal | Minimal, no_std, bring only essentials | Ergonomics, macros, conventions |
| Performance (Compute Units) | Lean paths; zero-copy access reduces CU | Higher CU from serialization/framework layers |
| Binary size & deploy cost | Smaller binaries; cheaper, faster deploys | Larger binaries due to abstractions |
| Developer experience | Manual instruction wiring and checks | Boilerplate generated; smoother onboarding |
| IDL & tooling | External IDL generation (e.g., Shank/Codama) | Built-in IDL; broad tool support |
| Risk profile | More foot-guns if validation is missed | Guardrails via patterns and macros |
If you’re optimizing for speed to ship, collaboration, or maintainability across teams, Anchor is a great choice. If you’re pushing Solana’s performance limits or want full control over every instruction and account check, Pinocchio gives you the freedom to do exactly that.
Create a Pinocchio Vault Program
Let’s start by creating a simple Vault program using Pinocchio. This program is intentionally scoped to cover the core concepts of building a program with Pinocchio.
Our Vault program will:
- Create a Vault PDA on the first deposit.
- Allow the owner to deposit SOL into the Vault.
- Allow only the owner to withdraw the stored balance.
Most of the logic involves manual validation. The entrypoint parses a one-byte discriminator and routes execution to either Deposit or Withdraw.
Initialize a Project
Create a new Rust library project and install the required dependencies:
cargo new pinocchio-vault --lib --edition 2021
cd pinocchio-vault
cargo add pinocchio pinocchio-system pinocchio-log pinocchio-pubkey shank
Update Cargo Configuration
Next, open your Cargo.toml file and update it to include the configuration for generating deployment artifacts:
[lib]
crate-type = ["lib", "cdylib"]
Create lib.rs
Next, we’ll create the file that handles what happens when clients interact with your program:
touch src/lib.rs
lib.rs serves as your program’s entrypoint — the first code that executes when a transaction is sent. It takes in the program ID, accounts, and instruction data, then reads the first byte as a discriminator to determine which method to call (for example, 0 = Deposit, 1 = Withdraw).
// lib.rs
#![no_std]
use pinocchio::{
account_info::AccountInfo,
entrypoint,
program_error::ProgramError,
pubkey::Pubkey,
ProgramResult,
};
use pinocchio_pubkey::declare_id;
entrypoint!(process_instruction);
pub mod instructions;
pub use instructions::*;
declare_id!("YOUR_PROGRAM_PUBKEY");
fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
match instruction_data.split_first() {
Some((Deposit::DISCRIMINATOR, data)) => Deposit::try_from((data, accounts))?.process(),
Some((Withdraw::DISCRIMINATOR, _)) => Withdraw::try_from(accounts)?.process(),
_ => Err(ProgramError::InvalidInstructionData),
}
}
Generate a Program ID
Pinocchio’s declare_id!("PUBKEY") macro adds your program’s public key into the crate. It expands to a constant Pubkey named ID (accessible as crate::ID) plus helpers so you can compare program_id in the entrypoint, derive PDAs, and keep the onchain address consistent across your code and client.
How to get a Program ID and use it:
Generate a new program keypair.
solana-keygen new -o target/deploy/vault-keypair.json
Print its public key (this is your Program ID).
solana address -k target/deploy/vault-keypair.json
Paste it into lib.rs in the declare_id! macro.
You'll also need this keypair later when deploying your program and in your client/IDL generation.
Create instructions.rs
Next, we’ll create the file responsible for handling our program’s logic:
touch src/instructions.rs
instructions.rs defines what your program can do and how it does it. It declares each instruction’s shape (names, arguments, and expected accounts), contains the validation and business logic for those instructions, and provides the handlers that the entrypoint in lib.rs dispatches to when a transaction targets your program.
First, we’ll add use statements at the top of instructions.rs to import the crates and types our program relies on, and any utilities we’ll reference in the instruction handlers.
// instructions.rs
use core::convert::TryFrom;
use core::mem::size_of;
use pinocchio::{
account_info::AccountInfo,
instruction::{Seed, Signer},
program_error::ProgramError,
pubkey::{find_program_address, Pubkey},
sysvars::{rent::Rent, Sysvar},
ProgramResult,
};
use pinocchio_log::log;
use pinocchio_system::instructions::{CreateAccount, Transfer as SystemTransfer};
use shank::ShankInstruction;
Use Shank to Generate an IDL
IDL (interface description language) files are used by clients programs and other onchain programs to descrive your instruction handlers, their arguments and accounts.
Anchor generates an IDL automatically because it owns the macros that describe your program, but Pinocchio, with it's more minimal focus, does not.
To make an IDL, we'll annotate a small Rust enum using Shank attributes and generate an IDL from your program code that we'll use later when we create our client code.
Let's add the Shank enum and attributes to our instructions.rs now.
// instructions.rs
/// Shank IDL facade enum describing all program instructions and their required accounts.
/// This is used only for IDL generation and does not affect runtime behavior.
#[derive(ShankInstruction)]
pub enum ProgramIx {
/// Deposit lamports into the vault.
#[account(0, signer, writable, name = "owner", desc = "Vault owner and payer")]
#[account(1, writable, name = "vault", desc = "Vault PDA for lamports")]
#[account(2, name = "program", desc = "Program Address")]
#[account(3, name = "system_program", desc = "System Program Address")]
Deposit { amount: u64 },
/// Withdraw all lamports from the vault back to the owner.
#[account(0, signer, writable, name = "owner", desc = "Vault owner and authority")]
#[account(1, writable, name = "vault", desc = "Vault PDA for lamports")]
#[account(2, name = "program", desc = "Program Address")]
Withdraw {},
}
We're going to add a few helper functions to keep our code clean and reusable.
// instructions.rs
/// Parse a u64 from instruction data.
fn parse_amount(data: &[u8]) -> Result<u64, ProgramError> {
if data.len() != core::mem::size_of::<u64>() {
return Err(ProgramError::InvalidInstructionData);
}
let amt = u64::from_le_bytes(data.try_into().unwrap());
if amt == 0 {
return Err(ProgramError::InvalidInstructionData);
}
Ok(amt)
}
/// Derive the vault PDA for an owner and return (pda, bump).
fn derive_vault(owner: &AccountInfo) -> (Pubkey, u8) {
find_program_address(&[b"vault", owner.key().as_ref()], &crate::ID)
}
/// Ensure the vault exists; if not, create it with PDA seeds.
fn ensure_vault_exists(owner: &AccountInfo, vault: &AccountInfo) -> ProgramResult {
if !owner.is_signer() {
return Err(ProgramError::InvalidAccountOwner);
}
// Create when empty and fund rent-exempt.
if vault.lamports() == 0 {
const ACCOUNT_DISCRIMINATOR_SIZE: usize = 8;
let (_pda, bump) = derive_vault(owner);
let signer_seeds = [
Seed::from(b"vault".as_slice()),
Seed::from(owner.key().as_ref()),
Seed::from(core::slice::from_ref(&bump)),
];
let signer = Signer::from(&signer_seeds);
// Make the account rent-exempt.
const VAULT_SIZE: usize = ACCOUNT_DISCRIMINATOR_SIZE + size_of::<u64>();
let needed_lamports = Rent::get()?.minimum_balance(VAULT_SIZE);
CreateAccount {
from: owner,
to: vault,
lamports: needed_lamports,
space: VAULT_SIZE as u64,
owner: &crate::ID,
}
.invoke_signed(&[signer])?;
log!("Vault created");
} else {
// If vault already exists, validate owner matches the program.
if !vault.is_owned_by(&crate::ID) {
return Err(ProgramError::InvalidAccountOwner);
}
log!("Vault already exists");
}
Ok(())
}
In Pinocchio programs you’ll use log! instead of msg!.
The familiar msg! macro from solana-program isn’t available here because Pinocchio strips out those dependencies to stay lightweight and no_std.
Make a Deposit Into the Vault
Deposit moves SOL from the owner’s wallet into the program’s vault PDA.
On first use, it creates and rent-funds the vault PDA. After ensuring the PDA exists and is owned by the program, it transfers the requested amount of SOL from the owner to the vault.
Basic checks include that owner must be a signer, amount must be non-zero, vault must be writable, and rent minimum must be respected for creation.
// instructions.rs
pub struct Deposit<'a> {
pub owner: &'a AccountInfo,
pub vault: &'a AccountInfo,
pub amount: u64,
}
impl<'a> Deposit<'a> {
pub const DISCRIMINATOR: &'a u8 = &0;
pub fn process(self) -> ProgramResult {
let Deposit {
owner,
vault,
amount,
} = self;
ensure_vault_exists(owner, vault)?;
SystemTransfer {
from: owner,
to: vault,
lamports: amount,
}
.invoke()?;
log!("{} Lamports deposited to vault", amount);
Ok(())
}
}
impl<'a> TryFrom<(&'a [u8], &'a [AccountInfo])> for Deposit<'a> {
type Error = ProgramError;
fn try_from(value: (&'a [u8], &'a [AccountInfo])) -> Result<Self, Self::Error> {
let (data, accounts) = value;
if accounts.len() < 2 {
return Err(ProgramError::NotEnoughAccountKeys);
}
let owner = &accounts[0];
let vault = &accounts[1];
let amount = parse_amount(data)?;
Ok(Self {
owner,
vault,
amount,
})
}
}
Withdraw SOL from the Vault
Withdraw moves SOL from the program’s Vault PDA back to the owner.
Basic checks include that the vault is owned by the program, matches the PDA derived from the owner, and the owner is the signer of the withdraw transaction. The withdrawn amount is everything above the rent minimum.
// instructions.rs
pub struct Withdraw<'a> {
pub owner: &'a AccountInfo,
pub vault: &'a AccountInfo,
}
impl<'a> Withdraw<'a> {
pub const DISCRIMINATOR: &'a u8 = &1;
/// Transfer lamports from the vault PDA to the owner, leaving the rent minimum in place.
pub fn process(self) -> ProgramResult {
let Withdraw { owner, vault } = self;
if !owner.is_signer() {
return Err(ProgramError::InvalidAccountOwner);
}
// Validate that the vault is owned by the program
if !vault.is_owned_by(&crate::ID) {
return Err(ProgramError::InvalidAccountOwner);
}
// Validate that the provided vault account is the correct PDA for this owner
let (expected_vault_pda, _bump) = derive_vault(owner);
if vault.key() != &expected_vault_pda {
return Err(ProgramError::InvalidAccountData);
}
// Compute how much can be withdrawn while keeping the account rent-exempt
let data_len = vault.data_len();
let min_balance = Rent::get()?.minimum_balance(data_len);
let current = vault.lamports();
if current <= min_balance {
// Nothing withdrawable; keep behavior strict to avoid rent violations
return Err(ProgramError::InsufficientFunds);
}
let withdraw_amount = current - min_balance;
// Transfer from vault to owner
{
let mut vault_lamports = vault.try_borrow_mut_lamports()?;
*vault_lamports = vault_lamports
.checked_sub(withdraw_amount)
.ok_or(ProgramError::InsufficientFunds)?;
}
{
let mut owner_lamports = owner.try_borrow_mut_lamports()?;
*owner_lamports = owner_lamports
.checked_add(withdraw_amount)
.ok_or(ProgramError::InsufficientFunds)?;
}
log!("{} lamports withdrawn from vault", withdraw_amount);
Ok(())
}
}
impl<'a> TryFrom<&'a [AccountInfo]> for Withdraw<'a> {
type Error = ProgramError;
fn try_from(accounts: &'a [AccountInfo]) -> Result<Self, Self::Error> {
if accounts.len() < 2 {
return Err(ProgramError::NotEnoughAccountKeys);
}
let owner = &accounts[0];
let vault = &accounts[1];
Ok(Self { owner, vault })
}
}
Build and Deploy the Program (locally)
Now it’s time to build and deploy our program!
Before we deploy our program, open a separate terminal and run the solana-test-validator command to start your local Solana validator.
We'll also specify the vault-keypair.json we created earlier.
cargo build-sbf
solana program deploy --program-id target/deploy/vault-keypair.json target/deploy/pinocchio_vault.so --url localhost
After your program is successfully deployed you'll see a Program ID and Signature like this (yours will be different):
Program Id: 4e8cmnRSw3p6Q8YarvEPrmfpEsAg19mqfAM2TC4hKxfi
Signature: BxvfsnVUSDcLXvpsVXpemX9VN2pUBnCVRQaqvbupUZ43PVJRTLdBfBqCTen3ZNxN9BBjKtuuUd5kBZsxNhfpFjU
Create a Client
For the client side, we’ll use @solana/kit.
Even though we’ll run it as tests, the code is similar to what you’d use in a front end that interacts with our program.
In our tests, we will:
- Airdrop SOL to a generated keypair
- Derive the same PDA as the program
- Construct transactions with the Codama-generated instruction library
- Send transactions to a local validator
From the root of our project, create a client folder to house our test code.
mkdir client
cd client
touch tests.ts
Before we can start writing our client code, we need to create our program's IDL using Shank, and generate a client library using Codama that we can use in our client code.
Generate IDL Using Shank
You run the Shank CLI against your program crate, point it to an output path, and check that both instructions and their accounts match what the runtime expects.
If you change your instruction layout later, you'll need to regenerate the IDL so your clients stay in sync.
In the root of our project, run the following commands:
cargo install shank-cli
shank idl -o idl
The -o argument is for the output path of where your IDL file wil be generated. In our case, we're just adding it to an IDL folder in the root of our project.
The idl folder in our project now contains pinocchio_vault.json which is our program's IDL. It's a good idea to manually review that the IDL was created as you expected.
Generate Client Library with Codama
Codama takes the Shank IDL and emits a TypeScript client.
The generated code includes instruction builders, account types, and small conveniences that keep your client code focused on composing transactions.
Install and initialize Codama in the root of our project, accept the defaults, and point to our idl/pinocchio_vault.json file:
npm init
npm install codama
npx codama init
npx codama run js
You'll see a clients/js/src/generated/ folder in our project with the program types our client code uses to send transactions to our program.
Create a Test Script
First, we'll add all the packages our client code will use:
cd client
npm i @solana-program/system @solana/kit tsx typescript @types/node ws
Add a test script to our package.json, which should look like this:
{
"name": "client",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"build": "tsc",
"test": "npx tsx ./tests.ts"
},
"dependencies": {
"@solana-program/system": "^0.1.0",
"@solana/kit": "^3.0.3"
},
"devDependencies": {
"@types/node": "^24.7.2",
"typescript": "^5.9.3",
"tsx": "^4.19.2"
}
}
Now we can add the following code to our test script to Deposit and Withdraw from the program:
// client/tests.ts
import { describe, it, before } from "node:test";
import assert from "node:assert";
import {
airdropFactory,
createSolanaRpc,
createSolanaRpcSubscriptions,
generateKeyPairSigner,
lamports,
sendAndConfirmTransactionFactory,
pipe,
createTransactionMessage,
setTransactionMessageFeePayer,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstruction,
signTransactionMessageWithSigners,
getSignatureFromTransaction,
getProgramDerivedAddress,
getAddressEncoder,
getUtf8Encoder,
} from "@solana/kit";
import { SYSTEM_PROGRAM_ADDRESS } from "@solana-program/system";
import * as vault from "../clients/js/src/generated/index";
const LAMPORTS_PER_SOL = BigInt(1_000_000_000);
describe('Vault Program', () => {
let rpc: any;
let rpcSubscriptions: any;
let signer: any;
let vaultRent: BigInt;
let vaultPDA: any;
const ACCOUNT_DISCRIMINATOR_SIZE = 8; // same as Anchor/Rust
const U64_SIZE = 8; // u64 is 8 bytes
const VAULT_SIZE = ACCOUNT_DISCRIMINATOR_SIZE + U64_SIZE; // 16
const DEPOSIT_AMOUNT = BigInt(100000000);
before(async () => {
// Establish connection to Solana cluster
const httpProvider = 'http://127.0.0.1:8899';
const wssProvider = 'ws://127.0.0.1:8900';
rpc = createSolanaRpc(httpProvider);
rpcSubscriptions = createSolanaRpcSubscriptions(wssProvider);
// Generate signers
signer = await generateKeyPairSigner();
const signerAddress = await signer.address;
// Airdrop SOL to signer
const airdrop = airdropFactory({ rpc, rpcSubscriptions });
await airdrop({
commitment: 'confirmed',
lamports: lamports(LAMPORTS_PER_SOL),
recipientAddress: signerAddress,
});
console.log(`Airdropped SOL to Signer: ${signerAddress}`);
// get vault rent
vaultRent = await rpc
.getMinimumBalanceForRentExemption(VAULT_SIZE)
.send();
// Get vault PDA
const seedSigner = getAddressEncoder().encode(await signer.address);
const seedTag = getUtf8Encoder().encode("vault");
vaultPDA = await getProgramDerivedAddress({
programAddress: vault.PINOCCHIO_VAULT_PROGRAM_ADDRESS,
seeds: [seedTag, seedSigner],
});
console.log(`Vault PDA: ${vaultPDA[0]}`);
});
it("can deposit to vault", async () => {
// Create Deposit transaction using generated client
const depositIx = vault.getDepositInstruction(
{
owner: signer,
vault: vaultPDA[0],
program: vault.PINOCCHIO_VAULT_PROGRAM_ADDRESS,
systemProgram: SYSTEM_PROGRAM_ADDRESS,
amount: lamports(DEPOSIT_AMOUNT),
},
{
programAddress: vault.PINOCCHIO_VAULT_PROGRAM_ADDRESS,
}
);
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
const tx = await pipe(
createTransactionMessage({ version: 0 }),
tx => setTransactionMessageFeePayer(signer.address, tx),
tx => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
tx => appendTransactionMessageInstruction(depositIx, tx)
);
// Sign and send transaction
const signedTransaction = await signTransactionMessageWithSigners(tx);
const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions });
await sendAndConfirmTransaction(signedTransaction, {
commitment: 'confirmed',
});
const signature = getSignatureFromTransaction(signedTransaction);
console.log('Transaction signature:', signature);
const { value } = await rpc.getBalance(vaultPDA[0].toString()).send();
assert.equal(DEPOSIT_AMOUNT, Number(value) - Number(vaultRent));
});
it("can withdraw from vault", async () => {
const withdrawIx = vault.getWithdrawInstruction(
{
owner: signer,
vault: vaultPDA[0],
program: vault.PINOCCHIO_VAULT_PROGRAM_ADDRESS,
},
);
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
const tx = await pipe(
createTransactionMessage({ version: 0 }),
tx => setTransactionMessageFeePayer(signer.address, tx),
tx => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
tx => appendTransactionMessageInstruction(withdrawIx, tx)
);
const signedTransaction = await signTransactionMessageWithSigners(tx);
const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions });
await sendAndConfirmTransaction(signedTransaction, {
commitment: 'confirmed',
});
const signature = getSignatureFromTransaction(signedTransaction);
console.log('Transaction signature:', signature);
const { value } = await rpc.getBalance(vaultPDA[0].toString()).send();
assert.equal(Number(vaultRent), value);
});
it("doesn't allow other users to withdraw from the vault", async () => {
// signer that DOES NOT own the vault
const otherSigner = await generateKeyPairSigner();
const withdrawIx = vault.getWithdrawInstruction(
{
owner: otherSigner,
vault: vaultPDA[0],
program: vault.PINOCCHIO_VAULT_PROGRAM_ADDRESS,
},
);
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
const tx = await pipe(
createTransactionMessage({ version: 0 }),
tx => setTransactionMessageFeePayer(otherSigner.address, tx),
tx => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
tx => appendTransactionMessageInstruction(withdrawIx, tx)
);
const signedTransaction = await signTransactionMessageWithSigners(tx);
const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions });
await assert.rejects(
sendAndConfirmTransaction(signedTransaction, {
commitment: 'confirmed'
}),
{
message: "Transaction simulation failed",
}
);
});
});
Run Tests
To make sure everything is working, we run the tests:
npm test
Expected Output
(values will be different than what is shown here)
Airdropped SOL to Signer: CU5...b7b
Vault PDA: Gct...N8o
Transaction signature: 3H2...g7X
✔ can deposit to vault (375ms)
Vault PDA: Gct...N8o
Transaction signature: 39V...5fo
✔ can withdraw from vault (460ms)
✔ doesn't allow other users to withdraw from the vault (2.310792ms)
Wrapping Up
If all your tests pass, congratulations! If not, keep debugging. If you’re still having issues with your code, feel free to reach out to us on Discord - we’re happy to help.
Understanding how to read and write data accounts through your programs is an important concept that you will encounter frequently on your path as a Solana Developer.
Using this guide you:
- Built a Pinocchio program that allows users to deposit SOL and withdraw it later, provided they were the original account that deposited the SOL.
- Learned how Pinocchio differs from solana-program and Anchor and developed a program with manual validation and zero-copy data access.
- Generated IDL from your Rust annotations using Shank.
- Turned that IDL into a typed client library with Codama.
- Used Solana Kit to build a client from a set of tests that behave like a front end would.
- Finished with a working mental model of using Pinocchio and manual account validation in a
no_stdenvironment.
Resources
Explore the official GitHub repositories for each of the tools mentioned in this guide:
We ❤️ Feedback!
Let us know if you have any feedback or requests for new topics. We'd love to hear from you.