使用 Anchor 框架入门

Anchor 框架 使用 Rust 宏 来减少样板代码并简 化编写 Solana 程序所需的常见安全检查的实现。

可以将 Anchor 看作是 Solana 程序的框架,就像 Next.js 是用于 web 开发的框架一样。 正如 Next.js 允许开发者使用 React 创建网站,而不是仅依赖 HTML 和 TypeScript,Anchor 提供了一套工具和抽象,使构建 Solana 程序更加直观和安全。

Anchor 程序中主要的宏包括:

  • declare_id: 指定程序的链上地址
  • #[program]: 指定包含程序指令逻辑的模块
  • #[derive(Accounts)]: 应用于结构体以指示指令所需的 账户列表
  • #[account]: 应用于结构体以创建特定于程序的自定义账户类型

Anchor 程序 #

下面是一个包含单个指令的简单 Anchor 程序,该指令创建一个新账户。 我们将逐步解释 Anchor 程序的基本结构。 以下是在 Solana Playground 上的程序。

lib.rs
use anchor_lang::prelude::*;
 
declare_id!("11111111111111111111111111111111");
 
#[program]
mod hello_anchor {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> {
        ctx.accounts.new_account.data = data;
        msg!("Changed data to: {}!", data);
        Ok(())
    }
}
 
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = signer, space = 8 + 8)]
    pub new_account: Account<'info, NewAccount>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
}
 
#[account]
pub struct NewAccount {
    data: u64,
}

declare_id 宏 #

declare_id 宏用于指定程序的链上地址(程序 ID)。

lib.rs
use anchor_lang::prelude::*;
 
declare_id!("11111111111111111111111111111111");

当你第一次构建 Anchor 程序时,框架会生成一个新的密钥对用于部署程序(除非另有指 定)。 这个密钥对的公钥应作为declare_id宏中的程序 ID 使用。

  • 使用 Solana Playground 时,程序 ID 会自动为你更新, 并可以通过 UI 导出。
  • 本地构建时,程序密钥对可以在/target/deploy/your_program_name.json中找到。

program 宏 #

#[program] 宏指定包含所有程序指令的模块。 模块中的每个公共函数代表程序的一个单独指令。

在每个函数中,第一个参数始终是Context类型。 后续参数是可选的,定义指令所需的任 何额外data

lib.rs
use anchor_lang::prelude::*;
 
declare_id!("11111111111111111111111111111111");
 
#[program]
mod hello_anchor {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> {
        ctx.accounts.new_account.data = data;
        msg!("Changed data to: {}!", data);
        Ok(())
    }
}
 
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = signer, space = 8 + 8)]
    pub new_account: Account<'info, NewAccount>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
}
 
#[account]
pub struct NewAccount {
    data: u64,
}

Context 类型为指令提供以下非参数输入:

pub struct Context<'a, 'b, 'c, 'info, T> {
    /// Currently executing program id.
    pub program_id: &'a Pubkey,
    /// Deserialized accounts.
    pub accounts: &'b mut T,
    /// Remaining accounts given but not deserialized or validated.
    /// Be very careful when using this directly.
    pub remaining_accounts: &'c [AccountInfo<'info>],
    /// Bump seeds found during constraint validation. This is provided as a
    /// convenience so that handlers don't have to recalculate bump seeds or
    /// pass them in as arguments.
    pub bumps: BTreeMap<String, u8>,
}

Context是一个泛型类型,其中T代表指令所需的账户集。 在定义指令 的Context时,T类型是实现Accounts特性的结构体(Context<Initialize>)。

这个上下文参数允许指令访问:

  • ctx.accounts: 指令的账户
  • ctx.program_id: 程序本身的地址
  • ctx.remaining_accounts: 提供给指令但未在Accounts结构体中指定的所有剩余账户
  • ctx.bumps: 任何程序派生地址 (PDA) 账户在Accounts结构 体中指定的 bump seeds

derive(Accounts) 宏 #

#[derive(Accounts)] 宏应用于结构体并实现 Accounts 特性。 这用于指定和验证特定指令所需的一组账户。

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

结构体中的每个字段代表指令所需的一个账户。 每个字段的命名是任意的,但建议使用描 述性名称以指示账户的用途。

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

在构建 Solana 程序时,验证客户端提供的账户是至关重要的。 这种验证通过 Anchor 中 的账户约束和指定适当的账户类型来实现:

  • 账户约束: 约束定义了账户必须满足的附加条件,以被视为指令的有效账户。 约束通 过#[account(..)]属性应用,该属性放置在Accounts结构体中的账户字段上方。

    #[derive(Accounts)]
    pub struct Initialize<'info> {
        #[account(init, payer = signer, space = 8 + 8)]
        pub new_account: Account<'info, NewAccount>,
        #[account(mut)]
        pub signer: Signer<'info>,
        pub system_program: Program<'info, System>,
    }
  • 账户类型: Anchor 提供了各种账户类型,以帮助确保客户端提供的账户与程序预期的账户匹配。

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

Accounts结构体中的账户可以通过Context在指令中访问,使用ctx.accounts语法。

lib.rs
use anchor_lang::prelude::*;
 
declare_id!("11111111111111111111111111111111");
 
#[program]
mod hello_anchor {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> {
        ctx.accounts.new_account.data = data;
        msg!("Changed data to: {}!", data);
        Ok(())
    }
}
 
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = signer, space = 8 + 8)]
    pub new_account: Account<'info, NewAccount>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
}
 
#[account]
pub struct NewAccount {
    data: u64,
}

当 Anchor 程序中的指令被调用时,程序会根据Accounts结构体中指定的内容执行以下检 查:

  • 账户类型验证:验证传入指令的账户是否与指令上下文中定义的账户类型相对应。

  • 约束检查:根据任何附加约束检查账户。

这有助于确保从客户端传递给指令的账户是有效的。 如果任何检查失败,则指令在到达指 令处理函数的主要逻辑之前会因错误而失败。

有关更详细的示例,请参阅 Anchor 文档中 的约束账户类型部 分。

account 宏 #

#[account] 宏应用于结构体以定义程序的自定义数据账户类型的格式。 结构体中的每个字段代表将存 储在账户数据中的一个字段。

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

这个宏实现了各种特性 详见此处#[account]宏的关键功能包括:

  • 分配所有权: 创建账户时,账户的所有权会自动分配给declare_id中指定的程序。
  • 设置鉴别器: 在初始化期间,特定于账户类型的唯一 8 字节鉴别器会作为账户数据的前 8 字节添加。 这有助于区分账户类型和账户验证。
  • 数据序列化和反序列化: 与账户类型对应的账户数据会自动序列化和反序列化。
lib.rs
use anchor_lang::prelude::*;
 
declare_id!("11111111111111111111111111111111");
 
#[program]
mod hello_anchor {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> {
        ctx.accounts.new_account.data = data;
        msg!("Changed data to: {}!", data);
        Ok(())
    }
}
 
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = signer, space = 8 + 8)]
    pub new_account: Account<'info, NewAccount>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
}
 
#[account]
pub struct NewAccount {
    data: u64,
}

在 Anchor 中,账户鉴别器是一个 8 字节的标识符,每种账户类型都是唯一的。 这个标识 符是从账户类型名称的 SHA256 哈希值的前 8 个字节派生的。 账户数据的前 8 个字节专 门保留给这个鉴别器。

#[account(init, payer = signer, space = 8 + 8)]
pub new_account: Account<'info, NewAccount>,

鉴别器在以下两种情况下使用:

  • 初始化:在账户初始化期间,鉴别器会设置为账户类型的鉴别器。
  • 反序列化:当账户数据被反序列化时,数据中的鉴别器会与账户类型的预期鉴别器进行检 查。

如果存在不匹配,这表明客户端提供了一个意外的账户。 这个机制在 Anchor 程序中作为 账户验证检查,确保使用正确和预期的账户。

IDL 文件 #

当一个 Anchor 程序被构建时,Anchor 会生成一个接口描述语言(IDL)文件,表示程序的 结构。 这个 IDL 文件提供了一种标准化的基于 JSON 的格式,用于构建程序指令和获取程 序账户。

以下是 IDL 文件与程序代码的关系示例。

指令 #

IDL 中的instructions数组对应于程序中的指令,并指定每个指令所需的账户和参数。

IDL.json
{
  "version": "0.1.0",
  "name": "hello_anchor",
  "instructions": [
    {
      "name": "initialize",
      "accounts": [
        { "name": "newAccount", "isMut": true, "isSigner": true },
        { "name": "signer", "isMut": true, "isSigner": true },
        { "name": "systemProgram", "isMut": false, "isSigner": false }
      ],
      "args": [{ "name": "data", "type": "u64" }]
    }
  ],
  "accounts": [
    {
      "name": "NewAccount",
      "type": {
        "kind": "struct",
        "fields": [{ "name": "data", "type": "u64" }]
      }
    }
  ]
}
lib.rs
use anchor_lang::prelude::*;
 
declare_id!("11111111111111111111111111111111");
 
#[program]
mod hello_anchor {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> {
        ctx.accounts.new_account.data = data;
        msg!("Changed data to: {}!", data);
        Ok(())
    }
}
 
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = signer, space = 8 + 8)]
    pub new_account: Account<'info, NewAccount>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
}
 
#[account]
pub struct NewAccount {
    data: u64,
}

账户 #

IDL 中的accounts数组对应于程序中用#[account]宏注释的结构体,这些结构体指定了 程序数据账户的结构。

IDL.json
{
  "version": "0.1.0",
  "name": "hello_anchor",
  "instructions": [
    {
      "name": "initialize",
      "accounts": [
        { "name": "newAccount", "isMut": true, "isSigner": true },
        { "name": "signer", "isMut": true, "isSigner": true },
        { "name": "systemProgram", "isMut": false, "isSigner": false }
      ],
      "args": [{ "name": "data", "type": "u64" }]
    }
  ],
  "accounts": [
    {
      "name": "NewAccount",
      "type": {
        "kind": "struct",
        "fields": [{ "name": "data", "type": "u64" }]
      }
    }
  ]
}
lib.rs
use anchor_lang::prelude::*;
 
declare_id!("11111111111111111111111111111111");
 
#[program]
mod hello_anchor {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> {
        ctx.accounts.new_account.data = data;
        msg!("Changed data to: {}!", data);
        Ok(())
    }
}
 
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = signer, space = 8 + 8)]
    pub new_account: Account<'info, NewAccount>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
}
 
#[account]
pub struct NewAccount {
    data: u64,
}
 

客户端 #

Anchor 提供了一个 Typescript 客户端库 (@coral-xyz/anchor) 简化了客户端与 Solana 程序交互的过程。

要使用客户端库,首先需要使用 Anchor 生成的 IDL 文件设置一个 Program 实例。

客户端程序 #

创建Program实例需要程序的 IDL、其链上地址(programId)和一个 AnchorProviderAnchorProvider结合了两件事:

  • Connection - 与 Solana 集群的连接(即 localhost、devnet、mainnet)
  • Wallet - (可选)用于支付和签署交易的默认钱包

在本地构建 Anchor 程序时,创建Program实例的设置会在测试文件中自动完成。 IDL 文 件可以在/target文件夹中找到。

import * as anchor from "@coral-xyz/anchor";
import { Program, BN } from "@coral-xyz/anchor";
import { HelloAnchor } from "../target/types/hello_anchor";
 
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.HelloAnchor as Program<HelloAnchor>;

当使用 wallet adapter 与前端集成时,需要手动设置AnchorProviderProgram

import { Program, Idl, AnchorProvider, setProvider } from "@coral-xyz/anchor";
import { useAnchorWallet, useConnection } from "@solana/wallet-adapter-react";
import { IDL, HelloAnchor } from "./idl";
 
const { connection } = useConnection();
const wallet = useAnchorWallet();
 
const provider = new AnchorProvider(connection, wallet, {});
setProvider(provider);
 
const programId = new PublicKey("...");
const program = new Program<HelloAnchor>(IDL, programId);

这意味着如果没有默认的Wallet,但允许在连接钱包之前使用Program获取账户。 这意 味着如果没有默认的Wallet,但允许在连接钱包之前使用Program获取账户。

import { Program } from "@coral-xyz/anchor";
import { clusterApiUrl, Connection, PublicKey } from "@solana/web3.js";
import { IDL, HelloAnchor } from "./idl";
 
const programId = new PublicKey("...");
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
 
const program = new Program<HelloAnchor>(IDL, programId, {
  connection,
});

调用指令 #

一旦Program设置完成,可以使用 AnchorMethodsBuilder 构建一个指令、一个交易,或构建并发送一个交易。基本格式如下: 基本格式如下:

  • program.methods - 这是用于创建与程序 IDL 相关的指令调用的构建器 API
  • .instructionName - 程序 IDL 中的特定指令,传入任何指令数据作为逗号分隔的值
  • .accounts - 传入 IDL 指定的指令所需的每个账户的地址
  • .signers - 可选地传入一个密钥对数组,作为指令所需的额外签名者
await program.methods
  .instructionName(instructionData1, instructionData2)
  .accounts({})
  .signers([])
  .rpc();

以下是使用方法构建器调用指令的示例。

rpc() #

rpc()方 法发送一个签名的交易带 有指定的指令并返回一个TransactionSignature。 使用.rpc时,Provider中 的Wallet会自动作为签名者包含在内。

// Generate keypair for the new account
const newAccountKp = new Keypair();
 
const data = new BN(42);
const transactionSignature = await program.methods
  .initialize(data)
  .accounts({
    newAccount: newAccountKp.publicKey,
    signer: wallet.publicKey,
    systemProgram: SystemProgram.programId,
  })
  .signers([newAccountKp])
  .rpc();

transaction() #

transaction()方 法构建一个Transaction并 将指定的指令添加到交易中(不自动发送)。

// Generate keypair for the new account
const newAccountKp = new Keypair();
 
const data = new BN(42);
const transaction = await program.methods
  .initialize(data)
  .accounts({
    newAccount: newAccountKp.publicKey,
    signer: wallet.publicKey,
    systemProgram: SystemProgram.programId,
  })
  .transaction();
 
const transactionSignature = await connection.sendTransaction(transaction, [
  wallet.payer,
  newAccountKp,
]);

instruction() #

instruction()方 法构建一个TransactionInstruction使 用指定的指令。 如果你想手动将指令添加到交易中并与其他指令组合,这是很有用的。

// Generate keypair for the new account
const newAccountKp = new Keypair();
 
const data = new BN(42);
const instruction = await program.methods
  .initialize(data)
  .accounts({
    newAccount: newAccountKp.publicKey,
    signer: wallet.publicKey,
    systemProgram: SystemProgram.programId,
  })
  .instruction();
 
const transaction = new Transaction().add(instruction);
 
const transactionSignature = await connection.sendTransaction(transaction, [
  wallet.payer,
  newAccountKp,
]);

获取账户 #

客户端Program还允许你轻松获取和过滤程序账户。 只需使用program.account,然后 指定 IDL 中的账户类型名称。 Anchor 然后反序列化并返回所有指定的账户。

all() #

使 用all()获 取特定账户类型的所有现有账户。

const accounts = await program.account.newAccount.all();

memcmp #

使用memcmp过滤存储在特定偏移量处与特定值匹配的数据的账户。 在计算偏移量时,请 记住前 8 个字节是为通过 Anchor 程序创建的账户中的账户鉴别器保留的。 使 用memcmp需要你了解要获取的账户类型的数据字段的字节布局。

const accounts = await program.account.newAccount.all([
  {
    memcmp: {
      offset: 8,
      bytes: "",
    },
  },
]);

fetch() #

使 用fetch()通 过传入账户地址获取特定账户的数据

const account = await program.account.newAccount.fetch(ACCOUNT_ADDRESS);

fetchMultiple() #

使 用fetchMultiple()通 过传入一个账户地址数组来获取多个账户的账户数据

const accounts = await program.account.newAccount.fetchMultiple([
  ACCOUNT_ADDRESS_ONE,
  ACCOUNT_ADDRESS_TWO,
]);