Solana

2026年01月27日更新 17 人订阅
原价: ¥ 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 合约全记录

仅 0.6 秒编译!用 Pinocchio 打造极致轻量化 Solana Vault 合约全记录

仅0.6秒编译!用Pinocchio打造极致轻量化SolanaVault合约全记录在Solana开发世界中,性能和效率是永远的关键词。你是否厌倦了臃肿的框架依赖?想尝试更纯粹、更快速的原生Rust开发吗?本文将带你走进Pinocchio的世界——一个无外部依赖、极致零拷贝

仅 0.6 秒编译!用 Pinocchio 打造极致轻量化 Solana Vault 合约全记录

在 Solana 开发世界中,性能和效率是永远的关键词。你是否厌倦了臃肿的框架依赖?想尝试更纯粹、更快速的原生 Rust 开发吗?本文将带你走进 Pinocchio 的世界——一个无外部依赖、极致零拷贝的库。我们将从创建一个简单的 Vault 存款与取款程序开始,利用 ShankCodama 构建一套自动化的客户端 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() 获取金库当前所有余额并进行转账。
  • PDA 签名机制:由于金库(vault)是一个 PDA 账户,它没有私钥,因此必须在调用系统程序的 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 二进制文件。

🛠 自动化代码生成:

Shank + Codama

Shank 生成IDL文件

1. 安装工具 (Shank CLI)

cargo install shank-cli

查看版本信息确认安装成功

shank --version
shank-cli 0.4.6

2. 定义合约功能清单 (Instruction Schema) (让工具读懂你的业务)

基于枚举的指令分发 (Enum-based Dispatching)

基于枚举的指令分发,就是给合约里的每个功能编个号,然后根据用户发来的编号,自动把任务派发给正确的处理函数。

简单来说,这一步就是给合约写一份“说明书”。通过定义一个枚举类,我们告诉工具(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)。

3. 生成并导出 IDL 文件 (生成标准说明书)

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"]注释 ,执行完毕后要继续注释或者删除!

codama 生成客户端代码

创建客户端项目并初始化

➜ 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

方式一:脚本生成 程序化脚本 (Scripting / Programmatic API)

实现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

方式二:声明式配置 (Configuration-driven / CLI-first)

codama init 生成 codama.json 的方式

你不再编写“如何做”的代码,而是编写一个“要做什么”的 配置文件 (Configuration File)。Codama CLI 会根据这个 JSON 文件自动运行内部的指令。

Codama 初始化:构建自动化 SDK 生成器的配置文件


blueshift_vault/clients on  master [?] is 📦 1.0.0 via 🦀 1.92.0 
➜ bunx codama init    
Welcome to Codama!
✔ Where is yo...

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

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

0 条评论

请先 登录 后评论