如何使用 Pinocchio 构建和部署Solana程序

本文介绍了Pinocchio,一个轻量级的、零依赖的Rust库,用于编写高性能的Solana程序。通过构建一个简单的Vault程序,演示了如何使用Pinocchio进行Solana程序开发,并结合Shank、Codama和Solana Kit SDK,实现了IDL生成、客户端脚手架和端到端测试。文章还对比了Pinocchio与Anchor的差异,强调Pinocchio在性能和控制方面的优势。

概述

Pinocchio 是一个轻量级的、零依赖的库,用于编写 Solana 程序,专为那些希望获得最大性能而又不想承受大型框架开销的开发者而构建。

在本指南中,你将使用 Pinocchio 构建一个 Vault 程序,该程序允许用户将 SOL 存入和提取到 Vault PDA 中。在此过程中,你将使用 Shank、Codama 和 Solana Kit SDK 来处理 IDL 生成、客户端脚手架和端到端测试。

你将做什么

  • 使用 Pinocchio 设置 Solana 程序
  • 开发一个包含两个指令处理程序的 vault 程序:DepositWithdraw。只有存款人才能从 vault 中提取资金。
  • 将 Shank 性添加到你的程序并生成 IDL
  • 使用 IDL 通过 Codama 创建一个 TypeScript 客户端
  • 使用@solana/kit编写一个 TS 客户端进行端到端测试

你需要什么

本指南假设你对 Solana 编程、Rust 和 TypeScript 有基本的了解。你还可以参考我们现有的 Solana 指南以了解更多信息:

在开始之前,请确保你已安装以下内容:

依赖项 版本
Rust 1.90.0
solana-cli 3.0.6
Node 24.8.0
shank 0.4
@solana/kit 3.0.3
tsx 4.20.6
codama 1.3.7

什么是 Pinocchio?

Pinocchio 是一个零依赖的 Rust 框架,用于编写 Solana 程序,它优先考虑性能和精确控制。与更高级别的框架不同,它避免了不必要的抽象和样板,以保持程序快速高效。

Pinocchio 程序指示 Rust 编译器默认不包含标准库,使用#![no_std]属性。

这种方法使它们与 Solana 的轻量级链上运行时兼容,在该运行时中,程序以确定性的方式执行,无需操作系统、文件系统或线程。为了支持这种环境,Pinocchio 依赖于 Rust 的最小core库及其自身的实用程序,以仅提供安全有效地运行所需的必要的东西。

优点

这种“裸机”方法使 Pinocchio 保持精简和高效,从而带来以下几个实际优势:

  • 更小的二进制文件: 凭借更少的抽象,Pinocchio 程序部署得更快且成本更低,因为部署成本与二进制文件大小成正比。
  • 更低的计算单元 (CU) 使用率: 零拷贝访问帐户数据消除了昂贵的序列化和反序列化,从而节省了计算单元。这在接近 Solana 计算限制的程序中至关重要。
  • 没有外部依赖项: 默认情况下,它是纯 Rust,类似于嵌入式系统开发,你只引入必要的东西。

权衡

但是,这些好处是有代价的:

  • 手动设置: 你负责编写指令分发器、帐户验证和数据布局。没有自动生成的样板或宏脚手架。
  • 更多责任: 你必须显式检查所有权、签名者状态和指令流。
  • 额外的工具: 由于 Pinocchio 不会自动生成 IDL,因此你需要诸如 ShankCodama之类的工具来生成一个。

Pinocchio vs Anchor

Anchor 和 Pinocchio 都简化了构建 Solana 程序的过程,但它们的设计理念截然不同。

Anchor 优先考虑开发人员的生产力和团队的可扩展性。它使用属性宏、自动生成的样板和内置 IDL 生成来简化设置并强制执行最佳实践。对于许多团队来说,这意味着更快的入职、更简洁的约定和更少的手动验证。权衡是更重的抽象,这可能导致更大的二进制文件和更高的计算使用率。

另一方面,Pinocchio 是为那些重视控制和性能而不是便利性的开发人员而构建的。它采用极简主义方法:没有标准库,没有依赖膨胀,并通过零拷贝模式直接访问帐户数据。这种设计产生更小的二进制文件和更严格的计算预算,但这也意味着你需要手动编写指令、验证帐户和生成 IDL。

尽管级别较低,但许多开发人员发现 Pinocchio 出奇地平易近人。例如,在 Anchor 中检查帐户是否已签署交易需要在#[derive(Accounts)]结构体中使用 Signer 类型,而在 Pinocchio 中,它只是一个if语句。

Pinocchio 的设计以性能和细粒度控制为首要任务,即使这意味着牺牲一些开发人员的便利性。

因素 Pinocchio Anchor
设计目标 最小,no_std,只引入必要的 人体工程学,宏,约定
性能(计算单元) 精简路径;零拷贝访问减少 CU 来自序列化/框架层的更高 CU
二进制文件大小和部署成本 更小的二进制文件;更便宜,更快的部署 由于抽象而导致更大的二进制文件
开发者体验 手动指令连接和检查 生成样板;更顺畅的入职
IDL 和工具 外部 IDL 生成(例如,Shank/Codama) 内置 IDL;广泛的工具支持
风险概况 如果遗漏了验证,则会有更多的失误 通过模式和宏进行防护

如果你为了速度、协作或团队之间的可维护性而进行优化,那么 Anchor 是一个不错的选择。如果你正在推动 Solana 的性能限制或想要完全控制每个指令和帐户检查,那么 Pinocchio 可以让你自由地做到这一点。

创建 Pinocchio Vault 程序

让我们首先使用 Pinocchio 创建一个简单的 Vault 程序。此程序的范围是故意涵盖使用 Pinocchio 构建程序的核心概念。

我们的 Vault 程序将:

  • 在首次存款时创建一个 Vault PDA。
  • 允许所有者将 SOL 存入 Vault。
  • 仅允许所有者提取存储的余额。

大多数逻辑都涉及手动验证。入口点解析一个字节的鉴别器,并将执行路由到DepositWithdraw

初始化项目

创建一个新的 Rust 库项目并安装所需的依赖项:

cargo new pinocchio-vault --lib --edition 2021
cd pinocchio-vault
cargo add pinocchio pinocchio-system pinocchio-log pinocchio-pubkey shank

更新 Cargo 配置

接下来,打开你的Cargo.toml文件并更新它以包含用于生成部署工件的配置:

[lib]
crate-type = ["lib", "cdylib"]

创建 lib.rs

接下来,我们将创建处理客户端与你的程序交互时发生的情况的文件:

touch src/lib.rs

lib.rs充当程序的入口点,即发送事务时执行的第一个代码。它接收程序 ID、帐户和指令数据,然后读取第一个字节作为鉴别器来确定要调用的方法(例如,0 = Deposit,1 = Withdraw)。

// lib.rs
#![no_std]

use pinocchio::{
    account_info::AccountInfo,
    entrypoint,
    program_error::ProgramError,
    pubkey::Pubkey,
    ProgramResult,
};
use pinocchio_pubkey::declare_id;

entrypoint!(process_instruction);

pub mod instructions;
pub use instructions::*;

declare_id!("YOUR_PROGRAM_PUBKEY");

fn process_instruction(
    _program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    match instruction_data.split_first() {
        Some((Deposit::DISCRIMINATOR, data)) => Deposit::try_from((data, accounts))?.process(),
        Some((Withdraw::DISCRIMINATOR, _)) => Withdraw::try_from(accounts)?.process(),
        _ => Err(ProgramError::InvalidInstructionData),
    }
}

生成程序 ID

Pinocchio 的declare_id!("PUBKEY")宏将你的程序的公钥添加到 crate 中。它扩展为一个名为 ID 的常量 Pubkey(可作为crate::ID访问)加上帮助程序,因此你可以在入口点中比较 program_id,派生 PDA,并保持代码和客户端中链上地址的一致性。

如何获取程序 ID 并使用它:

生成一个新的程序密钥对。

solana-keygen new -o target/deploy/vault-keypair.json

打印它的公钥(这是你的程序 ID)。

solana address -k target/deploy/vault-keypair.json

将其粘贴到lib.rs中的declare_id!宏中。

稍后在部署程序以及客户端/IDL 生成中,你也需要此密钥对。

创建 instructions.rs

接下来,我们将创建负责处理我们程序的逻辑的文件:

touch src/instructions.rs

instructions.rs定义了你的程序可以做什么以及如何做。它声明了每个指令的形状(名称、参数和预期帐户),包含这些指令的验证和业务逻辑,并提供当事务以你的程序为目标时lib.rs中的入口点分派的处理程序。

首先,我们将在instructions.rs的顶部添加use语句,以导入我们的程序依赖的 crate 和类型,以及我们将在指令处理程序中引用的任何实用程序。

// instructions.rs
use core::convert::TryFrom;
use core::mem::size_of;
use pinocchio::{
    account_info::AccountInfo,
    instruction::{Seed, Signer},
    program_error::ProgramError,
    pubkey::{find_program_address, Pubkey},
    sysvars::{rent::Rent, Sysvar},
    ProgramResult,
};
use pinocchio_log::log;
use pinocchio_system::instructions::{CreateAccount, Transfer as SystemTransfer};
use shank::ShankInstruction;

使用 Shank 生成 IDL

IDL(接口描述语言)文件被客户端程序和其他链上程序用于描述你的指令处理程序、它们的参数和帐户。

Anchor 会自动生成 IDL,因为它拥有描述你的程序的宏,但 Pinocchio 以其更小的关注点,则不会。

为了制作 IDL,我们将使用 Shank 性注释一个小的 Rust 枚举,并从你的程序代码中生成一个 IDL,我们稍后将在创建客户端代码时使用它。

让我们现在将 Shank 枚举和属性添加到我们的instructions.rs中。

// instructions.rs
/// Shank IDL facade enum describing all program instructions and their required accounts.
/// This is used only for IDL generation and does not affect runtime behavior.
// / Shank IDL facade 枚举,描述所有程序指令及其所需的帐户。
// / 这仅用于 IDL 生成,不会影响运行时行为。
#[derive(ShankInstruction)]
pub enum ProgramIx {
    /// Deposit lamports into the vault.
    // / 将 lamports 存入 vault。
    #[account(0, signer, writable, name = "owner", desc = "Vault owner and payer")]
    #[account(1, writable, name = "vault", desc = "Vault PDA for lamports")]
    #[account(2, name = "program", desc = "Program Address")]
    #[account(3, name = "system_program", desc = "System Program Address")]
    Deposit { amount: u64 },

    /// Withdraw all lamports from the vault back to the owner.
    // / 将所有 lamports 从 vault 中提取回所有者。
    #[account(0, signer, writable, name = "owner", desc = "Vault owner and authority")]
    #[account(1, writable, name = "vault", desc = "Vault PDA for lamports")]
    #[account(2, name = "program", desc = "Program Address")]
    Withdraw {},
}

我们将添加一些帮助程序函数来保持我们的代码干净且可重用。

// instructions.rs
/// Parse a u64 from instruction data.
// / 从指令数据中解析 u64。
fn parse_amount(data: &[u8]) -> Result<u64, ProgramError> {
    if data.len() != core::mem::size_of::<u64>() {
        return Err(ProgramError::InvalidInstructionData);
    }
    let amt = u64::from_le_bytes(data.try_into().unwrap());
    if amt == 0 {
        return Err(ProgramError::InvalidInstructionData);
    }
    Ok(amt)
}

/// Derive the vault PDA for an owner and return (pda, bump).
// / 为所有者派生 vault PDA 并返回 (pda, bump)。
fn derive_vault(owner: &AccountInfo) -> (Pubkey, u8) {
    find_program_address(&[b"vault", owner.key().as_ref()], &crate::ID)
}

/// Ensure the vault exists; if not, create it with PDA seeds.
// / 确保 vault 存在;如果不存在,则使用 PDA 种子创建它。
fn ensure_vault_exists(owner: &AccountInfo, vault: &AccountInfo) -> ProgramResult {
    if !owner.is_signer() {
        return Err(ProgramError::InvalidAccountOwner);
    }

    // Create when empty and fund rent-exempt.
    // / 在为空时创建并免除租金。
    if vault.lamports() == 0 {

        const ACCOUNT_DISCRIMINATOR_SIZE: usize = 8;

        let (_pda, bump) = derive_vault(owner);
        let signer_seeds = [\
            Seed::from(b"vault".as_slice()),\
            Seed::from(owner.key().as_ref()),\
            Seed::from(core::slice::from_ref(&bump)),\
        ];
        let signer = Signer::from(&signer_seeds);

        // Make the account rent-exempt.
        // / 使帐户免除租金。
        const VAULT_SIZE: usize = ACCOUNT_DISCRIMINATOR_SIZE + size_of::<u64>();
        let needed_lamports = Rent::get()?.minimum_balance(VAULT_SIZE);

        CreateAccount {
            from: owner,
            to: vault,
            lamports: needed_lamports,
            space: VAULT_SIZE as u64,
            owner: &crate::ID,
        }
        .invoke_signed(&[signer])?;

        log!("Vault created");

    } else {
        // If vault already exists, validate owner matches the program.
        // / 如果 vault 已经存在,则验证所有者是否与程序匹配。
        if !vault.is_owned_by(&crate::ID) {
            return Err(ProgramError::InvalidAccountOwner);
        }

        log!("Vault already exists");
    }

    Ok(())
}

使用 log! 宏

在 Pinocchio 程序中,你将使用log!而不是msg!

来自 solana-program 的熟悉的msg!宏在这里不可用,因为 Pinocchio 会剥离这些依赖项以保持轻量级和no_std

存款到 Vault

Deposit将 SOL 从所有者的钱包转移到程序的 vault PDA 中。

首次使用时,它会创建并出租 vault PDA。在确保 PDA 存在并且由程序拥有之后,它会将请求数量的 SOL 从所有者转移到 vault。

基本检查包括所有者必须是签名者、金额必须为非零、vault 必须可写,并且必须遵守创建的租金最小值。

// instructions.rs
pub struct Deposit<'a> {
    pub owner: &'a AccountInfo,
    pub vault: &'a AccountInfo,
    pub amount: u64,
}

impl<'a> Deposit<'a> {
    pub const DISCRIMINATOR: &'a u8 = &0;

    pub fn process(self) -> ProgramResult {
        let Deposit {
            owner,
            vault,
            amount,
        } = self;

        ensure_vault_exists(owner, vault)?;

        SystemTransfer {
            from: owner,
            to: vault,
            lamports: amount,
        }
        .invoke()?;
        log!("{} Lamports deposited to vault", amount);
        Ok(())
    }
}

impl<'a> TryFrom<(&'a [u8], &'a [AccountInfo])> for Deposit<'a> {
    type Error = ProgramError;

    fn try_from(value: (&'a [u8], &'a [AccountInfo])>) -> Result<Self, Self::Error> {
        let (data, accounts) = value;
        if accounts.len() < 2 {
            return Err(ProgramError::NotEnoughAccountKeys);
        }
        let owner = &accounts[0];
        let vault = &accounts[1];
        let amount = parse_amount(data)?;
        Ok(Self {
            owner,
            vault,
            amount,
        })
    }
}

从 Vault 提取 SOL

Withdraw将 SOL 从程序的 Vault PDA 转移回所有者。

基本检查包括 vault 归程序所有,与从所有者派生的 PDA 匹配,并且所有者是提取交易的签名者。提取的金额是高于租金最低限额的所有金额。

// instructions.rs
pub struct Withdraw<'a> {
    pub owner: &'a AccountInfo,
    pub vault: &'a AccountInfo,
}

impl<'a> Withdraw<'a> {
    pub const DISCRIMINATOR: &'a u8 = &1;

    /// Transfer lamports from the vault PDA to the owner, leaving the rent minimum in place.
    // / 将 lamports 从 vault PDA 转移到所有者,并将租金最低限额留在原位。
    pub fn process(self) -> ProgramResult {
        let Withdraw { owner, vault } = self;
        if !owner.is_signer() {
            return Err(ProgramError::InvalidAccountOwner);
        }

        // Validate that the vault is owned by the program
        // / 验证 vault 是否归程序所有
        if !vault.is_owned_by(&crate::ID) {
            return Err(ProgramError::InvalidAccountOwner);
        }

        // Validate that the provided vault account is the correct PDA for this owner
        // / 验证提供的 vault 帐户是否是此所有者的正确 PDA
        let (expected_vault_pda, _bump) = derive_vault(owner);
        if vault.key() != &expected_vault_pda {
            return Err(ProgramError::InvalidAccountData);
        }

        // Compute how much can be withdrawn while keeping the account rent-exempt
        // / 计算在保持帐户免除租金的同时可以提取多少
        let data_len = vault.data_len();
        let min_balance = Rent::get()?.minimum_balance(data_len);
        let current = vault.lamports();
        if current <= min_balance {
            // Nothing withdrawable; keep behavior strict to avoid rent violations
            // / 没有可提取的东西;保持行为严格,以避免违反租金
            return Err(ProgramError::InsufficientFunds);
        }
        let withdraw_amount = current - min_balance;

        // Transfer from vault to owner
        // / 从 vault 转移到所有者
        {
            let mut vault_lamports = vault.try_borrow_mut_lamports()?;
            *vault_lamports = vault_lamports
                .checked_sub(withdraw_amount)
                .ok_or(ProgramError::InsufficientFunds)?;
        }

        {
            let mut owner_lamports = owner.try_borrow_mut_lamports()?;
            *owner_lamports = owner_lamports
                .checked_add(withdraw_amount)
                .ok_or(ProgramError::InsufficientFunds)?;
        }

        log!("{} lamports withdrawn from vault", withdraw_amount);
        Ok(())
    }
}

impl<'a> TryFrom<&'a [AccountInfo]> for Withdraw<'a> {
    type Error = ProgramError;

    fn try_from(accounts: &'a [AccountInfo]) -> Result<Self, Self::Error> {
        if accounts.len() < 2 {
            return Err(ProgramError::NotEnoughAccountKeys);
        }
        let owner = &accounts[0];
        let vault = &accounts[1];
        Ok(Self { owner, vault })
    }
}

构建并部署程序(本地)

现在是构建和部署我们的程序的时候了!

在我们部署程序之前,打开一个单独的终端并运行solana-test-validator命令以启动你的本地 Solana 验证器。

我们还将指定我们之前创建的vault-keypair.json

cargo build-sbf
solana program deploy --program-id target/deploy/vault-keypair.json target/deploy/pinocchio_vault.so --url localhost

在你的程序成功部署后,你将看到一个程序 ID 和签名,如下所示(你的将不同):

Program Id: 4e8cmnRSw3p6Q8YarvEPrmfpEsAg19mqfAM2TC4hKxfi
Signature: BxvfsnVUSDcLXvpsVXpemX9VN2pUBnCVRQaqvbupUZ43PVJRTLdBfBqCTen3ZNxN9BBjKtuuUd5kBZsxNhfpFjU

创建客户端

对于客户端,我们将使用@solana/kit

即使我们将它作为测试运行,该代码也与你将在与我们的程序交互的前端中使用的代码类似。

在我们的测试中,我们将:

  • 将 SOL 空投到生成的密钥对
  • 派生与程序相同的 PDA
  • 使用 Codama 生成的指令库构建交易
  • 将交易发送到本地验证器

从我们项目的根目录中,创建一个客户端文件夹来存放我们的测试代码。

mkdir client
cd client
touch tests.ts

在我们开始编写我们的客户端代码之前,我们需要使用Shank创建我们程序的 IDL,并使用Codama生成一个客户端库,我们可以在我们的客户端代码中使用它。

使用 Shank 生成 IDL

你针对你的程序 crate 运行 Shank CLI,将其指向一个输出路径,并检查指令及其帐户是否与运行时期望的匹配。

如果你稍后更改你的指令布局,你需要重新生成 IDL,以便你的客户端保持同步。

在我们项目的根目录中,运行以下命令:

cargo install shank-cli
shank idl -o idl

-o参数用于 IDL 文件将生成的输出路径。在我们的例子中,我们只是将其添加到我们项目根目录中的一个 IDL 文件夹中。

我们项目中的idl文件夹现在包含pinocchio_vault.json,这是我们程序的 IDL。手动检查 IDL 是否按你预期的创建是一个好主意。

使用 Codama 生成客户端库

Codama 采用 Shank IDL 并发出一个 TypeScript 客户端。

生成的代码包括指令构建器、帐户类型和小的便利措施,使你的客户端代码专注于组成交易。

在我们项目的根目录中安装并初始化 Codama,接受默认值,并指向我们的idl/pinocchio_vault.json文件:

npm init
npm install codama
npx codama init
npx codama run js

你将在我们的项目中看到一个clients/js/src/generated/文件夹,其中包含我们的客户端代码用于将交易发送到我们的程序的程序类型。

创建测试脚本

首先,我们将添加我们的客户端代码将使用的所有包:

cd client
npm i @solana-program/system @solana/kit tsx typescript @types/node ws

将一个测试脚本添加到我们的package.json,它应该看起来像这样:

{
  "name": "client",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "build": "tsc",
    "test": "npx tsx ./tests.ts"
  },
  "dependencies": {
    "@solana-program/system": "^0.1.0",
    "@solana/kit": "^3.0.3"
  },
  "devDependencies": {
    "@types/node": "^24.7.2",
    "typescript": "^5.9.3",
    "tsx": "^4.19.2"
  }
}

现在我们可以将以下代码添加到我们的测试脚本中,以从程序中DepositWithdraw

// client/tests.ts
import { describe, it, before } from "node:test";
import assert from "node:assert";
import {
    airdropFactory,
    createSolanaRpc,
    createSolanaRpcSubscriptions,
    generateKeyPairSigner,
    lamports,
    sendAndConfirmTransactionFactory,
    pipe,
    createTransactionMessage,
    setTransactionMessageFeePayer,
    setTransactionMessageLifetimeUsingBlockhash,
    appendTransactionMessageInstruction,
    signTransactionMessageWithSigners,
    getSignatureFromTransaction,
    getProgramDerivedAddress,
    getAddressEncoder,
    getUtf8Encoder,
} from "@solana/kit";
import { SYSTEM_PROGRAM_ADDRESS } from "@solana-program/system";
import * as vault from "../clients/js/src/generated/index";

const LAMPORTS_PER_SOL = BigInt(1_000_000_000);

describe('Vault Program', () => {
    let rpc: any;
    let rpcSubscriptions: any;
    let signer: any;
    let vaultRent: BigInt;
    let vaultPDA: any;

    const ACCOUNT_DISCRIMINATOR_SIZE = 8; // same as Anchor/Rust
    const U64_SIZE = 8; // u64 is 8 bytes
    const VAULT_SIZE = ACCOUNT_DISCRIMINATOR_SIZE + U64_SIZE; // 16
    const DEPOSIT_AMOUNT = BigInt(100000000);

    before(async () => {
        // Establish connection to Solana cluster
        // / 建立与 Solana 集群的连接
        const httpProvider = 'http://127.0.0.1:8899';
        const wssProvider = 'ws://127.0.0.1:8900';
        rpc = createSolanaRpc(httpProvider);
        rpcSubscriptions = createSolanaRpcSubscriptions(wssProvider);

        // Generate signers
        // / 生成签名者
        signer = await generateKeyPairSigner();
        const signerAddress = await signer.address;

        // Airdrop SOL to signer
        // / 将 SOL 空投到签名者
        const airdrop = airdropFactory({ rpc, rpcSubscriptions });
        await airdrop({
            commitment: 'confirmed',
            lamports: lamports(LAMPORTS_PER_SOL),
            recipientAddress: signerAddress,
        });

        console.log(`Airdropped SOL to Signer: ${signerAddress}`);

        // get vault rent
        // / 获取 vault 租金
        vaultRent = await rpc
            .getMinimumBalanceForRentExemption(VAULT_SIZE)
            .send();

        // Get vault PDA
        // / 获取 vault PDA
        const seedSigner = getAddressEncoder().encode(await signer.address);
        const seedTag = getUtf8Encoder().encode("vault");
        vaultPDA = await getProgramDerivedAddress({
            programAddress: vault.PINOCCHIO_VAULT_PROGRAM_ADDRESS,
            seeds: [seedTag, seedSigner],
        });

        console.log(`Vault PDA: ${vaultPDA[0]}`);
    });

    it("can deposit to vault", async () => {

        // Create Deposit transaction using generated client
        // / 使用生成的客户端创建存款交易
        const depositIx = vault.getDepositInstruction(
            {
                owner: signer,
                vault: vaultPDA[0],
                program: vault.PINOCCHIO_VAULT_PROGRAM_ADDRESS,
                systemProgram: SYSTEM_PROGRAM_ADDRESS,
                amount: lamports(DEPOSIT_AMOUNT),
            },
            {
                programAddress: vault.PINOCCHIO_VAULT_PROGRAM_ADDRESS,
            }
        );

        const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
        const tx = await pipe(
            createTransactionMessage({ version: 0 }),
            tx => setTransactionMessageFeePayer(signer.address, tx),
            tx => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
            tx => appendTransactionMessageInstruction(depositIx, tx)
        );

        // Sign and send transaction
        // / 签署并发送交易
        const signedTransaction = await signTransactionMessageWithSigners(tx);
        const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions });

        await sendAndConfirmTransaction(signedTransaction, {
            commitment: 'confirmed',
        });

        const signature = getSignatureFromTransaction(signedTransaction);
        console.log('Transaction signature:', signature);

        const { value } = await rpc.getBalance(vaultPDA[0].toString()).send();
        assert.equal(DEPOSIT_AMOUNT, Number(value) - Number(vaultRent));

    });

    it("can withdraw from vault", async () => {

        const withdrawIx = vault.getWithdrawInstruction(
            {
                owner: signer,
                vault: vaultPDA[0],
                program: vault.PINOCCHIO_VAULT_PROGRAM_ADDRESS,
            },
        );

        const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
        const tx = await pipe(
            createTransactionMessage({ version: 0 }),
            tx => setTransactionMessageFeePayer(signer.address, tx),
            tx => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
            tx => appendTransactionMessageInstruction(withdrawIx, tx)
        );

        const signedTransaction = await signTransactionMessageWithSigners(tx);
        const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions });

        await sendAndConfirmTransaction(signedTransaction, {
            commitment: 'confirmed',
        });

        const signature = getSignatureFromTransaction(signedTransaction);
        console.log('Transaction signature:', signature);

        const { value } = await rpc.getBalance(vaultPDA[0].toString()).send();
        assert.equal(Number(vaultRent), value);
    });

    it("doesn't allow other users to withdraw from the vault", async () => {

        // signer that DOES NOT own the vault
        // / 不拥有 vault 的签名者
        const otherSigner = await generateKeyPairSigner();

        const withdrawIx = vault.getWithdrawInstruction(
            {
                owner: otherSigner,
                vault: vaultPDA[0],
                program: vault.PINOCCHIO_VAULT_PROGRAM_ADDRESS,
            },
        );

        const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
        const tx = await pipe(
            createTransactionMessage({ version: 0 }),
            tx => setTransactionMessageFeePayer(otherSigner.address, tx),
            tx => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
            tx => appendTransactionMessageInstruction(withdrawIx, tx)
        );

        const signedTransaction = await signTransactionMessageWithSigners(tx);
        const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions });

        await assert.rejects(
            sendAndConfirmTransaction(signedTransaction, {
                commitment: 'confirmed'
            }),
            {
                message: "Transaction simulation failed",
            }
        );
    });
});

运行测试

为了确保一切正常,我们运行测试:

npm test

预期输出

(值将与此处显示的不同)

Airdropped SOL to Signer: CU5...b7b
Vault PDA: Gct...N8o
Transaction signature: 3H2...g7X
    ✔ can deposit to vault (375ms)
Vault PDA: Gct...N8o
Transaction signature: 39V...5fo
    ✔ can withdraw from vault (460ms)
    ✔ doesn't allow other users to withdraw from the vault (2.310792ms)

总结

如果你的所有测试都通过了,那么恭喜你!如果没有,请继续调试。如果你仍然遇到代码问题,请随时在 Discord 上与我们联系 - 我们很乐意提供帮助。

了解如何通过你的程序读取和写入数据帐户是一个重要的概念,你将在你作为 Solana 开发者的道路上经常遇到它。

使用本指南,你:

  • 构建了一个 Pinocchio 程序,该程序允许用户存入 SOL 并在以后提取它,前提是他们是存入 SOL 的原始帐户。
  • 了解了 Pinocchio 与 solana-program 和 Anchor 的不同之处,并开发了一个具有手动验证和零拷贝数据访问的程序。
  • 使用 Shank 从你的 Rust 性生成 IDL。
  • 将该 IDL 转换为具有 Codama 的类型化客户端库。
  • 使用 Solana Kit 从一组行为类似于前端的测试中构建客户端。
  • 完成了在no_std环境中使用 Pinocchio 和手动帐户验证的工作心理模型。

资源

浏览本指南中提到的每个工具的官方 GitHub 存储库:

  • 原文链接: quicknode.com/guides/sol...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
QuickNode
QuickNode
江湖只有他的大名,没有他的介绍。