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 上的程序。
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)。
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
。
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
语法。
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 字节添加。 这有助于区分账户类型和账户验证。
- 数据序列化和反序列化: 与账户类型对应的账户数据会自动序列化和反序列化。
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
数组对应于程序中的指令,并指定每个指令所需的账户和参数。
{
"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" }]
}
}
]
}
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]
宏注释的结构体,这些结构体指定了
程序数据账户的结构。
{
"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" }]
}
}
]
}
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
)和一个
AnchorProvider
。
AnchorProvider
结合了两件事:
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
与前端集成时,需要手动设置AnchorProvider
和Program
。
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,
]);