仅0.6秒编译!用Pinocchio打造极致轻量化SolanaVault合约全记录在Solana开发世界中,性能和效率是永远的关键词。你是否厌倦了臃肿的框架依赖?想尝试更纯粹、更快速的原生Rust开发吗?本文将带你走进Pinocchio的世界——一个无外部依赖、极致零拷贝
在 Solana 开发世界中,性能和效率是永远的关键词。你是否厌倦了臃肿的框架依赖?想尝试更纯粹、更快速的原生 Rust 开发吗?本文将带你走进 Pinocchio 的世界——一个无外部依赖、极致零拷贝的库。我们将从创建一个简单的 Vault 存款与取款程序开始,利用 Shank 和 Codama 构建一套自动化的客户端 SDK,并最终完成从 Rust 到 TypeScript 的全链路自动化测试。
Pinocchio 是一个无外部依赖的库,用于在 Rust 中创建 Solana 程序。唯一的依赖是Solana SDK中专门为链上程序设计的类型。这缓解了依赖问题,并提供了一个高效的零拷贝库来编写程序,同时在计算单元消耗和二进制大小方面都得到了优化。
cargo new blueshift_vault --lib --edition 2021
Creating library `blueshift_vault` package
note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.htm
# 切换项目目录
cd blueshift_vault
cargo add pinocchio pinocchio-system
blueshift_vault on master [?] is 📦 0.1.0 via 🦀 1.92.0
➜ tree . -L 6 -I "docs|target"
.
├── Cargo.lock
├── Cargo.toml
├── _typos.toml
├── cliff.toml
├── deny.toml
├── deploy_out
│ └── blueshift_vault.so
└── src
├── instructions
│ ├── deposit.rs
│ ├── mod.rs
│ └── withdraw.rs
└── lib.rs
4 directories, 10 files
lib.rs 文件#![no_std]
use pinocchio::{
address::address, entrypoint, error::ProgramError, nostd_panic_handler, AccountView, Address,
ProgramResult,
};
use solana_program_log::log;
nostd_panic_handler!();
entrypoint!(process_instruction);
pub mod instructions;
pub use instructions::*;
// 22222222222222222222222222222222222222222222
pub const ID: Address = address!("22222222222222222222222222222222222222222222");
fn process_instruction(
_program_id: &Address,
accounts: &[AccountView],
instruction_data: &[u8],
) -> ProgramResult {
log("Hello from my pinocchio program!");
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),
}
}
这段代码是一个基于 Pinocchio 库实现的轻量级 Solana 智能合约入口,它采用了 #![no_std] 模式来禁用 Rust 标准库,从而追求极小的二进制体积和极高的执行性能。代码通过 entrypoint! 宏定义了合约与 Solana 运行时的交互接口,并在主函数 process_instruction 中利用“指令判别码(Discriminator)”机制,根据传入数据的首字节将交易请求精确路由至 Deposit(存款)或 Withdraw(取款)业务逻辑,是该合约的核心调度中枢。
instructions/mod.rs 文件pub mod deposit;
pub mod withdraw;
pub use deposit::*;
pub use withdraw::*;
instructions/deposit.rs 文件use pinocchio::{error::ProgramError, AccountView, Address, ProgramResult};
use pinocchio_system::instructions::Transfer;
pub struct DepositAccounts<'a> {
pub owner: &'a AccountView,
pub vault: &'a AccountView,
}
impl<'a> TryFrom<&'a [AccountView]> for DepositAccounts<'a> {
type Error = ProgramError;
fn try_from(accounts: &'a [AccountView]) -> Result<Self, Self::Error> {
let [owner, vault, _] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};
// Accounts CHecks
if !owner.is_signer() {
return Err(ProgramError::InvalidAccountOwner);
}
if !vault.owned_by(&pinocchio_system::ID) {
return Err(ProgramError::InvalidAccountOwner);
}
if vault.lamports().ne(&0) {
return Err(ProgramError::InvalidAccountData);
}
let (vault_key, _) =
Address::find_program_address(&[b"vault", owner.address().as_ref()], &crate::ID);
if vault.address().ne(&vault_key) {
return Err(ProgramError::InvalidAccountOwner);
}
// Return the accounts
Ok(Self { owner, vault })
}
}
pub struct DepositInstructionData {
pub amount: u64,
}
impl<'a> TryFrom<&'a [u8]> for DepositInstructionData {
type Error = ProgramError;
fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
if data.len() != size_of::<u64>() {
return Err(ProgramError::InvalidInstructionData);
}
let amount = u64::from_le_bytes(data.try_into().unwrap());
// Instruction CHecks
if amount.eq(&0) {
return Err(ProgramError::InvalidInstructionData);
}
Ok(Self { amount })
}
}
pub struct Deposit<'a> {
pub accounts: DepositAccounts<'a>,
pub instruction_data: DepositInstructionData,
}
impl<'a> TryFrom<(&'a [u8], &'a [AccountView])> for Deposit<'a> {
type Error = ProgramError;
fn try_from((data, accounts): (&'a [u8], &'a [AccountView])) -> Result<Self, Self::Error> {
let accounts = DepositAccounts::try_from(accounts)?;
let instruction_data = DepositInstructionData::try_from(data)?;
Ok(Self {
accounts,
instruction_data,
})
}
}
impl<'a> Deposit<'a> {
pub const DISCRIMINATOR: &'a u8 = &0;
pub fn process(&self) -> ProgramResult {
Transfer {
from: self.accounts.owner,
to: self.accounts.vault,
lamports: self.instruction_data.amount,
}
.invoke()?;
Ok(())
}
}
这段代码基于 Pinocchio 框架定义了 Solana 合约的存款(Deposit)逻辑,通过结构化组件实现了严谨的账户校验、数据解析与业务执行。在账户层面,它严格校验了所有者(owner)的签名权限,并利用 find_program_address 验证金库(vault)账户是否为根据所有者地址派生的合法程序派生地址(PDA),同时确保其属于系统程序且初始状态为空;在数据层面,它将传入的字节流解析为 u64 类型的存款金额并进行非零校验;最终,通过 process 函数发起跨程序调用(CPI),驱动系统程序完成从所有者到金库账户的 SOL 转移操作。
这段代码采用了分层验证、递归组合的架构模式:它通过为专门负责账户校验的 DepositAccounts 和负责参数解析的 DepositInstructionData 分别实现 TryFrom trait,将复杂的安全检查拆解为独立的原子操作,最终在顶层 Deposit 结构体中通过“套娃”式的组合完成整体验证,从而实现从原始字节流到安全、强类型指令对象的全自动化、类型安全的转换流程。
这种设计的妙处在于:
TryFrom 都是一道防火墙,任何一环验证失败都会立即通过 ProgramError 熔断交易。DepositAccounts 结构体及其验证逻辑。instructions/withdraw.rs 文件use pinocchio::{
cpi::{Seed, Signer},
error::ProgramError,
AccountView, Address, ProgramResult,
};
use pinocchio_system::instructions::Transfer;
pub struct WithdrawAccounts<'a> {
pub owner: &'a AccountView,
pub vault: &'a AccountView,
pub bumps: [u8; 1],
}
impl<'a> TryFrom<&'a [AccountView]> for WithdrawAccounts<'a> {
type Error = ProgramError;
fn try_from(accounts: &'a [AccountView]) -> Result<Self, Self::Error> {
let [owner, vault, _] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};
// Basic Accounts Checks
if !owner.is_signer() {
return Err(ProgramError::InvalidAccountOwner);
}
if !vault.owned_by(&pinocchio_system::ID) {
return Err(ProgramError::InvalidAccountOwner);
}
if vault.lamports().eq(&0) {
return Err(ProgramError::InvalidAccountData);
}
let (vault_key, bump) =
Address::find_program_address(&[b"vault", owner.address().as_ref()], &crate::ID);
if vault.address() != &vault_key {
return Err(ProgramError::InvalidAccountOwner);
}
Ok(Self {
owner,
vault,
bumps: [bump],
})
}
}
pub struct Withdraw<'a> {
pub accounts: WithdrawAccounts<'a>,
}
impl<'a> TryFrom<&'a [AccountView]> for Withdraw<'a> {
type Error = ProgramError;
fn try_from(accounts: &'a [AccountView]) -> Result<Self, Self::Error> {
let accounts = WithdrawAccounts::try_from(accounts)?;
Ok(Self { accounts })
}
}
impl<'a> Withdraw<'a> {
pub const DISCRIMINATOR: &'a u8 = &1;
pub fn process(&mut self) -> ProgramResult {
// Create PDA signer seeds
let seeds = [
Seed::from(b"vault"),
Seed::from(self.accounts.owner.address().as_ref()),
Seed::from(&self.accounts.bumps),
];
let signers = [Signer::from(&seeds)];
// Transfer all lamports from vault to owner
Transfer {
from: self.accounts.vault,
to: self.accounts.owner,
lamports: self.accounts.vault.lamports(),
}
.invoke_signed(&signers)?;
Ok(())
}
}
这段代码实现了 Solana 合约的提现(Withdraw)逻辑,它延续了“嵌套校验”模式,通过 WithdrawAccounts 结构体严格验证提现者(owner)的签名权限、金库(vault)的 PDA 合法性以及账户余额,并在校验过程中捕获并存储了用于后续签名的 Bump 值。其核心业务逻辑位于 process 函数中:它利用所有者地址和存储的 Bump 值重新构造 PDA 种子,并通过 invoke_signed 发起带有 PDA 签名 的跨程序调用(CPI),将金库账户中的所有 SOL 余额全额划转回所有者账户,从而实现了一个安全且完全由程序逻辑控制的资金提取流程。
try_from 中,不仅检查了 owner 是否签名,还通过 find_program_address 重新计算 PDA,确保传入的 vault 账户正是由当前 owner 派生的那个唯一金库地址。Deposit 需要传入金额,Withdraw 直接通过 self.accounts.vault.lamports() 获取金库当前所有余额并进行转账。Transfer 指令时,使用 invoke_signed 并传入正确的种子(Seeds)和 Bump,由 Solana 运行时(Runtime)代为验证签名。至此,你已经完成了这个 Vault 合约最核心的两个功能:存入和完整取出。
blueshift_vault on master [?] is 📦 0.1.0 via 🦀 1.92.0
➜ cargo build-sbf
Compiling blueshift_vault v0.1.0 (/Users/qiaopengjun/Code/Solana/blueshift_vault)
Finished `release` profile [optimized] target(s) in 0.68s
这段运行结果表明你已成功使用 cargo build-sbf 工具,在极短的时间内(0.68秒)将 blueshift_vault 合约编译成了优化后的、可直接部署至 Solana 链上的 SBF 二进制文件。
cargo install shank-cli
查看版本信息确认安装成功
shank --version
shank-cli 0.4.6
基于枚举的指令分发,就是给合约里的每个功能编个号,然后根据用户发来的编号,自动把任务派发给正确的处理函数。
简单来说,这一步就是给合约写一份“说明书”。通过定义一个枚举类,我们告诉工具(Shank)这个合约有哪些功能、每个功能需要哪些账户以及什么参数。
// 只有在开启 idl-build 时才引入和编译这段
#[cfg(feature = "idl-build")]
use {
borsh::{BorshDeserialize, BorshSerialize},
shank::ShankInstruction,
};
#[cfg(feature = "idl-build")]
#[derive(Debug, Clone, ShankInstruction, BorshSerialize, BorshDeserialize)]
#[rustfmt::skip]
pub enum VaultInstruction {
/// 指令 0: 向 Vault 存入 SOL
/// 账户顺序必须对应 DepositAccounts 的 try_from 逻辑
#[account(0, signer, writable, name = "owner", desc = "存款人和支付者")]
#[account(1, writable, name = "vault", desc = "派生的 Vault PDA 账户")]
#[account(2, name = "system_program", desc = "System Program")]
Deposit(DepositArgs), // Deposit { amount: u64 }, // 直接写成 struct 风格更直观
/// 指令 1: 从 Vault 提取所有 SOL
/// 账户顺序必须对应 WithdrawAccounts 的 try_from 逻辑
#[account(0, signer, writable, name = "owner", desc = "提款人/所有者")]
#[account(1, writable, name = "vault", desc = "派生的 Vault PDA 账户")]
#[account(2, name = "system_program", desc = "System Program")]
Withdraw,
}
#[cfg(feature = "idl-build")]
/// 定义 Deposit 指令接收的参数
#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
pub struct DepositArgs {
pub amount: u64,
}
注意:另一种方式是在合约中添加宏标记即基于结构体的指令定义 (Struct-based Instructions)。
blueshift_vault on master [?] is 📦 0.1.0 via 🦀 1.92.0
➜ shank idl -o idl -r .
shank DEBUG crate_root is relative, resolving from current dir
shank DEBUG out_dir is relative, resolving from current dir
shank INFO Writing IDL to /Users/qiaopengjun/Code/Solana/blueshift_vault/idl/blueshift_vault.json
注意:运行的时候需要打开default = ["idl-build"]注释 ,执行完毕后要继续注释或者删除!
➜ mkdir clients
➜ cd clients
➜ pnpm init
➜ tsc --init
blueshift_vault/clients on main [!] is 📦 1.0.0 via 🍞 v1.2.17 via 🦀 1.92.0 took 11.6s
➜ tree . -L 6 -I "docs|target|node_modules"
.
├── bun.lock
├── codama.json
├── codegen.ts
├── package.json
├── pnpm-lock.yaml
├── src
│ └── generated
│ ├── js
│ │ ├── index.ts
│ │ ├── instructions
│ │ │ ├── deposit.ts
│ │ │ ├── index.ts
│ │ │ └── withdraw.ts
│ │ ├── programs
│ │ │ ├── blueshiftVault.ts
│ │ │ └── index.ts
│ │ └── shared
│ │ └── index.ts
│ └── rust
│ ├── Cargo.lock
│ ├── Cargo.toml
│ ├── README.md
│ └── src
│ ├── generated
│ │ ├── errors
│ │ ├── instructions
│ │ ├── mod.rs
│ │ └── programs.rs
│ ├── lib.rs
│ └── main.rs
├── test_vault.ts
└── tsconfig.json
12 directories, 21 files
codegen.ts 文件import { createFromRoot } from 'codama'
import { rootNodeFromAnchor } from "@codama/nodes-from-anchor"
import { renderVisitor as renderJavaScriptVisitor } from "@codama/renderers-js"
import { renderVisitor as renderRustVisitor } from "@codama/renderers-rust"
import * as fs from "fs"
import * as path from "path"
import { fileURLToPath } from 'url'
// 兼容性处理
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
async function main() {
const projectRoot = path.resolve(__dirname, "..")
const idlPath = path.join(projectRoot, "idl", "blueshift_vault.json")
// 统一输出路径
const outputBaseDir = path.join(__dirname, "src", "generated")
const outputTsPath = path.join(outputBaseDir, "js")
const outputRsPath = path.join(outputBaseDir, "rust")
console.log(`🚀 正在从 Shank IDL 生成 SDK...`)
try {
// 1. 读取 Shank 生成的 IDL
if (!fs.existsSync(idlPath)) {
throw new Error(`找不到 IDL 文件: ${idlPath}。请先运行 shank idl。`)
}
const idl = JSON.parse(fs.readFileSync(idlPath, "utf-8"))
// 2. 转换 IDL
console.log(`🚀 正在解析 IDL...`)
const codama = createFromRoot(rootNodeFromAnchor(idl))
// 确保目录存在
if (!fs.existsSync(outputBaseDir)) {
fs.mkdirSync(outputBaseDir, { recursive: true })
}
// 3. 生成 TypeScript 客户端
console.log(`📦 生成 TypeScript 客户端...`)
codama.accept(
renderJavaScriptVisitor(outputTsPath, {
formatCode: true,
deleteFolderBeforeRendering: true,
})
)
console.log(`✅ TypeScript SDK 已生成: ${outputTsPath}`)
// 4. 生成 Rust 客户端
console.log(`🦀 生成 Rust 客户端...`)
codama.accept(renderRustVisitor(outputRsPath, {
formatCode: true,
anchorTraits: false,
deleteFolderBeforeRendering: true,
}))
console.log(`\n✨ 全部生成成功!位置: ${outputBaseDir}`)
} catch (error) {
console.error(`❌ 生成失败:`, error)
process.exit(1)
}
}
main()
codegen.ts脚本生成客户端代码你通过调用 Codama 提供的 JS 库函数(如 createFromRoot, renderVisitor 等),手动控制 IDL 的读取、转换和写入过程。
blueshift_vault/clients on master [?] is 📦 1.0.0 via 🦀 1.92.0
➜ bun run codegen.ts
🚀 正在从 Shank IDL 生成 SDK...
🚀 正在解析 IDL...
📦 生成 TypeScript 客户端...
✅ TypeScript SDK 已生成: /Users/qiaopengjun/Code/Solana/blueshift_vault/clients/src/generated/js
🦀 生成 Rust 客户端...
No crate folder specified, skipping formatting.
✨ 全部生成成功!位置: /Users/qiaopengjun/Code/Solana/blueshift_vault/clients/src/generated
codama init 生成 codama.json 的方式你不再编写“如何做”的代码,而是编写一个“要做什么”的 配置文件 (Configuration File)。Codama CLI 会根据这个 JSON 文件自动运行内部的指令。
blueshift_vault/clients on master [?] is 📦 1.0.0 via 🦀 1.92.0
➜ bunx codama init
Welcome to Codama!
✔ Where is yo... 如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!