Anchor开发指南

参考文档:

https://www.hackquest.io/zh

https://foresightnews.pro/article/detail/53242

https://www.solana-cn.com/SolanaDocumention/core/cpl.html

https://www.anchor-lang.com/docs/cross-program-invocations

 

Anchor 简介

Anchor 是一个用于快速安全的构建 Solana 程序的框架。它通过减少诸如账户(反)序列化和指令数据等领域的样板文件、进行必要的安全检查、自动生成客户端库以及提供广泛的测试环境来简化开发流程。

Anchor 也为前端项目提供了一系列的库和工具,简化了跟链上程序交互的复杂度。它也对 PDA (程序衍生账户)、CPI(跨程序调用) 提供了一系列的支持。

总的来说,Anchor 使开发者能够更轻松地在 Solana 区块链上构建、部署和维护他们的去中心化应用。

 

安装

Anchor 框架仓库:https://github.com/coral-xyz/anchor

可以通过以下命令全局安装 Anchor

npm i -g @coral-xyz/anchor-cli

也可以用官方推荐的形式,先安装 Anchor 版本管理工具(Anchor Version Manager) avm,再通过avm安装anchor,具体的安装指南:https://www.anchor-lang.com/docs/installation

注:如果你先安装了 anchor,后面想补上 avm,那就要先安装和 anchor 版本一样的 avm才行,不然会可能报错

 

常用指令

1. 初始化新项目

使用 anchor init 命令可以快速创建一个全新的 Anchor 项目模板,包含必要的目录结构和基础文件,帮助开发者迅速上手。

anchor init my_project
  • my_project 是项目的名称,可以根据需要更改。
  • 此命令会生成一个包含以下目录的基础项目结构:
    • programs/:存放智能合约程序代码。
    • tests/:存放测试代码。
    • target/:存放编译后的输出文件。

 

2. 创建新程序

使用 anchor new 命令在现有项目中创建一个新的智能合约程序。每个程序都是独立的智能合约,具有独立的逻辑和状态。

anchor new my_program
  • my_program 是程序的名称,可以根据需求更改。
  • 该命令会在 programs/ 目录下创建一个新的程序目录,包含相关的 Rust 源代码和配置文件。

 

3. 编译程序

编译程序是将 Rust 源代码转换为 Solana 可执行的二进制文件。使用 anchor build 命令完成此操作。

anchor build [my_project]
  • 如果在项目根目录下,可以省略项目名称,直接执行 anchor build
  • 编译后的二进制文件会生成在 target/deploy/ 目录下,文件名为 <program_name>.so

常用选项:

  • --skip-lint: 跳过代码静态检查。
  • --features <features>: 指定编译特性。

 

4. 测试程序

在开发过程中,编写并运行测试是确保程序功能正确的重要步骤。使用 anchor test 命令可以执行项目中的所有测试套件。

anchor test
  • 该命令会自动构建程序、部署到本地 Solana 测试网络(通常是 localhost),并运行位于 tests/ 目录下的所有测试文件。
  • 测试结果会在终端中显示,帮助开发者快速定位和修复问题。

常用选项:

  • --skip-build: 跳过构建步骤,仅运行测试。
  • --bpf: 使用 BPF 测试,更接近实际部署环境。
  • --features <features>: 指定编译特性。

 

5. 部署程序

将智能合约部署到 Solana 区块链上,使其在区块链上可用。Anchor 提供了 anchor deploy 命令,用于将编译后的程序上传到指定的网络环境。

# 部署到开发测试网 (Devnet)
anchor deploy --env devnet

# 部署到主网 (Mainnet Beta)
anchor deploy --env mainnet-beta
  • --env 参数指定部署的目标网络环境,包括 localnetdevnettestnetmainnet-beta
  • 部署前需要确保已配置相应的 Solana 钱包并有足够的 SOL 余额。

配置部署环境:

  • 编辑项目根目录下的 Anchor.toml 文件,配置不同环境的 RPC URL 和程序 ID。

 

6. 验证部署

使用 anchor verify 命令可以验证链上部署的程序与本地编译的构件(artifact)是否匹配,确保部署的程序版本正确无误。

anchor verify
  • 需要在包含 Cargo.toml 文件的程序目录中运行此命令。
  • 验证过程包括:
    1. 检查程序 ID: 确保部署到链上的程序 ID 与本地配置一致,避免因 ID 不匹配导致的调用失败。
    2. 对比字节码: 比较链上程序的字节码与本地编译的二进制文件,确保二者匹配。

常用选项:

  • --url <url>: 指定 Solana 节点的 RPC URL。
  • --address <address>: 指定要验证的程序地址。

 

7. 升级程序

在需要更新智能合约逻辑时,可以使用 anchor upgrade 命令来升级已部署的程序。此命令集成了部署、初始化 IDL 和执行迁移脚本的步骤,确保升级过程顺利进行。

# 升级部署到开发测试网
anchor upgrade --env devnet

# 升级部署到主网
anchor upgrade --env mainnet-beta

anchor upgrade 会执行以下步骤:

  1. 部署最新版本的程序: 将最新编译的程序二进制文件上传到指定网络,并创建新的程序实例。
  2. 初始化接口定义(IDL): 将程序的接口定义上传至链上,供客户端交互使用。
  3. 执行迁移脚本: 运行所有必要的迁移逻辑,确保链上状态与新程序版本兼容。

 

8. 获取帮助信息

Anchor 提供了丰富的命令帮助信息,帮助开发者了解各个命令的用法和选项。

  • 列出所有可用命令:

    anchor help
  • 获取特定子命令的帮助信息:

    anchor help <subcommand>
    
    # 示例:获取 deploy 命令的帮助信息
    anchor help deploy

 

9. 其他常用命令

除了上述主要命令,Anchor 还提供了一些实用的命令,辅助开发和管理项目。

  1. 清理缓存

    anchor clean
    • 删除 target/ 目录下的所有构建产物,通常用于解决构建过程中出现的问题或释放磁盘空间。
  2. 生成 IDL

    anchor idl init <program_id>
    • 根据已部署的程序 ID 生成接口定义文件(IDL),供客户端交互使用。

 

目录结构

创建完项目 my_project 后,我们进入 my_project 文件夹, 可以看到 Anchor 自动生成的文件和目录:

my_project/
├── Anchor.toml
├── programs/
│   └── my_program/
│       ├── Cargo.toml
│       ├── src/
│       │   └── lib.rs
│       └── tests/
│           └── program_test.rs
├── target/
└── tests/
    └── integration_test.rs

这是一个简化的结构,提供了一个基本的框架,使你能够开始编写、测试和部署程序。在具体的项目中,你可能需要根据需求添加其他文件和目录,例如配置文件、文档等。以下是一些关键文件和目录的说明:

  • Anchor.toml:项目的配置文件,包含项目的基本信息、依赖关系和其他配置项。
  • programs目录:包含你的程序的目录。在这个例子中,有一个名为 my_program 的子目录。
    • Cargo.toml: 程序的Rust项目配置文件。
    • src目录: 包含实际的程序代码文件,通常是lib.rs,在实际的项目中我们会根据模块划分,拆的更细。
    • tests目录: 包含用于测试程序的测试代码文件。
  • target目录: 包含构建和编译生成的文件。
  • tests目录: 包含整合测试代码文件,用于测试整个项目的集成性能。

 

Anchor.toml

一个默认的 Anchor.toml 文件如下,更详细的配置可以查看文档:https://www.anchor-lang.com/docs/manifest

# Anchor项目配置文件

[toolchain]

[features]
# 是否启用分辨率功能
resolution = true
# 是否跳过代码检查
skip-lint = false

[programs.localnet]
# Solana程序ID(公钥)
solana_counter = "GaY3jNPukfZiHnPWiGHiEh6RTkjs4E7gprq6RCE9hp3d"

[registry]
# Anchor程序注册表的URL地址,本质上是一个程序包管理服务器
url = "https://api.apr.dev"

[provider]
# 使用的网络环境(Localnet/Devnet/Mainnet)
cluster = "Localnet"
# 钱包密钥对文件路径
wallet = "~/.config/solana/id.json"

[scripts]
# 测试脚本命令
# 使用ts-mocha运行TypeScript测试文件
# -p 指定TypeScript配置文件
# -t 设置测试超时时间(毫秒)
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

 

程序结构

在执行完 anchor init my_project 命令后,会自动生成 Anchor 示例项目,其中的lib.rs文件是 Anchor 框架的核心模块,包含了许多macros宏,这些宏为我们的程序生成 Rust 模板代码以及相应的安全校验,这也是Anchor框架便利的核心。

我们先来看生成的示例代码

// 引入 anchor 框架的预导入模块
use anchor_lang::prelude::*;

// 程序的链上地址
declare_id!("7b5HeSN9d5RhbPRdHfMY55zFteDPu75peeWgGxeNteHf");

// 指令处理逻辑
#[program]
pub mod anchor_counter {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        Ok(())
    }
}

// 指令涉及的账户集合
#[derive(Accounts)]
pub struct Initialize {}

 

代码详解

1、导入框架依赖

// 引入相关依赖
use anchor_lang::prelude::*;

这行代码引入了 Anchor 框架的预导入模块 (prelude),提供了开发 Solana 程序所需的基本功能和宏,例如:

  • 序列化与反序列化AnchorDeserializeAnchorSerialize,用于在链上处理数据。
  • 账户管理Accounts 宏,用于定义和管理程序需要的账户。
  • 上下文信息Context,提供当前指令执行时的上下文信息,包括涉及的账户、调用者等。

通过引入 prelude,我们可以使用 Anchor 提供的所有必要功能,而无需逐一导入每个模块。

 

2、声明程序地址

// 这里只是示意,实际的 program_id 会有所不同
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

declare_id! 宏用于声明程序的链上地址(Program ID)。当你首次构建 Anchor 程序时,框架会自动生成用于部署程序的默认密钥对,其中相应的公钥即为 declare_id! 宏所声明的程序ID(program_id)。

通常情况下,每次构建 Anchor 框架的 Solana 程序时,program_id 都会有所不同。但是通过declare_id!宏指定某个地址,我们就能保证程序升级后的地址不变。这种升级方式比起以太坊中智能合约的升级(使用代理模式),要简单很多。

 

3、定义指令处理逻辑

// 指令处理逻辑
#[program]
pub mod anchor_counter {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        Ok(())
    }
}

#[program] 宏用于标记包含所有指令处理逻辑的模块。在这个模块内,每个 pub 函数代表一个独立的指令(Instruction),可以被外部调用以执行特定的业务逻辑。

示例详解:

  • 模块名称anchor_counter,表示该模块用于计数器程序。
  • 函数 initialize:这是一个公共函数,代表一个初始化指令,用于设置或配置程序的初始状态。

函数签名说明:

  • ctx: Context<Initialize>:第一个参数是一个上下文对象,包含执行该指令所需的账户信息。Initialize 是一个自定义的账户集合结构体,定义了指令需要的具体账户。
  • 返回类型 Result<()>:表示指令执行的结果。Ok(()) 表示指令成功执行,Err 则表示执行失败并返回错误信息。

 

4、 定义账户集合

#[derive(Accounts)]
pub struct Initialize {}

#[derive(Accounts)] 宏用于定义指令执行时所需的账户集合,该宏实现了给定结构体 Initialize(反)序列化的 Trait 特征,因此在获取账户时不再需要手动迭代账户以及反序列化操作。通过这个结构体,开发者可以明确指定指令需要哪些账户,并为每个账户设置相应的访问权限和约束条件。

 

更复杂的示例

假设我们需要在初始化时创建一个新的账户并存储一些数据,InitializeAccounts 结构体可能如下所示:

#[derive(Accounts)]
pub struct InitializeAccounts<'info> {
    #[account(init, payer = user, space = 8 + 64)]
    pub my_account: Account<'info, MyAccount>,
    
    #[account(mut)]
    pub user: Signer<'info>,
    
    pub system_program: Program<'info, System>,
}

#[account]
pub struct MyAccount {
    pub data: u64,
}

字段说明:

  • my_account:该账户将在指令执行时被初始化。

    • init:表示初始化一个新账户。

    • payer = user:指定支付账户创建费用的账户。

    • space = 8 + 64:分配给账户的数据空间(字节数),这里包括 8 字节的前缀(常用于 Anchor 的账户标识)和 64 字节的数据存储。

    • pub my_account: Account<'info, MyAccount>:定义一个名为 my_account 的账户,存储 MyAccount 类型的数据。

  • user

    • #[account(mut)]:标记该账户在指令执行过程中可能会被修改。
    • pub user: Signer<'info>:指定这是一个签名者账户,必须是指令的调用者,并持有足够的权限。
  • system_program

    • pub system_program: Program<'info, System>:引用 Solana 的系统程序,用于处理账户创建和基础操作。

通过这种方式,Anchor 框架自动处理账户的验证和初始化,确保在执行指令时所有账户满足预期的条件,从而提高程序的安全性和可靠性。

 

#[program]宏

#[program] 宏是 Anchor 框架的核心部分之一,用于定义一个 Solana 程序模块。该模块包含了程序的指令(instructions)以及相关的业务逻辑。通过 #[program] 宏,开发者可以轻松地编写、组织和管理智能合约的功能,简化与 Solana SDK 的交互,并自动生成接口定义语言(IDL)。

 

函数签名要求

  • 必须是 pub 函数:所有指令处理函数必须声明为 pub,以便外部调用。
  • 第一个参数为 Context:包含当前指令执行所需的账户信息。
  • 其余参数为指令执行所需的数据:可以有多个参数,具体取决于指令的需求。

 

主要功能

1. 定义处理不同指令的函数

在程序模块中,开发者可以定义处理不同指令的函数。这些函数包含了具体的指令处理逻辑。这里我们在 #[program] 宏中实现了2个指令函数:initialize 和 increment,用来实现计数器的相关逻辑,使其更接近于实际的业务场景。

#[program]
pub mod anchor_counter {
    use super::*;

    /// 初始化账户,并以传入的 `instruction_data` 作为计数器的初始值
    pub fn initialize(ctx: Context<InitializeAccounts>, instruction_data: u64) -> Result<()> {
        ctx.accounts.counter.count = instruction_data;
        Ok(())
    }

    /// 在初始值的基础上实现累加 1 操作
    pub fn increment(ctx: Context<UpdateAccounts>) -> Result<()> {
        let counter = &mut ctx.accounts.counter;
        msg!("Previous counter: {}", counter.count);
        counter.count = counter.count.checked_add(1).ok_or(ErrorCode::Overflow)?;
        msg!("Counter incremented. Current count: {}", counter.count);
        Ok(())
    }
}

 

2. 简化与 Solana SDK 的交互

Anchor 框架通过 #[program] 宏,提供了一系列便捷的功能,使得与 Solana SDK 的交互更加简单和高效。例如:

  • 通过 ContextAccounts 宏,自动处理账户的验证、初始化和反序列化,开发者无需手动管理账户数据。
  • 自动执行程序 ID 声明、账户权限验证等安全措施,确保程序的可靠性和安全性。
  • 可以直接使用 declare_id、Account、Context、Sysvar 等结构和宏,而不必手动编写底层的 Solana 交互代码。

 

3. 自动生成接口定义语言(IDL)

#[program] 宏不仅仅用于定义指令处理函数,它还自动生成程序的接口定义语言(IDL)。IDL 描述了程序的接口,包括指令、数据结构等信息,便于客户端与程序进行交互。

IDL 与以太坊的 ABI 类似,两者都是用于描述智能合约接口的规范,但 Anchor 的 IDL 更加适应 Solana 的账户模型和程序架构。

 

Context

在 Anchor 框架中,Context 是一个核心结构体,用于封装与 Solana 程序执行相关的上下文信息。它包含了指令(instruction)的元数据以及执行逻辑所需的所有账户信息。通过 Context,开发者可以方便地访问程序ID、账户集合以及其他相关数据,从而简化智能合约的编写和管理。

结构体定义

// anchor_lang::context
pub struct Context<'a, 'b, 'c, 'info, T> {
    /// 当前程序的 ID
    pub program_id: &'a Pubkey,
    
    /// 反序列化后的账户集合
    pub accounts: &'b mut T,
    
    /// 不在 `accounts` 中的剩余账户集合
    pub remaining_accounts: &'c [AccountInfo<'info>],
    
    /// PDA (Program Derived Address) 的 bump 值映射
    /// 存储在 #[account(...)] 属性中使用 bump 约束的所有 PDA 的 bump 值
    /// T::Bumps 是由 Anchor 自动生成的结构体,包含所有 PDA 的 bump 值
    pub bumps: T::Bumps,
}

生命周期

Context 结构体包含多个生命周期参数,分别用于管理不同引用的生命周期,以确保内存安全,避免悬垂引用(dangling reference):

  • 'a (program_id): 引用 program_id 的生命周期,它通常与整个程序执行的生命周期相关。
  • 'b (accounts): 引用 accounts 的可变生命周期,用于操作和修改账户数据。
  • 'c (remaining_accounts): 引用 remaining_accounts 的生命周期,通常比 accounts 的生命周期短,因为剩余账户的使用可能仅限于特定逻辑块。
  • 'info: 关联 AccountInfo 数据的生命周期,设计为最长生命周期,确保账户信息在整个程序执行期间有效。

虽然Context 内部使用了多个生命周期参数,但在实际开发中,我们通常无需关注。这些生命周期参数主要用于框架内部的内存管理和优化,开发者仅需专注于业务逻辑即可。

 

使用方法

Context 使用泛型 T 来指定指令函数所需的具体账户集合。在实际应用中,开发者需要为每个指令定义一个特定的账户结构体,并将其作为 Context 的泛型参数。例如,Context<InitializeAccounts>Context<UpdateAccounts>。通过这种方式,指令函数可以轻松访问和操作所需的账户数据。

主要字段及其用途

  • ctx.program_id: 当前执行程序的 ID,类型为 Pubkey。用于标识和验证当前程序的唯一地址。

  • ctx.accounts: 账户集合,类型由泛型 T 指定。通过 #[derive(Accounts)] 宏自动生成并反序列化,开发者可以直接访问和操作这些账户。例如:

    #[derive(Accounts)]
    pub struct InitializeAccounts<'info> {
        #[account(init, payer = user, space = 8 + 32)]
        pub my_account: Account<'info, MyAccount>,
        #[account(mut)]
        pub user: Signer<'info>,
        pub system_program: Program<'info, System>,
    }
  • ctx.remaining_accounts: 剩余账户集合,类型为切片 &[AccountInfo<'info>]。这些账户没有在 #[derive(Accounts)] 中明确声明,提供了一种灵活的方式来处理动态或未知数量的账户。例如,在某些情况下,程序可能需要处理外部传入的多个账户,但在编写程序时无法预先确定其数量或具体类型。开发者需要手动处理这些账户的序列化和验证。

    for account in ctx.remaining_accounts.iter() {
        // 手动处理每个剩余账户
        let data = account.try_borrow_data()?;
        // 解析和验证数据
    }
  • ctx.bumps: 包含所有 PDA 的 bump 值映射。PDA(Program Derived Address)是通过特定算法生成的地址,通常用于关联特定账户。bump 是为了确保 PDA 的唯一性和安全性而引入的参数。通过 #[account(..., bump)] 属性,Anchor 自动生成 T::Bumps 结构体,存储所有相关的 bump 值,开发者可以直接在指令逻辑中使用这些值。

    pub fn initialize(ctx: Context<InitializeAccounts>) -> ProgramResult {
        let bump = ctx.bumps.my_account;
        // 使用 bump 进行 PDA 操作
    }

 

示例代码

下面是一个使用 Context 的实际示例,展示了如何定义账户结构体并在指令函数中使用 Context 访问和操作账户数据。

use anchor_lang::prelude::*;

declare_id!("GaY3jNPukfZiHnPWiGHiEh6RTkjs4E7gprq6RCE9hp3d");

// 指令处理逻辑
#[program]
mod anchor_counter {
    use super::*;
    pub fn initialize(ctx: Context<InitializeAccounts>, instruction_data: u64) -> Result<()> {
        ctx.accounts.pda_counter.count = instruction_data;
        Ok(())
    }

    pub fn increment(ctx: Context<UpdateAccounts>) -> Result<()> {
        let counter = &mut ctx.accounts.counter;
        msg!("Previous counter: {}", counter.count);
        counter.count = counter.count.checked_add(1).unwrap();
        msg!("Counter incremented. Current count: {}", counter.count);
        Ok(())
    }
}

// 指令涉及的账户集合
#[derive(Accounts)]
pub struct InitializeAccounts<'info> {
    #[account(init, seeds = [b"my_seed", user.key.to_bytes().as_ref()], bump, payer = user, space = 8 + 8)]
    pub pda_counter: Account<'info, Counter>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct UpdateAccounts<'info> {
    #[account(mut)]
    pub counter: Account<'info, Counter>,
    pub user: Signer<'info>,
}

// 自定义账户类型
#[account]
pub struct Counter {
    count: u64
}

在上述示例中:

  1. 定义账户结构体:
  • Counter 结构体通过 #[account] 宏定义了计数器账户的数据结构
  • InitializeAccountsUpdateAccounts 通过 #[derive(Accounts)] 宏定义了每个指令所需的账户集合
  • 使用 #[account()] 属性指定了账户的约束条件,如 init(初始化)、mut(可修改)等
  1. 指令函数:
  • initialize 函数接收初始值参数并设置计数器的初始状态
  • increment 函数实现计数增加逻辑,通过 ctx.accounts 访问计数器账户
  • 使用 msg! 宏记录计数器的变化过程
  1. 自动处理:
  • Anchor 自动处理 PDA 账户的创建和管理
  • 框架处理账户的序列化/反序列化
  • 自动验证账户的所有权和签名

 

Context<T> 泛型

在 Anchor 中,Context<T> 是一个泛型结构,其中 T 代表指令所需的账户集合。每个指令函数都会接收一个 Context<T> 参数,这使得 Anchor 可以在指令执行前自动验证所有必需的账户。

上面代码中的定义了两个账户结构

InitializeAccounts 结构

  1. pda_counter: PDA数据账户,用于存储计数器数据
    • init: 表示需要初始化这个账户
    • seeds: 定义了PDA的种子,包含字符串"my_seed"和用户公钥
    • payer: 指定由user支付账户创建费用
    • space: 分配8+8字节的存储空间
  2. user: 交易签名者账户,用于支付交易费用
  3. system_program: Solana系统程序账户,用于创建PDA账户

 

UpdateAccounts 结构

  1. counter: 要更新的计数器账户
    • mut: 表示这个账户需要可修改权限,因为我们要更新其中的数据
  2. user: 交易签名者账户,用于验证更新操作的权限

UpdateAccounts 相比 InitializeAccounts 更简单,因为它只需要访问已存在的账户进行更新,不需要创建新账户,所以不需要 system_program

 

Context<T> 的主要作用是:

  • 提供类型安全的账户访问
  • 自动进行账户验证
  • 确保所有必需的账户都被正确传入
  • 在指令执行前验证账户的权限和约束条件

 

指令参数

在 Anchor 框架中,指令函数的第一个参数ctx是必须的,而第二个参数是指令函数执行时传递的额外数据,是可选的,是否需要取决于指令的具体逻辑和需求。在initialize中,它被用于初始化计数器的初始值;而在increment中,该指令不需要额外的数据,所以只有ctx参数。

总的来说,Context 结构体的目的是为开发者提供方便的方式来访问与程序执行相关的信息。通过将这些信息组织在一个结构体中,可以更清晰地管理和访问上下文信息,而不必在函数参数中传递大量的单独参数。

 

Account

在 Solana 上,账户是存储数据的基本单位。每个账户有一个唯一的地址(Pubkey),并存储数据和程序所需的元数据(如余额、所有者等)。账户的数据采用字节序列的形式存储,程序通过读取和写入这些字节来操作数据。Anchor 框架进一步抽象了 Solana 的账户管理,使其更加易于使用和安全。Anchor 提供了多种账户类型,以适应不同的需求。

 

常见账户类型

Account<T>

  • 描述: 用于存储单一类型数据的账户。

  • 用途: 适用于存储结构化的数据,如用户信息、配置信息等。

  • 示例

    #[account]
    pub struct MyAccount {
        pub data: u64,
        pub owner: Pubkey,
    }

Signer

  • 描述: 表示一个必须签署交易的账户。

  • 用途: 用于验证用户身份和权限,如支付账户、权限控制等。

  • 示例

    #[derive(Accounts)]
    pub struct MyContext<'info> {
        #[account(mut)]
        pub user: Signer<'info>,
        // 其他账户...
    }

Program

  • 描述: 表示一个程序账户。

  • 用途: 用于引用系统程序或其他依赖的程序。

  • 示例

    #[derive(Accounts)]
    pub struct MyContext<'info> {
        pub system_program: Program<'info, System>,
        // 其他账户...
    }

 

#[account]

Anchor 利用 Rust 宏提供了简洁的方式来定义账户结构,它用于处理账户的**(反)序列化**、账户识别器、所有权验证。这个宏大大简化了程序的开发过程,使开发者可以更专注于业务逻辑而不是底层的账户处理。它主要实现了以下几个 Trait 特征:

(反)序列化:Anchor 使用 Borsh 序列化协议自动处理账户数据的序列化与反序列化,确保数据在链上和链下的一致性。框架会自动为使用 #[account] 标记的结构体实现序列化和反序列化。这是因为 Solana 账户需要将数据序列化为字节数组以便在网络上传输,同时在接收方需要将这些字节数组反序列化为合适的数据结构进行处理。

Discriminator(账户识别器):它是帐户类型的 8 字节唯一标识符,源自帐户类型名称 SHA256 哈希值的前 8 个字节。在实现帐户序列化特征时,前 8 个字节被保留用于帐户鉴别器。因此,在对数据反序列化时,就会验证传入账户的前8个字节,如果跟定义的不匹配,则是无效账户,账户反序列化失败。

Owner(所有权校验):使用 #[account] 标记的结构体所对应的 Solana 账户的所有权属于程序本身,也就是在程序的**declare_id!**宏中声明的程序ID。上面代码中MyAccount账户的所有权为程序本身。

总的来说,#[account] 宏语法简单,但 Anchor 框架却在底层为我们实现了许多功能,提高了开发效率。

 

#[derive(Accounts)]

该宏应用于指令所要求的账户列表,实现了给定 struct 结构体数据的反序列化功能,因此在获取账户时不再需要手动迭代账户以及反序列化操作,并且实现了账户满足程序安全运行所需要的安全检查,当然,需要#[account]宏配合使用。

1、下面我们看下示例中的InitializeAccounts结构体,当initialize指令函数被调用时,程序会做如下2个校验:

#[derive(Accounts)]
pub struct InitializeAccounts<'info> {
    #[account(
        init,                                  // 表示要创建新账户
        seeds = [b"my_seed", user.key().as_ref()],  // 如果是 PDA 则必需
        bump,                                  // 如果是 PDA 则必需
        payer = user,                          // 必需,指定支付账户创建费用
        space = 8 + 8,                         // 必需,指定账户空间大小
    )]
    pub pda_counter: Account<'info, Counter>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}
  • 账户类型校验:传入的账户是否跟 InitializeAccounts 定义的账户类型相匹配,例如Account、Singer、Program等类型。
  • 账户权限校验:根据账户标注的权限,框架会进行相应的权限校验,例如检查是否有足够的签名权限、是否可以修改等。

如果其中任何一个校验失败,将导致指令执行失败并产生错误。

 

2、InitializeAccounts结构体中有如下3种账户类型:

2.1、 Account类型:它是AccountInfo类型的包装器,可用于验证账户所有权并将底层数据反序列化为Rust类型。对于结构体中的counter账户,Anchor 会实现如下功能:

pub pda_counter: Account<'info, Counter>,

① 该账户类型的 Counter 数据自动实现反序列化。

② 检查传入的所有者是否跟 Counter 的所有者匹配。

2.2、Signer类型:这个类型会检查给定的账户是否签署了交易,但并不做所有权的检查。只有在并不需要底层数据的情况下,才应该使用Signer类型。

pub user: Signer<'info>,

2.3、Program类型:验证这个账户是个特定的程序。对于system_program 字段,Program 类型用于指定程序应该为系统程序,Anchor 会替我们完成校验。

pub system_program: Program<'info, System>,

 

账户属性约束

在定义账户时,可以通过附加的属性和约束条件来指定账户的行为和要求。这些约束条件通过 #[derive(Accounts)] 宏和字段属性来实现。下面是一些常见用法,Anchor 为我们提供了许多这样的属性约束,可以看 这里

**1. 初始化一个派生账户地址 PDA **:它是根据seeds、program_id以及bump动态计算而来的,其中的bump是程序在计算地址时自动生成的一个值(Anchor 默认使用符合条件的第一个 bump 值),不需要我们手动指定。

#[account(
	init, 
	seeds = [b"my_seed"], 
	bump,
	payer = user, 
	space = 8 + 8
)]
pub pda_counter: Account<'info, Counter>,
pub user: Signer<'info>,

init:Anchor 会通过相关属性配置初始化一个派生帐户地址 PDA 。

seeds:种子(seeds)是一个任意长度的字节数组,通常包含了派生账户地址 PDA 所需的信息,在这个例子中我们仅使用了字符串my_seed作为种子。当然,也可以包含其他信息:如指令账户集合中的其他字段user、指令函数中的参数instruction_data,示意代码如下:

#[derive(Accounts)]
#[instruction(instruction_data: String)]
pub struct InitializeAccounts<'info> {
		#[account(
			init, 
			seeds = [b"my_seed", 
							 user.key.to_bytes().as_ref(),
							 instruction_data.as_ref()
							]
			bump,
			payer = user, 
			space = 8 + 8
		)]
		pub pda_counter: Account<'info, Counter>,
		pub user: Signer<'info>,
}

payer:指定了支付账户,即进行账户初始化时,使用user这个账户支付交易费用。

space:指定账户的空间大小为16个字节,前 8 个字节存储 Anchor 自动添加的鉴别器,用于识别帐户类型。接下来的 8 个字节为存储在Counter帐户类型中的数据分配空间(count为 u64 类型,占用 8 字节)。

 

**2.验证派生账户地址 PDA **:有些时候我们需要在调用指令函数时,验证传入的 PDA 地址是否正确,也可以采用类似的方式,只需要传入对应的seeds和bump即可,Anchor就会按照此规则并结合program_id来计算 PDA 地址,完成验证工作。注意:这里不需要init属性。

#[derive(Accounts)]
#[instruction(instruction_data: String)]
pub struct InitializeAccounts<'info> {
		#[account(
			seeds = [b"my_seed", 
							 user.key.to_bytes().as_ref(),
							 instruction_data.as_ref()
							]
			bump
		)]
		pub pda_counter: Account<'info, Counter>,
		pub user: Signer<'info>,
}

 

3、#[account(mut)] 属性约束

mut:表示这是一个可变账户,即在程序的执行过程中,这个账户的数据(包括余额)可能会发生变化。在Solana 程序中,对账户进行写操作需要将其标记为可变。

 

总的来说,#[account(..)] 宏在 Solana 的 Anchor 框架中用于声明性地配置账户属性。通过宏中的参数,开发者可以指定账户是否可初始化、是否可变、是否需要签名、支付者、存储空间大小等,更重要的是,通过seeds属性,可以方便地生成程序派生账户(PDA),将种子信息与程序 ID 结合动态计算账户地址。这使得代码更加清晰、易读,并帮助开发者遵循 Solana 的账户规范,提高了程序的可维护性和可读性。

 

CPI

Cross Program Invocations (CPI) 也叫跨程序调用,是指一个程序调用另一个程序的指令,将指令视为程序向网络公开的 API 端点,将 CPI 视为一个 API 在内部调用另一个 API

image-20241210220732524

当一个程序向另一个程序发起跨程序调用(CPI)时:

  • 初始交易中调用程序A的签署者权限会被延申给程序B。
  • 被调用的程序B也可以进一步对其他程序进行CPI,深度最多为4(例如: B->C,C->D)。
  • 这些程序可以代表源自其程序ID的程序PDAs进行“签名”

 

每个CPI指令都必须指定以下信息:

  • 程序地址:指定被调用的程序
  • 账户:列出指令要读取或写入的每个账户,包括其他程序
  • 指令数据:指定要调用的程序上的哪个指令,以及该指令需要的任何的额外的数据(函数参数)

原生调用

invoke 函数用于创建不需要PDA签署者的CPI。当创建CPI时,提供给调用程序的签署者权限会自动的扩展到被调用程序。

	pub fn invoke(
		instruction: &Instruction,
		account_infos: &[AccountInfo<'_>]
	) -> Result<(), ProgramError>

这是Solana Playground上的一个示例程序,它使用invoke函数进行CPI,以调用系统程序上的转账指令。你也可以参考基本的CPI指南以获取更多的详细信息。

 

有PDA签署者的CPI

invoke_signed 是 Solana 提供的一个函数,用于在 CPI 调用中使用 PDA 作为签署者。它允许程序在没有私钥的情况下,为特定账户添加额外的签名权,以授权 CPI 操作。

pub fn invoke_signed(
    instruction: &Instruction,
    account_infos: &[AccountInfo<'_>],
    signers_seeds: &[&[&[u8]]]
) -> Result<(), ProgramError>
  • 当程序需要以PDA的身份签署交易时,使用 invoke_signed
  • 系统会通过 create_program_address 和提供的 seeds 重新生成PDA
  • 验证生成的地址确实是一个有效的PDA
  • 如果验证通过,该PDA将被用作交易的签署者

 

Anchor 调用示例

下面是源自官网的一个 cpi 调用示例:https://www.anchor-lang.com/docs/cross-program-invocations

首先新建一个项目

anchor init puppet

拷贝以下代码

use anchor_lang::prelude::*;

declare_id!("FVPc3sQPeWn7BjBdQbZEoF8CSXQVvujcZb4qFZPZgXPp");

#[program]
pub mod puppet {
    use super::*;
    pub fn initialize(_ctx: Context<Initialize>) -> Result<()> {
        Ok(())
    }

    pub fn set_data(ctx: Context<SetData>, data: u64) -> Result<()> {
        let puppet = &mut ctx.accounts.puppet;
        puppet.data = data;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = user, space = 8 + 8)]
    pub puppet: Account<'info, Data>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct SetData<'info> {
    #[account(mut)]
    pub puppet: Account<'info, Data>,
}

#[account]
pub struct Data {
    pub data: u64,
}

然后在这个项目内再新建一个 program

anchor new puppet-master

在这个子项目的 cargo.toml 中做如下配置

[dependencies]
puppet = { path = "../puppet", features = ["cpi"] }

拷贝以下代码

use anchor_lang::prelude::*;
use puppet::cpi::accounts::SetData;
use puppet::program::Puppet;
use puppet::{self, Data};

declare_id!("H4b64fyuo8hi1e8wxUDQgT2YozQVdpiA7XdeWpLo2h2p");

#[program]
mod puppet_master {
    use super::*;
    pub fn pull_strings(ctx: Context<PullStrings>, data: u64) -> Result<()> {
        // 获取puppet程序的account_info
        let cpi_program = ctx.accounts.puppet_program.to_account_info();
        
        // 构建CPI调用需要的账户集合
        let cpi_accounts = SetData {
            puppet: ctx.accounts.puppet.to_account_info(),
        };
        
        // 创建CPI上下文,包含程序和账户信息
        let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
        
        // 执行CPI调用,调用puppet程序的set_data指令
        puppet::cpi::set_data(cpi_ctx, data)
    }
}

#[derive(Accounts)]
pub struct PullStrings<'info> {
    #[account(mut)]
    pub puppet: Account<'info, Data>,
    pub puppet_program: Program<'info, Puppet>,
}

 

此时项目的 anchor.toml 应该是

[toolchain]

[features]
resolution = true
skip-lint = false

[programs.localnet]
puppet = "FVPc3sQPeWn7BjBdQbZEoF8CSXQVvujcZb4qFZPZgXPp"
puppet-master = "H4b64fyuo8hi1e8wxUDQgT2YozQVdpiA7XdeWpLo2h2p"

[registry]
url = "https://api.apr.dev"

[provider]
cluster = "Localnet"
wallet = "~/.config/solana/id.json"

[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

 

然后我们写一个测试脚本

import * as anchor from '@coral-xyz/anchor'
import { Program } from '@coral-xyz/anchor'
import { Keypair, PublicKey  } from '@solana/web3.js'
import { expect } from 'chai'
import { Puppet } from '../target/types/puppet'
import { PuppetMaster } from '../target/types/puppet_master'

&nbsp;
describe('puppet', () => {
  const provider = anchor.AnchorProvider.env()
  anchor.setProvider(provider)
    
  const puppetProgram = anchor.workspace.Puppet as Program<Puppet>
  const puppetMasterProgram = anchor.workspace
      .PuppetMaster as Program<PuppetMaster>
    
  const puppetKeypair = Keypair.generate()

  it('Does CPI!', async () => {
    await puppetProgram.methods
        .initialize()
        .accounts({
          puppet: puppetKeypair.publicKey,
          user: provider.wallet.publicKey,
        })
        .signers([puppetKeypair])
        .rpc()

    await puppetMasterProgram.methods
        .pullStrings(new anchor.BN(42))
        .accounts({
          puppet: puppetKeypair.publicKey,
        })
        .rpc()
      
    expect(
        (
            await puppetProgram.account.data.fetch(puppetKeypair.publicKey)
        ).data.toNumber()
    ).to.equal(42)
  })
})

 

运行测试命令

anchor test --skip-build

作者:加密鲸拓

版权:此文章版权归 加密鲸拓 所有,如有转载,请注明出处!