Solana

2026年02月05日更新 18 人订阅
原价: ¥ 8.6 限时优惠
专栏简介 Solana 开发学习之Solana 基础知识 Solana 开发学习之通过RPC与Solana交互 Web3与Solana实操指南:如何签名与发送交易 Web3 新玩法:Solana Nonce Account 让你交易无忧 Web3 快上手:Solana 造你的链上名片 Web3 开发实战:用 Anchor 打造 Solana 猜数游戏 @solana/web3.js 2.0:Solana 转账全流程解析 玩转 Web3 Solana:从零到代币开发 Web3 开发入门:Solana CLI 配置与本地验证器实战 Web3 Eclipse 开发环境搭建与资产跨链桥接实战指南 用 Gill 库解锁 Web3:Solana 代币转账实战 Web3开发:用Rust实现Solana SOL转账教程 用 Rust 开发 Solana:解锁 Web3 交易费用计算 Web3开发入门:Solana账户创建与Rust实践全攻略 Web3 实战:用 Anchor 打造 Solana 智能合约全流程 Solana Web3 快速入门:创建并获取钱包账户的完整指南 Web3 开发实操:用 Anchor 在 Solana 创建代币 Mint Account 从零到 Web3:使用 @solana/kit 快速查询 Solana 账户余额 快速上手 Web3:用 @solana/kit 在 Solana 上创建钱包并查询余额 Web3实战:使用Anchor与Rust开发和调用Solana智能合约 Web3实战:Solana CPI全解析,从Anchor封装到PDA转账 用 Rust 在 Solana 上打造你的专属代币:从零到一的 Web3 实践 探索Solana SDK实战:Web3开发的双路径与轻量模块化 手把手教你用 Solana Token-2022 创建支持元数据的区块链代币 Solana 开发实战:Rust 客户端调用链上程序全流程 Solana 开发进阶:在 Devnet 上实现链上程序部署、调用与更新 Solana 开发进阶:链上事件到链下解析全攻略 从零打造Solana空投工具库:Rust开发实战指南 从零开始:用 Rust 开发 Solana 链上 Token 元数据查询工具 Solana 智能合约终极部署指南:从入门到主网,定制你的专属靓号 Program ID 【Solana 开发实战】轻松搞定链上 IDL:从上传到获取全解析 Solana 投票 DApp 开发实战:从合约到部署的完整指南 Surfpool:Solana 上的 Anvil,本地开发闪电般⚡️ 【Solana实操】64字节私钥文件解析难题:用三种姿势安全获取钱包地址 Solana 密钥实战:一文搞懂私钥、公钥、PDA 的底层关系与 CLI 操作 Solana 地址进阶:从 TS/JS 到 Rust SDK V3,完全掌握公钥与 PDA 的底层逻辑 Solana 开发者笔记:PDA 与账户操作的10个关键要点 拒绝“版本代差”:基于 Solana SDK V3 的「链上动态存储器」工业级实现 从零到 Devnet:Solana Anchor Vault 个人金库开发全流程实操 Anchor 中一个隐蔽但致命的坑:Accounts 顺序导致 AccountNotInitialized 从核心逻辑到上链部署:Solana Anchor 托管程序实战全记录 Solana 开发实战:使用 @solana/web3.js 与 Bun 铸造首个 SPL 代币 Solana 开发实战:使用 @solana/kit (v2) 发行 SPL 代币全流程 仅 0.6 秒编译!用 Pinocchio 打造极致轻量化 Solana Vault 合约全记录 深度解析:解决 Pinocchio 框架下 Address 方法“爆红”与编译冲突 极致轻量:Solana Pinocchio v0.10 演进实战,0.15s 极速开启托管合约新篇章

极致轻量:Solana Pinocchio v0.10 演进实战,0.15s 极速开启托管合约新篇章

极致轻量:SolanaPinocchiov0.10演进实战,0.15s极速开启托管合约新篇章在Solana极简开发领域,Pinocchio框架一直以其“零抽象”带来的性能统治力著称。然而,随着v0.10.1版本的发布,这场关于极致性能的实验迎来了一个关键分水岭。本文将带你深度复

极致轻量:Solana Pinocchio v0.10 演进实战,0.15s 极速开启托管合约新篇章

在 Solana 极简开发领域,Pinocchio 框架一直以其“零抽象”带来的性能统治力著称。然而,随着 v0.10.1 版本的发布,这场关于极致性能的实验迎来了一个关键分水岭。

本文将带你深度复盘一个 Escrow(托管合约) 的进化之旅。我们将从 v0.9.2 的底层指针操作出发,一路跨越到 v0.10.1 全面拥抱 AccountViewAddress 新架构的现代实战。你将看到,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),从而将链上请求精准路由至 MakeTakeRefund 模块,完成从原始 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(零拷贝) 高性能模式的核心。通过在 loadload_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 函数的操作可以归纳为以下四个步骤:

  1. 确定身份 (PDA Derivation):利用 ESCROW_SEED、创建者地址和随机种子计算出唯一的 PDA 及其 bump
  2. 空间分配 (Account Init):调用 ProgramAccount::initAssociatedTokenAccount::init 为合约状态和代币存放点分配物理空间。
  3. 协议存证 (State Storage):利用之前定义的 Zero-Copy load_mut 方法,将交易条款(如期望获得的 Token B 数量)直接写入 PDA 内存。
  4. 资产锁定 (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 转移给创建者;最后,通过手动回收租金的方式关闭托管账户,从而在链上安全地解除了这笔原子交易。

💡 核心操作流程图解

  1. 自动补全账户:为 Taker 准备收 A 的账户,为 Maker 准备收 B 的账户(如果不存在则创建)。
  2. PDA 权限校验:重新计算 PDA 地址以确保操作的是正确的托管实例。
  3. 两阶段转移
    • 由合约转出 (Signed CPI):Vault (Token A) $\rightarrow$ Taker。
    • 由用户转入 (Direct CPI):Taker (Token B) $\rightarrow$ Maker。
  4. 清理现场:销毁 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::fro...

剩余50%的内容订阅专栏后可查看

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论