极致轻量:Solana Pinocchio v0.10 演进实战,0.15s 极速开启托管合约新篇章
极致轻量:Solana Pinocchio v0.10 演进实战,0.15s 极速开启托管合约新篇章
在 Solana 极简开发领域,Pinocchio 框架一直以其“零抽象”带来的性能统治力著称。然而,随着 v0.10.1 版本的发布,这场关于极致性能的实验迎来了一个关键分水岭。
本文将带你深度复盘一个 Escrow(托管合约) 的进化之旅。我们将从 v0.9.2 的底层指针操作出发,一路跨越到 v0.10.1 全面拥抱 AccountView 与 Address 新架构的现代实战。你将看到,Pinocchio 如何在不损失任何性能的前提下,通过对 Helper 库的重构和指令逻辑的封装,将安全性和开发体验提升到一个新高度。最后,那惊人的 0.15s 编译速度,将告诉你为什么 Pinocchio 是 Solana 开发者追求物理极限的终极选择。
本文聚焦 Solana 轻量级框架 Pinocchio v0.10.1 的重大进化。通过 Escrow 托管合约实战,深度解析 AccountView 架构、Address 类型迁移及安全回收账户等核心特性。带你见证 Pinocchio 如何在保持 0.15s 极速编译的同时,通过重构指令逻辑实现性能与安全的双重突破。
一、 核心架构设计
在深入代码前,我们先明确 Escrow 程序的逻辑流:
- Make: 创建者初始化托管账户并存入 Token A。
- Take: 接收者存入 Token B,换取 Token A,随后关闭账户。
- Refund: 创建者在交易未达成前撤回 Token A。
二、 Pinocchio v0.9.2:实操旧版
1. 环境准备与项目结构
cargo new --lib solana_pinocchio_escrow
cd solana_pinocchio_escrow
查看项目结构
pinocchio-escrow-workspace/programs/solana_pinocchio_escrow on main is 📦 0.1.0 via 🦀 1.92.0
➜ tree . -L 6 -I ".gitignore|.github|.git|target"
.
├── Cargo.toml
├── Makefile
└── src
├── errors.rs
├── instructions
│ ├── helpers.rs
│ ├── make.rs
│ ├── mod.rs
│ ├── refund.rs
│ └── take.rs
├── lib.rs
└── state.rs
3 directories, 10 files
2. 核心代码实现
Cargo.toml 文件
Cargo.toml: 依赖管理。
cargo-features = ["edition2024"]
[package]
name = "solana_pinocchio_escrow"
version = "0.1.0"
edition = "2024"
license = "MIT"
[lib]
crate-type = ["lib", "cdylib"]
name = "solana_pinocchio_escrow"
[dependencies]
pinocchio = "0.9.2"
pinocchio-associated-token-account = "0.2.0"
pinocchio-system = "0.4.0"
pinocchio-token = "0.4.0"
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = [
'cfg(target_os, values("solana"))',
] }
这段 Cargo.toml 配置展示了基于 Pinocchio v0.9.2 的轻量化 Solana 项目基础,它率先采用了 Rust 2024 Edition(需开启 cargo-features)以利用最新的语言特性,并通过 cdylib 输出格式确保程序能正确编译为链上 BPF 字节码;同时,配置中集成了一系列专门针对底层开发优化的 Pinocchio 扩展库,并特别通过 [lints.rust] 解决了跨平台编译时针对 solana 目标系统的配置警告,为追求极致性能和现代语法的开发者提供了一个整洁、高效的工程起点。
💡 小贴士
- Rust 2024: 指出这是目前 Rust 最前沿的编译器版本,意味着更好的语法特性。
- crate-type: 说明这是 Solana 合约必须的配置(为了生成
.so文件)。 - lints: 这是“避雷针”配置,防止编译器一直报
unexpected_cfgs这种讨厌的警告。
src/lib.rs 文件
入口与指令分发
use pinocchio::{
ProgramResult, account_info::AccountInfo, entrypoint, program_error::ProgramError,
pubkey::Pubkey,
};
entrypoint!(process_instruction);
pub mod instructions;
pub use instructions::*;
pub mod state;
pub use state::*;
// 22222222222222222222222222222222222222222222
pub const ID: Pubkey = [
0x0f, 0x1e, 0x6b, 0x14, 0x21, 0xc0, 0x4a, 0x07, 0x04, 0x31, 0x26, 0x5c, 0x19, 0xc5, 0xbb, 0xee,
0x19, 0x92, 0xba, 0xe8, 0xaf, 0xd1, 0xcd, 0x07, 0x8e, 0xf8, 0xaf, 0x70, 0x47, 0xdc, 0x11, 0xf7,
];
fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
match instruction_data.split_first() {
Some((Make::DISCRIMINATOR, data)) => Make::try_from((data, accounts))?.process(),
Some((Take::DISCRIMINATOR, _)) => Take::try_from(accounts)?.process(),
Some((Refund::DISCRIMINATOR, _)) => Refund::try_from(accounts)?.process(),
_ => Err(ProgramError::InvalidInstructionData),
}
}
这段代码构成了 Pinocchio v0.9.2 程序的核心入口与指令分发器:它通过 entrypoint! 宏接入 Solana 运行时,手动定义了程序的唯一 ID,并利用 match 模式匹配对输入指令数据的首字节进行鉴别(Discriminator),从而将链上请求精准路由至 Make、Take 或 Refund 模块,完成从原始 AccountInfo 账户切片到具体业务逻辑的转换与执行。
src/state.rs 文件
数据结构定义
use core::mem::size_of;
use pinocchio::{program_error::ProgramError, pubkey::Pubkey};
// --- 定义常量种子 ---
pub const ESCROW_SEED: &[u8] = b"escrow";
#[repr(C)]
pub struct Escrow {
pub seed: u64, // Random seed for PDA derivation
pub maker: Pubkey, // Creator of the escrow
pub mint_a: Pubkey, // Token being deposited
pub mint_b: Pubkey, // Token being requested
pub receive: u64, // Amount of token B wanted
pub bump: [u8; 1], // PDA bump seed
}
impl Escrow {
pub const LEN: usize = size_of::<u64>()
+ size_of::<Pubkey>()
+ size_of::<Pubkey>()
+ size_of::<Pubkey>()
+ size_of::<u64>()
+ size_of::<[u8; 1]>();
#[inline(always)]
pub fn load_mut(bytes: &mut [u8]) -> Result<&mut Self, ProgramError> {
if bytes.len() != Escrow::LEN {
return Err(ProgramError::InvalidAccountData);
}
Ok(unsafe { &mut *core::mem::transmute::<*mut u8, *mut Self>(bytes.as_mut_ptr()) })
}
#[inline(always)]
pub fn load(bytes: &[u8]) -> Result<&Self, ProgramError> {
if bytes.len() != Escrow::LEN {
return Err(ProgramError::InvalidAccountData);
}
Ok(unsafe { &*core::mem::transmute::<*const u8, *const Self>(bytes.as_ptr()) })
}
#[inline(always)]
pub fn set_seed(&mut self, seed: u64) {
self.seed = seed;
}
#[inline(always)]
pub fn set_maker(&mut self, maker: Pubkey) {
self.maker = maker;
}
#[inline(always)]
pub fn set_mint_a(&mut self, mint_a: Pubkey) {
self.mint_a = mint_a;
}
#[inline(always)]
pub fn set_mint_b(&mut self, mint_b: Pubkey) {
self.mint_b = mint_b;
}
#[inline(always)]
pub fn set_receive(&mut self, receive: u64) {
self.receive = receive;
}
#[inline(always)]
pub fn set_bump(&mut self, bump: [u8; 1]) {
self.bump = bump;
}
#[inline(always)]
pub fn set_inner(
&mut self,
seed: u64,
maker: Pubkey,
mint_a: Pubkey,
mint_b: Pubkey,
receive: u64,
bump: [u8; 1],
) {
self.seed = seed;
self.maker = maker;
self.mint_a = mint_a;
self.mint_b = mint_b;
self.receive = receive;
self.bump = bump;
}
}
这段代码定义了 Escrow 状态结构体,并利用 #[repr(C)] 确保内存布局的确定性,这是实现 Zero-Copy(零拷贝) 高性能模式的核心。通过在 load 和 load_mut 函数中使用 core::mem::transmute,程序能够将原始账户字节流直接“映射”为结构体引用,彻底省去了传统序列化(如 Borsh)带来的计算开销;配合 size_of 静态计算的精确长度校验,该实现可以在极致节省 Compute Units (CU) 的前提下,安全且高效地管理托管合约的交易条款。
💡 技术点拨
- 为什么要用
#[repr(C)]? 因为 Rust 默认的结构体布局是不确定的,加了它才能保证字段顺序和字节对齐与你定义的完全一致,从而安全地进行指针强转。 - 关于
unsafe: 在底层框架如 Pinocchio 中,使用unsafe进行零拷贝是提升性能的标准做法,它让程序直接在内存“视窗”上操作数据,而不是在内存中搬运数据。
src/instructions/helpers.rs 文件
使用 unsafe 余额修改
use pinocchio::instruction::{Seed, Signer};
use pinocchio::program_error::ProgramError;
use pinocchio::{ProgramResult, account_info::AccountInfo};
use pinocchio_associated_token_account::instructions::CreateIdempotent;
use pinocchio_system::instructions::CreateAccount;
use pinocchio_token::state::TokenAccount;
// --- 1. 签名者检查助手 ---
pub struct SignerAccount;
impl SignerAccount {
#[inline(always)]
pub fn check(account: &AccountInfo) -> Result<(), ProgramError> {
if !account.is_signer() {
return Err(ProgramError::MissingRequiredSignature);
}
Ok(())
}
}
// --- 2. 程序账户 (PDA/State) 助手 ---
pub struct ProgramAccount;
impl ProgramAccount {
#[inline(always)]
/// 初始化程序状态账户 (如 Escrow)
pub fn init<T>(
payer: &AccountInfo, // 1. 支付租金的人
new_account: &AccountInfo, // 2. 要创建的 PDA
signer_seeds: &[Seed], // 3. PDA 种子
space: usize, // 4. 空间大小
lamports: u64,
) -> ProgramResult {
// 计算租金 (这里假设你已经算好了 lamports,或者调用系统程序计算)
// 最简单的做法是调用 pinocchio_system 的 CreateAccount
CreateAccount {
from: payer,
to: new_account,
lamports,
space: space as u64,
owner: &crate::ID,
}
.invoke_signed(&[Signer::from(signer_seeds)])
}
/// 检查该账户是否由本程序拥有
#[inline(always)]
pub fn check(account: &AccountInfo) -> Result<(), ProgramError> {
if account.owner() != &crate::ID {
return Err(ProgramError::InvalidAccountOwner);
}
Ok(())
}
/// 关闭账户并回收 Lamports (常用于 Refund/Take)
pub fn close(account: &AccountInfo, destination: &AccountInfo) -> ProgramResult {
// 将 lamports 转移给接收者
let lamports = account.lamports();
// 2. 手动转移 Lamports
// 注意:在 Pinocchio 0.9.2 源码中,修改 lamports 需要通过 unsafe 的 unchecked 方法
// 或者使用 try_borrow_mut_lamports (会增加 CU 开销)
// 鉴于 Pinocchio 追求底层效率,这里使用源码提供的 unchecked 方式:
unsafe {
// 将原账户余额清零
*account.borrow_mut_lamports_unchecked() = 0;
// 将余额累加到接收者账户
*destination.borrow_mut_lamports_unchecked() += lamports;
}
// 清理数据并将所有者重置为系统程序
account.close()
}
}
// --- 3. Mint (代币定义) 助手 ---
pub struct MintInterface;
impl MintInterface {
#[inline(always)]
pub fn check(account: &AccountInfo) -> Result<(), ProgramError> {
// SPL Token Mint 固定长度为 82
// 且必须由 Token Program 拥有
if account.data_len() != 82 {
// SPL Token Mint 固定长度
return Err(ProgramError::InvalidAccountData);
}
Ok(())
}
}
// --- 4. 关联代币账户 (ATA) 助手 ---
pub struct AssociatedTokenAccount;
impl AssociatedTokenAccount {
pub fn init(
funding_account: &AccountInfo, // 1. 出钱的人 (Payer/Signer)
account: &AccountInfo, // 2. 要创建的 ATA 地址
wallet: &AccountInfo, // 3. 所有者 (Owner,即这个 ATA 归谁管)
mint: &AccountInfo, // 4. Mint
system_program: &AccountInfo, // 5. 系统程序
token_program: &AccountInfo, // 6. 代币程序
) -> ProgramResult {
CreateIdempotent {
funding_account,
account,
wallet,
mint,
system_program,
token_program,
}
.invoke()
}
pub fn init_if_needed(
account: &AccountInfo,
mint: &AccountInfo,
funding_account: &AccountInfo, // 教程传入的第3个参数
wallet: &AccountInfo,
system_program: &AccountInfo,
token_program: &AccountInfo,
) -> ProgramResult {
CreateIdempotent {
funding_account,
account,
wallet,
mint,
system_program,
token_program,
}
.invoke()
}
pub fn check(
ata: &AccountInfo,
owner: &AccountInfo,
mint: &AccountInfo,
_token_program: &AccountInfo,
) -> Result<(), ProgramError> {
let token_account = TokenAccount::from_account_info(ata)?;
if token_account.owner() != owner.key() || token_account.mint() != mint.key() {
return Err(ProgramError::InvalidAccountData);
}
Ok(())
}
}
这段代码构建了一套高性能的底层助手工具库,通过封装签名者校验、PDA 的生命周期管理(包括初始化与利用 unsafe 指针手动回收 Lamports 的“硬核”关户操作)、以及关联代币账户(ATA)的幂等创建与一致性检查,将复杂的底层账户操作抽象为简洁的接口,从而在确保指令逻辑安全性的同时,最大限度地发挥了 Pinocchio v0.9.2 极致节省计算单元(CU)的特性。
💡 深度观察:为什么close 方法这里用了 unsafe?
在 Pinocchio v0.9.2 中,AccountInfo 并没有像新版本那样提供便捷的安全 Setter。为了追求极致性能,开发者直接操作内存地址(borrow_mut_lamports_unchecked)来手动转移余额。这正是 Pinocchio 开发者常说的“给开发者最高的自由度,也要求开发者最严谨的逻辑”。
创建
make 指令完成以下三项工作:
- 初始化托管记录并存储所有交易条款。
- 创建金库(一个由
mint_a拥有的escrow的关联代币账户 (ATA))。 - 使用 CPI 调用 SPL-Token 程序,将创建者的 Token A 转移到该金库中。
instructions/make.rs 文件
/*
1. 初始化托管记录并存储所有交易条款。
2. 创建金库(一个由 mint_a 拥有的 escrow 的关联代币账户 (ATA))。
3. 使用 CPI 调用 SPL-Token 程序,将创建者的 Token A 转移到该金库中。
*/
use pinocchio::{
ProgramResult, account_info::AccountInfo, instruction::Seed, program_error::ProgramError,
pubkey::find_program_address,
};
use pinocchio_token::instructions::Transfer;
use crate::{
AssociatedTokenAccount, ESCROW_SEED, Escrow, MintInterface, ProgramAccount, SignerAccount,
};
pub struct MakeAccounts<'a> {
pub maker: &'a AccountInfo,
pub escrow: &'a AccountInfo,
pub mint_a: &'a AccountInfo,
pub mint_b: &'a AccountInfo,
pub maker_ata_a: &'a AccountInfo,
pub vault: &'a AccountInfo,
pub system_program: &'a AccountInfo,
pub token_program: &'a AccountInfo,
}
impl<'a> TryFrom<&'a [AccountInfo]> for MakeAccounts<'a> {
type Error = ProgramError;
fn try_from(accounts: &'a [AccountInfo]) -> Result<Self, Self::Error> {
let [
maker,
escrow,
mint_a,
mint_b,
maker_ata_a,
vault,
system_program,
token_program,
_,
] = accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
};
// Basic Accounts Checks
SignerAccount::check(maker)?;
MintInterface::check(mint_a)?;
MintInterface::check(mint_b)?;
AssociatedTokenAccount::check(maker_ata_a, maker, mint_a, token_program)?;
// Return the accounts
Ok(Self {
maker,
escrow,
mint_a,
mint_b,
maker_ata_a,
vault,
system_program,
token_program,
})
}
}
pub struct MakeInstructionData {
pub seed: u64,
pub receive: u64,
pub amount: u64,
}
impl<'a> TryFrom<&'a [u8]> for MakeInstructionData {
type Error = ProgramError;
fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
if data.len() != size_of::<u64>() * 3 {
return Err(ProgramError::InvalidInstructionData);
}
let seed = u64::from_le_bytes(data[0..8].try_into().unwrap());
let receive = u64::from_le_bytes(data[8..16].try_into().unwrap());
let amount = u64::from_le_bytes(data[16..24].try_into().unwrap());
// Instruction Checks
if amount == 0 {
return Err(ProgramError::InvalidInstructionData);
}
Ok(Self {
seed,
receive,
amount,
})
}
}
pub struct Make<'a> {
pub accounts: MakeAccounts<'a>,
pub instruction_data: MakeInstructionData,
pub bump: u8,
}
impl<'a> TryFrom<(&'a [u8], &'a [AccountInfo])> for Make<'a> {
type Error = ProgramError;
fn try_from((data, accounts): (&'a [u8], &'a [AccountInfo])) -> Result<Self, Self::Error> {
let accounts = MakeAccounts::try_from(accounts)?;
let instruction_data = MakeInstructionData::try_from(data)?;
// Initialize the Accounts needed
let (_, bump) = find_program_address(
&[
ESCROW_SEED,
accounts.maker.key().as_ref(),
&instruction_data.seed.to_le_bytes(),
],
&crate::ID,
);
Ok(Self {
accounts,
instruction_data,
bump,
})
}
}
impl<'a> Make<'a> {
pub const DISCRIMINATOR: &'a u8 = &0;
pub fn process(&mut self) -> ProgramResult {
// --- 1. 准备种子 (用于 init PDA) ---
let seed_binding = self.instruction_data.seed.to_le_bytes();
let bump_binding = [self.bump];
let escrow_seeds = [
Seed::from(ESCROW_SEED),
Seed::from(self.accounts.maker.key().as_ref()),
Seed::from(&seed_binding),
Seed::from(&bump_binding),
];
// --- 2. 创建 Escrow PDA ---
ProgramAccount::init::<Escrow>(
self.accounts.maker, // Payer (出钱签名的人)
self.accounts.escrow, // 要创建的 PDA
&escrow_seeds,
Escrow::LEN,
2_000_000,
)?;
// --- 3. 创建 Vault (ATA) ---
// Initialize the vault
AssociatedTokenAccount::init(
self.accounts.maker, // 1. funding_account -> 传 Maker (他是 Signer,付钱)
self.accounts.vault, // 2. account -> 传 Vault (这就是要创建的 ATA)
self.accounts.escrow, // 3. wallet -> 传 Escrow (它是 PDA,作为金库的主人)
self.accounts.mint_a, // 4. mint -> 传 MintA
self.accounts.system_program,
self.accounts.token_program,
)?;
// --- 4. 填充数据 ---
// Populate the escrow account
let mut data = self.accounts.escrow.try_borrow_mut_data()?;
let escrow = Escrow::load_mut(data.as_mut())?;
escrow.set_inner(
self.instruction_data.seed,
*self.accounts.maker.key(),
*self.accounts.mint_a.key(),
*self.accounts.mint_b.key(),
self.instruction_data.receive,
[self.bump],
);
// Transfer tokens to vault
Transfer {
from: self.accounts.maker_ata_a,
to: self.accounts.vault,
authority: self.accounts.maker,
amount: self.instruction_data.amount,
}
.invoke()?;
Ok(())
}
}
这段代码实现了托管合约(Escrow)的 Make 指令 核心逻辑:它首先通过 TryFrom 模式对输入账户进行严格的签名与关联校验,并解析指令数据;随后在执行过程中,程序先后初始化一个 Escrow PDA 用于存储交易条款数据,并创建一个由该 PDA 拥有的 Vault 关联代币账户(ATA),最后通过 CPI(跨程序调用)将创建者指定的 Token A 转移至金库锁定,从而完成了整个交易要约的链上初始化。
💡 关键逻辑拆解
该 process 函数的操作可以归纳为以下四个步骤:
- 确定身份 (PDA Derivation):利用
ESCROW_SEED、创建者地址和随机种子计算出唯一的 PDA 及其bump。 - 空间分配 (Account Init):调用
ProgramAccount::init和AssociatedTokenAccount::init为合约状态和代币存放点分配物理空间。 - 协议存证 (State Storage):利用之前定义的 Zero-Copy
load_mut方法,将交易条款(如期望获得的 Token B 数量)直接写入 PDA 内存。 - 资产锁定 (Asset Locking):执行
Transfer指令,正式将资产的所有权从创建者移交给合约控制的金库。
接受
take 指令完成交换操作:
- 关闭托管记录,将其租金 lamports 返还给创建者。
- 将 Token A 从保管库转移到接受者,然后关闭保管库。
- 将约定数量的 Token B 从接受者转移到创建者。
instructions/take.rs 文件
use std::slice;
use pinocchio::{
ProgramResult,
account_info::AccountInfo,
instruction::{Seed, Signer},
program_error::ProgramError,
pubkey::create_program_address,
};
use pinocchio_token::{
instructions::{CloseAccount, Transfer},
state::TokenAccount,
};
use crate::{
AssociatedTokenAccount, ESCROW_SEED, Escrow, MintInterface, ProgramAccount, SignerAccount,
};
/*
1. 关闭托管记录,将其租金 lamports 返还给创建者。
2. 将 Token A 从保管库转移到接受者,然后关闭保管库。
3. 将约定数量的 Token B 从接受者转移到创建者。
*/
pub struct TakeAccounts<'a> {
pub taker: &'a AccountInfo,
pub maker: &'a AccountInfo,
pub escrow: &'a AccountInfo,
pub mint_a: &'a AccountInfo,
pub mint_b: &'a AccountInfo,
pub vault: &'a AccountInfo,
pub taker_ata_a: &'a AccountInfo,
pub taker_ata_b: &'a AccountInfo,
pub maker_ata_b: &'a AccountInfo,
pub system_program: &'a AccountInfo,
pub token_program: &'a AccountInfo,
}
impl<'a> TryFrom<&'a [AccountInfo]> for TakeAccounts<'a> {
type Error = ProgramError;
fn try_from(accounts: &'a [AccountInfo]) -> Result<Self, Self::Error> {
let [
taker,
maker,
escrow,
mint_a,
mint_b,
vault,
taker_ata_a,
taker_ata_b,
maker_ata_b,
system_program,
token_program,
_,
] = accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
};
// Basic Accounts Checks
SignerAccount::check(taker)?;
ProgramAccount::check(escrow)?;
MintInterface::check(mint_a)?;
MintInterface::check(mint_b)?;
AssociatedTokenAccount::check(taker_ata_b, taker, mint_b, token_program)?;
AssociatedTokenAccount::check(vault, escrow, mint_a, token_program)?;
// Return the accounts
Ok(Self {
taker,
maker,
escrow,
mint_a,
mint_b,
taker_ata_a,
taker_ata_b,
maker_ata_b,
vault,
system_program,
token_program,
})
}
}
pub struct Take<'a> {
pub accounts: TakeAccounts<'a>,
}
impl<'a> TryFrom<&'a [AccountInfo]> for Take<'a> {
type Error = ProgramError;
fn try_from(accounts: &'a [AccountInfo]) -> Result<Self, Self::Error> {
let accounts = TakeAccounts::try_from(accounts)?;
// Initialize necessary accounts
AssociatedTokenAccount::init_if_needed(
accounts.taker_ata_a,
accounts.mint_a,
accounts.taker,
accounts.taker,
accounts.system_program,
accounts.token_program,
)?;
AssociatedTokenAccount::init_if_needed(
accounts.maker_ata_b,
accounts.mint_b,
accounts.taker,
accounts.maker,
accounts.system_program,
accounts.token_program,
)?;
Ok(Self { accounts })
}
}
impl<'a> Take<'a> {
pub const DISCRIMINATOR: &'a u8 = &1;
pub fn process(&mut self) -> ProgramResult {
let data = self.accounts.escrow.try_borrow_data()?;
let escrow = Escrow::load(&data)?;
// Check if the escrow is valid
let escrow_key = create_program_address(
&[
ESCROW_SEED,
self.accounts.maker.key(),
&escrow.seed.to_le_bytes(),
&escrow.bump,
],
&crate::ID,
)?;
if &escrow_key != self.accounts.escrow.key() {
return Err(ProgramError::InvalidAccountOwner);
}
let seed_binding = escrow.seed.to_le_bytes();
let bump_binding = escrow.bump;
let escrow_seeds = [
Seed::from(ESCROW_SEED),
Seed::from(self.accounts.maker.key().as_ref()),
Seed::from(&seed_binding),
Seed::from(&bump_binding),
];
let signer = Signer::from(&escrow_seeds);
let amount = TokenAccount::from_account_info(self.accounts.vault)?.amount();
// Transfer from the Vault to the Taker
Transfer {
from: self.accounts.vault,
to: self.accounts.taker_ata_a,
authority: self.accounts.escrow,
amount,
}
.invoke_signed(slice::from_ref(&signer))?;
// Close the Vault
CloseAccount {
account: self.accounts.vault,
destination: self.accounts.maker,
authority: self.accounts.escrow,
}
.invoke_signed(slice::from_ref(&signer))?;
// Transfer from the Taker to the Maker
Transfer {
from: self.accounts.taker_ata_b,
to: self.accounts.maker_ata_b,
authority: self.accounts.taker,
amount: escrow.receive,
}
.invoke()?;
// Close the Escrow
drop(data);
ProgramAccount::close(self.accounts.escrow, self.accounts.taker)?;
Ok(())
}
}
这段代码实现了托管合约的 Take 指令,即最终的交换执行逻辑:它首先通过 init_if_needed 确保接收者和创建者所需的代币账户已就绪,随后在校验托管账户(Escrow PDA)合法性后,利用 PDA 签名分两步完成资产互换——先将金库中的 Token A 转移给接收者并关闭金库,再将接收者持有的 Token B 转移给创建者;最后,通过手动回收租金的方式关闭托管账户,从而在链上安全地解除了这笔原子交易。
💡 核心操作流程图解
- 自动补全账户:为 Taker 准备收 A 的账户,为 Maker 准备收 B 的账户(如果不存在则创建)。
- PDA 权限校验:重新计算 PDA 地址以确保操作的是正确的托管实例。
- 两阶段转移:
- 由合约转出 (Signed CPI):Vault (Token A) $\rightarrow$ Taker。
- 由用户转入 (Direct CPI):Taker (Token B) $\rightarrow$ Maker。
- 清理现场:销毁 Vault 和 Escrow 账户,释放存储空间并将租金原路退回。
退款
refund 指令允许创建者取消一个未完成的报价:
- 关闭托管 PDA,并将其租金 lamports 返还给创建者。
- 将代币 A 的全部余额从保险库转回创建者,然后关闭保险库账户。
instructions/refund.rs 文件
/*
refund 指令允许创建者取消一个未完成的报价:
关闭托管 PDA,并将其租金 lamports 返还给创建者。
将代币 A 的全部余额从保险库转回创建者,然后关闭保险库账户。
*/
use std::slice;
use pinocchio::{
ProgramResult,
account_info::AccountInfo,
instruction::{Seed, Signer},
program_error::ProgramError,
};
use pinocchio_token::{
instructions::{CloseAccount, Transfer},
state::TokenAccount,
};
use crate::{AssociatedTokenAccount, ESCROW_SEED, Escrow, ProgramAccount};
pub struct Refund<'a> {
pub maker: &'a AccountInfo,
pub escrow: &'a AccountInfo,
pub mint_a: &'a AccountInfo,
pub vault: &'a AccountInfo,
pub maker_ata_a: &'a AccountInfo,
pub associated_token_program: &'a AccountInfo,
pub token_program: &'a AccountInfo,
pub system_program: &'a AccountInfo,
}
impl<'a> Refund<'a> {
pub const DISCRIMINATOR: &'a u8 = &2;
pub fn try_from(accounts: &'a [AccountInfo]) -> Result<Self, ProgramError> {
// 使用简单的切片模式匹配来获取账户,性能最优
let [
maker, // 1. Signer
escrow, // 2. Escrow PDA
mint_a, // 3. Mint A (Anchor 里的第三个账户)
vault, // 4. Vault (Token Account)
maker_ata_a, // 5. Maker ATA
associated_token_program,
token_program, // 6. Token Program
system_program, // 7. System Program
] = accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
};
Ok(Self {
maker,
escrow,
mint_a,
vault,
maker_ata_a,
associated_token_program,
token_program,
system_program,
})
}
pub fn process(&self) -> ProgramResult {
AssociatedTokenAccount::init_if_needed(
self.maker_ata_a,
self.mint_a,
self.maker,
self.maker,
self.system_program,
self.token_program,
)?;
// 1. 获取 Escrow 数据视图 (零拷贝)
let data = self.escrow.try_borrow_data()?;
let escrow_state = Escrow::load(&data)?;
// 2. 构造 PDA 签名
let seed_bytes = escrow_state.seed.to_le_bytes();
let seeds = [
Seed::from(ESCROW_SEED),
Seed::from(self.maker.key().as_ref()),
Seed::from(&seed_bytes),
Seed::from(&escrow_state.bump),
];
let signer = Signer::from(&seeds);
// 检查 amount 之前确保 vault 数据有效
let amount = TokenAccount::from_account_info(self.vault)?.amount();
if amount > 0 {
// 执行转账: Vault (from) -> Maker ATA (to)
// 必须确认识别到的 vault 账户的所有者是 escrow PDA
Transfer {
from: self.vault,
to: self.maker_ata_a,
authority: self.escrow, // 这里必须是 PDA
amount,
}
.invoke_signed(slice::from_ref(&signer))?;
}
// 关闭 Vault 账户
CloseAccount {
account: self.vault,
destination: self.maker,
authority: self.escrow,
}
.invoke_signed(&[signer])?;
drop(data); // 必须在 close escrow 前释放
ProgramAccount::close(self.escrow, self.maker)?;
Ok(())
}
}
这段代码实现了托管合约的 Refund 指令,作为交易的“撤销键”,它通过验证创建者的身份并重建 Escrow PDA 的签名,将原本锁定在金库(Vault)中的代币 A 全额转回给创建者,随后依次关闭金库账户与托管状态账户,并将所有剩余的租金(Lamports)返还至创建者钱包,从而在链上干净、安全地终止该笔托管交易。
💡 技术亮点总结
- 原子化清理:该指令不仅退回了资产,还通过
CloseAccount和ProgramAccount::close清理了链上空间,实现了资产与租金的双重回收。 - 权限闭环:通过
invoke_signed使用 PDA 种子进行签名,确保只有最初创建该托管的账户(Maker)能成功触发退款逻辑。 - 内存安全:在代码最后使用了
drop(data),这是为了在手动关闭账户(修改 Lamports)前显式释放对账户数据的借用,避免触发 Rust 的运行时错误。
三、 跨越分水岭:v0.10.1 带来的变革
为什么升级? v0.10 引入了
AccountView,彻底改变了我们对 Solana 账户数据的访问方式,从“结构体持有”转向“内存视图映射”。
核心变更对比表
| 特性 | v0.9.2 (Old) | v0.10.1 (New) | 优势 |
|---|---|---|---|
| 账户模型 | AccountInfo | AccountView | Zero-Copy,节省大量 CU |
| 地址表示 | Pubkey | Address | 更轻量,语义更明确 |
| Lamports 修改 | borrow_mut_lamports_unchecked() | set_lamports() | 告别 unsafe,提升安全性 |
| 入口声明 | 手动定义 const ID | declare_id!() | 遵循官方标准 |
新旧版本核心差异对照表
| 特性 | Pinocchio v0.9.2 (旧版) | Pinocchio v0.10.1 (新版) |
|---|---|---|
| 地址处理 | 使用 Pubkey | 全面升级为 Address |
| Lamports 修改 | *borrow_mut_lamports_unchecked() = 0 (危险) | account.set_lamports(0) (安全受控) |
| 数据访问 | 较多依赖手动指针/宏 | 基于 AccountView 视图与 load_mut |
| 编译耗时 | 极快 | 0.15s - 0.16s (极致优化) |
四、 Pinocchio v0.10.1:实操新版
创建项目
cargo new --lib pinocchio_escrow
Creating library `pinocchio_escrow` package
note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
切换到项目目录
cd pinocchio_escrow
查看项目目录结构
pinocchio-escrow-workspace/programs/pinocchio_escrow on main is 📦 0.1.0 via 🦀 1.92.0
➜ tree . -L 6 -I ".gitignore|.github|.git|target"
.
├── Cargo.toml
└── src
├── errors.rs
├── instructions
│ ├── helpers.rs
│ ├── make.rs
│ ├── mod.rs
│ ├── refund.rs
│ └── take.rs
├── lib.rs
└── state.rs
3 directories, 9 files
Cargo.toml 文件
Cargo.toml: (依赖 pinocchio 0.10.1)
cargo-features = ["edition2024"]
[package]
name = "pinocchio_escrow"
version = "0.1.0"
edition = "2024"
license = "MIT"
[lib]
crate-type = ["lib", "cdylib"]
name = "pinocchio_escrow"
[dependencies]
pinocchio = "0.10.1"
pinocchio-associated-token-account = "0.3.0"
pinocchio-system = "0.5.0"
pinocchio-token = "0.5.0"
solana-address = { version = "2.1.0", features = ["curve25519"] }
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = [
'cfg(target_os, values("solana"))',
] }
这段 Cargo.toml 配置文件标志着项目向 Pinocchio v0.10.1 版本的正式演进,它通过启用 edition2024 率先拥抱 Rust 的最新语言特性,并引入了 solana-address 库以支持更现代、更安全的地址处理机制;相比旧版,该配置不仅升级了所有核心依赖组件以适配最新的 AccountView 架构,还延续了针对 Solana 目标系统的 Lint 优化,旨在为开发者构建一个既符合未来 Rust 标准又能压榨极致链上性能的现代 Solana 程序开发环境。
💡 版本升级的关键发现
以下 v0.10.1 的配置亮点:
solana-address依赖:这是新版的标志。它取代了部分旧的Pubkey处理逻辑,配合curve25519特性提供更高效的地址校验。- 全线组件升级:
pinocchio、system和token库均升级到了最新版,这意味着项目现在可以全面使用 Zero-Copy 视图(AccountView)。 - 现代 Rust 特性:通过
cargo-features = ["edition2024"],你正在使用尚未完全普及的最前沿 Rust 标准,非常适合作为技术探索类文章的卖点。
lib.rs 文件
src/lib.rs: (使用 declare_id! 和 AccountView)
use pinocchio::{AccountView, Address, ProgramResult, entrypoint, error::ProgramError};
use solana_address::declare_id;
entrypoint!(process_instruction);
pub mod instructions;
pub use instructions::*;
pub mod state;
pub use state::*;
// 22222222222222222222222222222222222222222222
declare_id!("22222222222222222222222222222222222222222222");
fn process_instruction(
_program_id: &Address,
accounts: &[AccountView],
instruction_data: &[u8],
) -> ProgramResult {
match instruction_data.split_first() {
Some((Make::DISCRIMINATOR, data)) => Make::try_from((data, accounts))?.process(),
Some((Take::DISCRIMINATOR, _)) => Take::try_from(accounts)?.process(),
Some((Refund::DISCRIMINATOR, _)) => Refund::try_from(accounts)?.process(),
_ => Err(ProgramError::InvalidInstructionData),
}
}
这段代码展示了 Pinocchio v0.10.1 现代化架构下的程序入口实现:它通过 declare_id! 宏以更简洁的标准方式声明程序身份,并引入了核心的 AccountView 替代传统的账户对象,实现了对链上内存的零拷贝(Zero-copy)访问;在指令分发逻辑上,它保持了高效的字节鉴别器(Discriminator)匹配机制,将经过视图化处理的账户切片精准路由至对应的 Make、Take 或 Refund 指令,标志着程序从“数据持有型”向更高性能的“视图映射型”架构的转变。
💡 新旧对比亮点
几个关键词点出 v0.10.1 的进化:
- 从
Pubkey到Address:语义更明确,内存占用更优化。 - 从
AccountInfo到AccountView:这是性能飞跃的关键,直接在原始字节上操作,不产生中间结构。 declare_id!:告别了旧版手动定义字节数组的繁琐,代码可读性显著提升。
state.rs 文件
src/state.rs: (保持 #[repr(C)] 以支持零拷贝)
use core::mem::size_of;
use pinocchio::{Address, error::ProgramError};
// --- 定义常量种子 ---
pub const ESCROW_SEED: &[u8] = b"escrow";
#[repr(C)]
pub struct Escrow {
pub seed: u64, // Random seed for PDA derivation
pub maker: Address, // Creator of the escrow
pub mint_a: Address, // Token being deposited
pub mint_b: Address, // Token being requested
pub receive: u64, // Amount of token B wanted
pub bump: [u8; 1], // PDA bump seed
}
impl Escrow {
pub const LEN: usize = size_of::<u64>()
+ size_of::<Address>()
+ size_of::<Address>()
+ size_of::<Address>()
+ size_of::<u64>()
+ size_of::<[u8; 1]>();
#[inline(always)]
pub fn load_mut(bytes: &mut [u8]) -> Result<&mut Self, ProgramError> {
if bytes.len() != Escrow::LEN {
return Err(ProgramError::InvalidAccountData);
}
Ok(unsafe { &mut *core::mem::transmute::<*mut u8, *mut Self>(bytes.as_mut_ptr()) })
}
#[inline(always)]
pub fn load(bytes: &[u8]) -> Result<&Self, ProgramError> {
if bytes.len() != Escrow::LEN {
return Err(ProgramError::InvalidAccountData);
}
Ok(unsafe { &*core::mem::transmute::<*const u8, *const Self>(bytes.as_ptr()) })
}
#[inline(always)]
pub fn set_seed(&mut self, seed: u64) {
self.seed = seed;
}
#[inline(always)]
pub fn set_maker(&mut self, maker: Address) {
self.maker = maker;
}
#[inline(always)]
pub fn set_mint_a(&mut self, mint_a: Address) {
self.mint_a = mint_a;
}
#[inline(always)]
pub fn set_mint_b(&mut self, mint_b: Address) {
self.mint_b = mint_b;
}
#[inline(always)]
pub fn set_receive(&mut self, receive: u64) {
self.receive = receive;
}
#[inline(always)]
pub fn set_bump(&mut self, bump: [u8; 1]) {
self.bump = bump;
}
#[inline(always)]
pub fn set_inner(
&mut self,
seed: u64,
maker: Address,
mint_a: Address,
mint_b: Address,
receive: u64,
bump: [u8; 1],
) {
self.seed = seed;
self.maker = maker;
self.mint_a = mint_a;
self.mint_b = mint_b;
self.receive = receive;
self.bump = bump;
}
}
这段代码定义了 Pinocchio v0.10.1 版本中的 Escrow 状态结构体,它在保持 #[repr(C)] 内存布局以支持高效 Zero-Copy 访问的同时,将地址类型全面升级为新版的 Address ;通过内联(inline)的 load 与 load_mut 函数,该结构体能够利用 transmute 指针操作将账户原始字节流瞬间映射为结构体实例,并配合一系列原子化的 Setter 方法,在极低计算开销(Compute Units)下完成对托管条款、种子以及 PDA 偏移值(Bump)的精确读写管理。
“结构体字段从
Pubkey迁移到Address,不仅是类型的更迭,更是 Pinocchio 迈向更规范、更轻量化地址处理的关键一步。”
instructions/helpers.rs 文件
src/helpers.rs: (封装关联账户初始化逻辑)
use pinocchio::cpi::{Seed, Signer};
use pinocchio::error::ProgramError;
use pinocchio::{AccountView, ProgramResult};
use pinocchio_associated_token_account::instructions::CreateIdempotent;
use pinocchio_system::instructions::CreateAccount;
use pinocchio_token::state::TokenAccount;
// --- 1. 签名者检查助手 ---
pub struct SignerAccount;
impl SignerAccount {
#[inline(always)]
pub fn check(account: &AccountView) -> Result<(), ProgramError> {
if !account.is_signer() {
return Err(ProgramError::MissingRequiredSignature);
}
Ok(())
}
}
// --- 2. 程序账户 (PDA/State) 助手 ---
pub struct ProgramAccount;
impl ProgramAccount {
#[inline(always)]
/// 初始化程序状态账户 (如 Escrow)
pub fn init<T>(
payer: &AccountView, // 1. 支付租金的人
new_account: &AccountView, // 2. 要创建的 PDA
signer_seeds: &[Seed], // 3. PDA 种子
space: usize, // 4. 空间大小
lamports: u64,
) -> ProgramResult {
// 计算租金 (这里假设你已经算好了 lamports,或者调用系统程序计算)
// 最简单的做法是调用 pinocchio_system 的 CreateAccount
CreateAccount {
from: payer,
to: new_account,
lamports,
space: space as u64,
owner: &crate::ID,
}
.invoke_signed(&[Signer::from(signer_seeds)])
}
/// 检查该账户是否由本程序拥有
#[inline(always)]
pub fn check(account: &AccountView) -> Result<(), ProgramError> {
if !account.owned_by(&crate::ID) {
return Err(ProgramError::InvalidAccountOwner);
}
Ok(())
}
/// 关闭账户并回收 Lamports (常用于 Refund/Take)
pub fn close(account: &AccountView, destination: &AccountView) -> ProgramResult {
// 将 lamports 转移给接收者
let lamports = account.lamports();
account.set_lamports(0);
destination.set_lamports(destination.lamports() + lamports);
// 清理数据并将所有者重置为系统程序
account.close()
}
}
// --- 3. Mint (代币定义) 助手 ---
pub struct MintInterface;
impl MintInterface {
#[inline(always)]
pub fn check(account: &AccountView) -> Result<(), ProgramError> {
// SPL Token Mint 固定长度为 82
// 且必须由 Token Program 拥有
if account.data_len() != 82 {
// SPL Token Mint 固定长度
return Err(ProgramError::InvalidAccountData);
}
Ok(())
}
}
// --- 4. 关联代币账户 (ATA) 助手 ---
pub struct AssociatedTokenAccount;
impl AssociatedTokenAccount {
pub fn init(
account: &AccountView, // 1. 要创建的 ATA
mint: &AccountView, // 2. Mint
funding_account: &AccountView, // 3. 所有者 (Owner)
wallet: &AccountView, // 4. 出钱的人 (Payer)
system_program: &AccountView, // 5. 系统程序
token_program: &AccountView, // 6. 代币程序
) -> ProgramResult {
CreateIdempotent {
funding_account,
account,
wallet,
mint,
system_program,
token_program,
}
.invoke()
}
pub fn init_if_needed(
account: &AccountView,
mint: &AccountView,
funding_account: &AccountView, // 教程传入的第3个参数
wallet: &AccountView,
system_program: &AccountView,
token_program: &AccountView,
) -> ProgramResult {
CreateIdempotent {
funding_account,
account,
wallet,
mint,
system_program,
token_program,
}
.invoke()
}
pub fn check(
ata: &AccountView,
owner: &AccountView,
mint: &AccountView,
_token_program: &AccountView,
) -> Result<(), ProgramError> {
let token_account = TokenAccount::from_account_view(ata)?;
if token_account.owner() != owner.address() || token_account.mint() != mint.address() {
return Err(ProgramError::InvalidAccountData);
}
Ok(())
}
}
这段代码在 Pinocchio v0.10.1 环境下构建了一套更加安全且高效的工具库,其核心进化在于全面拥抱了 AccountView 架构:它不仅简化了账户所有权与签名的校验逻辑,更重要的是,在关闭账户回收 Lamports 时,告别了旧版的 unsafe 指针操作,转而使用官方提供的 set_lamports 安全接口;通过对 PDA 初始化、ATA 幂等创建以及代币账户一致性的封装,该助手库在保持底层高性能特性的同时,显著提升了代码的可读性与安全性。
💡 v0.10.1 中 ProgramAccount::close 的变化亮点
- 旧版 (v0.9.2):需要使用
*account.borrow_mut_lamports_unchecked() = 0这种危险的指针解引用。 - 新版 (v0.10.1):直接调用
account.set_lamports(0),语义清晰且受框架保护。
创建
instructions/make.rs 文件
use pinocchio::{AccountView, Address, ProgramResult, cpi::Seed, error::ProgramError};
use pinocchio_token::instructions::Transfer;
use crate::{
AssociatedTokenAccount, ESCROW_SEED, Escrow, MintInterface, ProgramAccount, SignerAccount,
};
/// 初始化托管记录并存储所有交易条款。
/// 创建金库(一个由 mint_a 拥有的 escrow 的关联代币账户 (ATA))。
/// 使用 CPI 调用 SPL-Token 程序,将创建者的 Token A 转移到该金库中。
pub struct MakeAccounts<'a> {
pub maker: &'a AccountView,
pub escrow: &'a AccountView,
pub mint_a: &'a AccountView,
pub mint_b: &'a AccountView,
pub maker_ata_a: &'a AccountView,
pub vault: &'a AccountView,
pub system_program: &'a AccountView,
pub token_program: &'a AccountView,
}
impl<'a> TryFrom<&'a [AccountView]> for MakeAccounts<'a> {
type Error = ProgramError;
fn try_from(accounts: &'a [AccountView]) -> Result<Self, Self::Error> {
let [
maker,
escrow,
mint_a,
mint_b,
maker_ata_a,
vault,
system_program,
token_program,
_,
] = accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
};
// Basic Accounts Checks
SignerAccount::check(maker)?;
MintInterface::check(mint_a)?;
MintInterface::check(mint_b)?;
AssociatedTokenAccount::check(maker_ata_a, maker, mint_a, token_program)?;
// Return the accounts
Ok(Self {
maker,
escrow,
mint_a,
mint_b,
maker_ata_a,
vault,
system_program,
token_program,
})
}
}
pub struct MakeInstructionData {
pub seed: u64,
pub receive: u64,
pub amount: u64,
}
impl<'a> TryFrom<&'a [u8]> for MakeInstructionData {
type Error = ProgramError;
fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
if data.len() != size_of::<u64>() * 3 {
return Err(ProgramError::InvalidInstructionData);
}
let seed = u64::from_le_bytes(data[0..8].try_into().unwrap());
let receive = u64::from_le_bytes(data[8..16].try_into().unwrap());
let amount = u64::from_le_bytes(data[16..24].try_into().unwrap());
// Instruction Checks
if amount == 0 {
return Err(ProgramError::InvalidInstructionData);
}
Ok(Self {
seed,
receive,
amount,
})
}
}
pub struct Make<'a> {
pub accounts: MakeAccounts<'a>,
pub instruction_data: MakeInstructionData,
pub bump: u8,
}
impl<'a> TryFrom<(&'a [u8], &'a [AccountView])> for Make<'a> {
type Error = ProgramError;
fn try_from((data, accounts): (&'a [u8], &'a [AccountView])) -> Result<Self, Self::Error> {
let accounts = MakeAccounts::try_from(accounts)?;
let instruction_data = MakeInstructionData::try_from(data)?;
// Initialize the Accounts needed
let (_, bump) = Address::find_program_address(
&[
ESCROW_SEED,
accounts.maker.address().as_ref(),
&instruction_data.seed.to_le_bytes(),
],
&crate::ID,
);
let seed_bytes = instruction_data.seed.to_le_bytes();
let bump_binding = [bump];
let escrow_seeds = [
Seed::from(ESCROW_SEED),
Seed::from(accounts.maker.address().as_ref()),
Seed::from(&seed_bytes),
Seed::from(&bump_binding),
];
ProgramAccount::init::<Escrow>(
accounts.maker,
accounts.escrow,
&escrow_seeds,
Escrow::LEN,
2_000_000,
)?;
// Initialize the vault
AssociatedTokenAccount::init(
accounts.vault,
accounts.mint_a,
accounts.maker,
accounts.escrow,
accounts.system_program,
accounts.token_program,
)?;
Ok(Self {
accounts,
instruction_data,
bump,
})
}
}
impl<'a> Make<'a> {
pub const DISCRIMINATOR: &'a u8 = &0;
pub fn process(&mut self) -> ProgramResult {
// Populate the escrow account
let mut data_guard = self.accounts.escrow.try_borrow_mut()?;
let escrow = Escrow::load_mut(&mut data_guard)?;
escrow.set_inner(
self.instruction_data.seed,
self.accounts.maker.address().clone(),
self.accounts.mint_a.address().clone(),
self.accounts.mint_b.address().clone(),
self.instruction_data.receive,
[self.bump],
);
// Transfer tokens to vault
Transfer {
from: self.accounts.maker_ata_a,
to: self.accounts.vault,
authority: self.accounts.maker,
amount: self.instruction_data.amount,
}
.invoke()?;
Ok(())
}
}
这段代码展示了在 Pinocchio v0.10.1 中实现 Make 指令的现代方式,其核心在于利用 AccountView 和新版 Address 接口进行极速的数据处理:它通过 TryFrom 模式在解析指令数据的同时,直接完成 Escrow PDA 的派生与初始化、以及 Vault 金库账户的创建;随后在执行阶段,利用 try_borrow_mut() 获取账户数据的视图引用,并配合 load_mut 零拷贝地将交易条款写入状态账户,最后发起代币转移 CPI,整个流程在保持逻辑严密性的同时,通过减少内存拷贝显著优化了链上计算资源的消耗。
💡 v0.10.1 实现的亮点
- 架构进化:注意
Make::try_from现在不仅负责解析,还承担了账户初始化的职责,这种封装让process函数变得极其简洁,只专注于业务数据的填充。 - 生命周期管理:代码中使用了
let mut data_guard = ...,这是新版视图模型下推荐的写法,确保了账户数据借用在写入完成后能及时释放。 - 地址克隆:由于
Address实现了高效的内存布局,使用.address().clone()可以安全且廉价地在状态结构体中存储地址。
接受
take 指令完成交换操作:
- 关闭托管记录,将其租金 lamports 返还给创建者。
- 将 Token A 从保管库转移到接受者,然后关闭保管库。
- 将约定数量的 Token B 从接受者转移到创建者。
instructions/take.rs 文件
use std::slice;
use pinocchio::{
AccountView, Address, ProgramResult,
cpi::{Seed, Signer},
error::ProgramError,
};
use pinocchio_token::{
instructions::{CloseAccount, Transfer},
state::TokenAccount,
};
use crate::{
AssociatedTokenAccount, ESCROW_SEED, Escrow, MintInterface, ProgramAccount, SignerAccount,
};
/*
关闭托管记录,将其租金 lamports 返还给创建者。
将 Token A 从保管库转移到接受者,然后关闭保管库。
将约定数量的 Token B 从接受者转移到创建者。
*/
pub struct TakeAccounts<'a> {
pub taker: &'a AccountView,
pub maker: &'a AccountView,
pub escrow: &'a AccountView,
pub mint_a: &'a AccountView,
pub mint_b: &'a AccountView,
pub vault: &'a AccountView,
pub taker_ata_a: &'a AccountView,
pub taker_ata_b: &'a AccountView,
pub maker_ata_b: &'a AccountView,
pub system_program: &'a AccountView,
pub token_program: &'a AccountView,
}
impl<'a> TryFrom<&'a [AccountView]> for TakeAccounts<'a> {
type Error = ProgramError;
fn try_from(accounts: &'a [AccountView]) -> Result<Self, Self::Error> {
let [
taker,
maker,
escrow,
mint_a,
mint_b,
vault,
taker_ata_a,
taker_ata_b,
maker_ata_b,
system_program,
token_program,
_,
] = accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
};
// Basic Accounts Checks
SignerAccount::check(taker)?;
ProgramAccount::check(escrow)?;
MintInterface::check(mint_a)?;
MintInterface::check(mint_b)?;
AssociatedTokenAccount::check(taker_ata_b, taker, mint_b, token_program)?;
AssociatedTokenAccount::check(vault, escrow, mint_a, token_program)?;
// Return the accounts
Ok(Self {
taker,
maker,
escrow,
mint_a,
mint_b,
taker_ata_a,
taker_ata_b,
maker_ata_b,
vault,
system_program,
token_program,
})
}
}
pub struct Take<'a> {
pub accounts: TakeAccounts<'a>,
}
impl<'a> TryFrom<&'a [AccountView]> for Take<'a> {
type Error = ProgramError;
fn try_from(accounts: &'a [AccountView]) -> Result<Self, Self::Error> {
let accounts = TakeAccounts::try_from(accounts)?;
// Initialize necessary accounts
AssociatedTokenAccount::init_if_needed(
accounts.taker_ata_a,
accounts.mint_a,
accounts.taker,
accounts.taker,
accounts.system_program,
accounts.token_program,
)?;
AssociatedTokenAccount::init_if_needed(
accounts.maker_ata_b,
accounts.mint_b,
accounts.taker,
accounts.maker,
accounts.system_program,
accounts.token_program,
)?;
Ok(Self { accounts })
}
}
impl<'a> Take<'a> {
pub const DISCRIMINATOR: &'a u8 = &1;
pub fn process(&mut self) -> ProgramResult {
let data = self.accounts.escrow.try_borrow()?;
let escrow = Escrow::load(&data)?;
// Check if the escrow is valid
let escrow_key = Address::create_program_address(
&[
ESCROW_SEED,
self.accounts.maker.address().as_ref(),
&escrow.seed.to_le_bytes(),
&escrow.bump,
],
&crate::ID,
)?;
if &escrow_key != self.accounts.escrow.address() {
return Err(ProgramError::InvalidAccountOwner);
}
let seed_bytes = escrow.seed.to_le_bytes();
let escrow_seeds = [
Seed::from(ESCROW_SEED),
Seed::from(self.accounts.maker.address().as_ref()),
Seed::from(&seed_bytes),
Seed::from(&escrow.bump),
];
let signer = Signer::from(&escrow_seeds);
let amount = TokenAccount::from_account_view(self.accounts.vault)?.amount();
// Transfer from the Vault to the Taker
Transfer {
from: self.accounts.vault,
to: self.accounts.taker_ata_a,
authority: self.accounts.escrow,
amount,
}
.invoke_signed(slice::from_ref(&signer))?;
// Close the Vault
CloseAccount {
account: self.accounts.vault,
destination: self.accounts.maker,
authority: self.accounts.escrow,
}
.invoke_signed(slice::from_ref(&signer))?;
// Transfer from the Taker to the Maker
Transfer {
from: self.accounts.taker_ata_b,
to: self.accounts.maker_ata_b,
authority: self.accounts.taker,
amount: escrow.receive,
}
.invoke()?;
// Close the Escrow
drop(data);
ProgramAccount::close(self.accounts.escrow, self.accounts.taker)?;
Ok(())
}
}
这段代码在 Pinocchio v0.10.1 下实现了托管合约最核心的 Take 指令,通过 AccountView 视图和 Address 校验逻辑,原子化地处理了复杂的资产交换与账户销毁:它首先确保接收者与创建者的代币账户准备就绪,随后在校验 Escrow 状态合法性后,利用 PDA 签名将金库中的 Token A 拨付给接收者并销毁金库,紧接着将接收者的 Token B 转移至创建者,最后通过高效的 ProgramAccount::close 回收托管账户租金并清理现场,实现了从资产互换到资源释放的全流程闭环。
💡 v0.10.1 的技术亮点对比
- 视图借用管理:在处理数据前使用
self.accounts.escrow.try_borrow(),并在关闭账户前显式调用drop(data),这种精确的借用控制体现了新版对内存安全的严苛要求。 - 地址处理现代化:使用
Address::create_program_address替代了旧版的全局方法,使得地址派生逻辑更具类型安全性且符合新版的 API 规范。 - 零拷贝性能:整个
process逻辑中,无论是TokenAccount::from_account_view还是Escrow::load,全部基于底层视图实现,没有任何多余的内存拷贝。
退款
refund 指令允许创建者取消一个未完成的报价:
- 关闭托管 PDA,并将其租金 lamports 返还给创建者。
- 将代币 A 的全部余额从保险库转回创建者,然后关闭保险库账户。
instructions/refund.rs 文件
/*
refund 指令允许创建者取消一个未完成的报价:
关闭托管 PDA,并将其租金 lamports 返还给创建者。
将代币 A 的全部余额从保险库转回创建者,然后关闭保险库账户。
*/
use std::slice;
use pinocchio::{
AccountView, ProgramResult,
cpi::{Seed, Signer},
error::ProgramError,
};
use pinocchio_token::{
instructions::{CloseAccount, Transfer},
state::TokenAccount,
};
use crate::{AssociatedTokenAccount, ESCROW_SEED, Escrow, ProgramAccount};
pub struct Refund<'a> {
pub maker: &'a AccountView,
pub escrow: &'a AccountView,
pub mint_a: &'a AccountView,
pub vault: &'a AccountView,
pub maker_ata_a: &'a AccountView,
pub associated_token_program: &'a AccountView,
pub token_program: &'a AccountView,
pub system_program: &'a AccountView,
}
impl<'a> Refund<'a> {
pub const DISCRIMINATOR: &'a u8 = &2;
pub fn try_from(accounts: &'a [AccountView]) -> Result<Self, ProgramError> {
// 使用简单的切片模式匹配来获取账户,性能最优
let [
maker, // 1. Signer
escrow, // 2. Escrow PDA
mint_a, // 3. Mint A (Anchor 里的第三个账户)
vault, // 4. Vault (Token Account)
maker_ata_a, // 5. Maker ATA
associated_token_program,
token_program, // 6. Token Program
system_program, // 7. System Program
] = accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
};
Ok(Self {
maker,
escrow,
mint_a,
vault,
maker_ata_a,
associated_token_program,
token_program,
system_program,
})
}
pub fn process(&self) -> ProgramResult {
AssociatedTokenAccount::init_if_needed(
self.maker_ata_a,
self.mint_a,
self.maker,
self.maker,
self.system_program,
self.token_program,
)?;
// 1. 获取 Escrow 数据视图 (零拷贝)
let data = self.escrow.try_borrow()?;
let escrow_state = Escrow::load(&data)?;
// 2. 构造 PDA 签名
let seed_bytes = escrow_state.seed.to_le_bytes();
let seeds = [
Seed::from(ESCROW_SEED),
Seed::from(self.maker.address().as_ref()),
Seed::from(&seed_bytes),
Seed::from(&escrow_state.bump),
];
let signer = Signer::from(&seeds);
// 检查 amount 之前确保 vault 数据有效
let amount = TokenAccount::from_account_view(self.vault)?.amount();
if amount > 0 {
// 执行转账: Vault (from) -> Maker ATA (to)
// 必须确认识别到的 vault 账户的所有者是 escrow PDA
Transfer {
from: self.vault,
to: self.maker_ata_a,
authority: self.escrow, // 这里必须是 PDA
amount,
}
.invoke_signed(slice::from_ref(&signer))?;
}
// 关闭 Vault 账户
CloseAccount {
account: self.vault,
destination: self.maker,
authority: self.escrow,
}
.invoke_signed(&[signer])?;
drop(data); // 必须在 close escrow 前释放
ProgramAccount::close(self.escrow, self.maker)?;
Ok(())
}
}
这段代码在 Pinocchio v0.10.1 框架下实现了 Refund 指令,用于撤销托管并回收资产:它首先通过 AccountView 零拷贝视图读取合约状态并重建 PDA 签名,随后在确保创建者关联代币账户就绪的前提下,通过跨程序调用(CPI)将金库中锁定的 Token A 全额转回给创建者,最后利用 CloseAccount 销毁金库并调用 ProgramAccount::close 手动回收托管账户的租金(Lamports),从而在彻底清理链上存储空间的同时,保障了资金退还的安全性与原子性。
💡 这段 Refund 实现展示了新版框架在处理“清理逻辑”时的优雅
- 视图借用与释放:代码通过
drop(data)严格遵守 Rust 的借用规则,确保在尝试修改账户 Lamports(执行close)之前,没有任何悬挂的只读引用,这比 v0.9.2 手动管理指针更加安全。 - 指令布局一致性:尽管底层 API 从
AccountInfo升级到了AccountView,但指令的核心路由逻辑保持了一致,这使得老项目的迁移重点在于“数据访问方式”的重构,而非业务逻辑的重写。
五、构建
pinocchio-escrow-workspace/programs/solana_pinocchio_escrow on main is 📦 0.1.0 via 🦀 1.92.0
➜ cargo build-sbf --version
solana-cargo-build-sbf 3.0.13
platform-tools v1.51
rustc 1.84.1
pinocchio-escrow-workspace/programs/solana_pinocchio_escrow on main is 📦 0.1.0 via 🦀 1.92.0
➜ cargo build-sbf
Finished `release` profile [optimized] target(s) in 0.15s
pinocchio-escrow-workspace/programs/pinocchio_escrow on main is 📦 0.1.0 via 🦀 1.92.0
➜ cargo build-sbf
Finished `release` profile [optimized] target(s) in 0.16s
这段构建结果展示了 Pinocchio 框架在性能上的绝对统治力:利用最新版的 solana-cargo-build-sbf 3.0.13 工具链,该程序在 release 优化模式下的编译速度达到了惊人的 0.15 秒 级别,这不仅体现了 Rust 1.84.1 编译器与平台工具链的高效协同,更直接证明了 Pinocchio 通过“去中心化依赖”和“零抽象”设计,极大地简化了 LLVM 的编译后端负载,使得开发者能够在几乎瞬时的反馈循环中完成高性能轻量级智能合约的构建。
💡 构建结果关键指标
| 指标 | 详情 | 说明 |
|---|---|---|
| 编译耗时 | 0.15s - 0.16s | 极速编译,远超传统 Anchor 框架。 |
| 工具链 | Platform-tools v1.51 | 支持 Solana 最新的 eBPF 优化特性。 |
| 优化配置 | [optimized] release | 最终生成的 .so 文件将拥有极小的体积。 |
🚀 为什么这么快?
Pinocchio 几乎不依赖复杂的宏展开和繁重的标准库重构,它更像是直接在写“裸机”代码。这种低开销不仅体现在链上执行时节省 CU,在开发阶段也通过极短的编译时间极大地提升了开发者的生产力。
总结
通过对 Pinocchio 从 v0.9.2 到 v0.10.1 的全流程实战,我们见证了轻量级合约开发的范式转移:
- 状态定义升级:通过
Address替代Pubkey并配合load_mut映射,在保持 Zero-Copy 访问的同时,实现了地址处理的规范化与极致读写。 - 助手库的安全封装 (helpers.rs):在
ProgramAccount::close中以安全接口set_lamports(0)替代旧版的unsafe指针操作,标志着框架在安全性上的关键进化。 - 指令逻辑的高效实现 (Make/Take/Refund):利用
AccountView视图模型和TryFrom模式,将 PDA 初始化、资产互换与账户销毁逻辑原子化,显著降低了链上计算开销(CU)。 - 构建性能的绝对统治:0.15s 的编译反馈与极致小巧的
.so文件,直接证明了 Pinocchio 在简化 LLVM 后端负载上的设计优势。
一句话总结: Pinocchio v0.10.1 证明了,在 Solana 上编写“极致性能”的代码,依然可以拥有“现代编程”的优雅、安全与丝滑。
参考
- https://docs.rs/pinocchio-associated-token-account/latest/pinocchio_associated_token_account/
- https://learn.blueshift.gg/zh-CN/challenges/pinocchio-escrow
- https://github.com/qiaopengjun5162/pinocchio-escrow-workspace
- https://github.com/Solana-ZH/Solana-bootcamp-2026-s1
- https://github.com/anza-xyz/pinocchio
- https://github.com/codama-idl/codama
- https://github.com/metaplex-foundation/shank
- https://crates.io/crates/pinocchio-log
- https://docs.rs/pinocchio/latest/pinocchio/