构建您的第一个 Solana 程序

本快速入门使用由 anchor init 生成的 Anchor 框架入门项目。您将在本地创建该项目,运行其测试,构建程序,并了解 Anchor 生成的程序代码。

前提条件

在开始之前,请安装 Solana 开发工具。安装内容包括 Rust、Solana CLI 和 Anchor CLI。

本模板需要使用 Anchor CLI 1.1.2 或更高版本。检查已安装的版本:

Terminal
$
anchor --version

创建项目

在终端中运行以下命令:

Terminal
$
anchor init my-program
$
cd my-program

入门项目在 programs/my-program 下包含一个 Solana 程序。该程序包含两条指令:一条用于初始化计数器账户,另一条用于递增计数器。

模板的某些部分展示了常见的 Solana 程序模式:派生 PDA 账户地址、通过 Cross Program Invocation(CPI)转移 SOL,以及使用自定义错误检查在条件不满足时终止指令。

Anchor.toml
Cargo.toml
Cargo.toml
lib.rs
constants.rs
error.rs
instructions.rs
initialize.rs
increment.rs
state.rs
test_initialize.rs

构建程序

运行 anchor build 以编译入门程序:

Terminal
$
anchor build

编译后的程序将写入 target/deploy/my_program.so。程序部署后,该 .so 文件的内容将存储在链上账户中。

运行测试

运行默认测试:

Terminal
$
anchor test

此模板的 Anchor.toml 使用 Rust 测试命令:

Anchor.toml
skip_local_validator = true
[scripts]
test = "cargo test"

该测试将已编译的程序加载到 LiteSVM 中,创建一个付款方,发送 initializeincrement 指令,然后检查计数器账户状态。

运行 anchor test 也会编译程序,因此在本地测试时无需先运行 anchor build

部署程序

本地测试是最快速的反馈方式。当您准备好部署到网络(例如 devnet)时,请先构建,再部署到集群。

部署 Solana 程序需要 SOL,因为程序存储在账户中,而账户必须为其占用的空间支付费用。在 devnet 上,可通过 Solana Faucet 或 Solana CLI 免费获取 devnet SOL:

Terminal
$
solana airdrop 2 --url devnet
Terminal
$
anchor build
$
anchor deploy --provider.cluster devnet

源文件

src 目录包含 Solana 程序。Anchor 的 程序结构 文档说明了此处使用的核心宏,包括 declare_id!#[program]#[derive(Accounts)]#[account]。本节将逐步介绍模板文件。

lib.rs

lib.rs 是程序入口点。它连接各源文件,定义程序地址,并定义用户可调用的程序指令。

programs/my-program/src/lib.rs
pub mod constants;
pub mod error;
pub mod instructions;
pub mod state;
use anchor_lang::prelude::*;
pub use constants::*;
pub use instructions::*;
pub use state::*;
declare_id!("82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd");
#[program]
pub mod my_program {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
crate::instructions::initialize::handle_initialize(ctx)
}
pub fn increment(ctx: Context<Increment>) -> Result<()> {
crate::instructions::increment::handle_increment(ctx)
}
}
Anchor.toml
[programs.localnet]
my_program = "82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd"
lib.rs
declare_id!("82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd");

相同的程序地址出现在配置和代码中。Anchor.toml 告诉 Anchor 在集群中部署或调用哪个地址。declare_id! 在程序中定义程序地址以进行安全检查。

constants.rs

constants.rs 将共享值集中在一处。在此模板中, COUNTER_SEED 用于派生计数器 PDA,HELLO_WORLD_LAMPORTS 在初始化时转入,MAX_COUNT 在递增前进行检查。

programs/my-program/src/constants.rs
use anchor_lang::prelude::*;
#[constant]
pub const COUNTER_SEED: &[u8] = b"counter";
#[constant]
pub const HELLO_WORLD_LAMPORTS: u64 = 1;
#[constant]
pub const MAX_COUNT: u64 = 10;
initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
// ...
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
// ...
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
// ...
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
Ok(())
}

initialize.rs 使用两个常量:

  • COUNTER_SEED 用于推导计数器 PDA 地址。
  • HELLO_WORLD_LAMPORTS 设置从付款方转入计数器账户的金额。
increment.rs
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
// ...
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
Ok(())
}

increment.rs 使用 MAX_COUNT 作为计数器的上限。如果当前计数已达到最大值,require! 将返回 CounterOverflow,账户数据不会被更改。

state.rs

state.rs 为程序创建并拥有的账户定义自定义数据类型。程序定义了创建、初始化和更新该数据的指令,但计数器数据并不存储在程序本身内,而是存储在一个拥有独立地址的单独账户中。

programs/my-program/src/state.rs
use anchor_lang::prelude::*;
#[account]
#[derive(InitSpace)]
pub struct Counter {
pub count: u64,
pub authority: Pubkey,
}
initialize.rs
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
// ...
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
Ok(())
}
increment.rs
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
// ...
ctx.accounts.counter.count += 1;
Ok(())
}

state.rs 定义了 Counter 账户数据。指令文件在创建和更新账户时使用该类型:

  • Counter::INIT_SPACE 根据 state.rs 中定义的字段为账户分配空间。
  • countauthority 是账户初始化时写入的字段值。
  • count += 1 在验证通过后更新存储的计数器值。

error.rs

error.rs 定义了程序的自定义错误。在此模板中,这些错误展示了当调用方无权更新计数器,或计数器已达到 MAX_COUNT 时,指令处理程序如何停止执行。

programs/my-program/src/error.rs
use anchor_lang::prelude::*;
#[error_code]
pub enum ErrorCode {
#[msg("Only the counter authority can update this counter")]
Unauthorized,
#[msg("Counter has reached the maximum value")]
CounterOverflow,
}
increment.rs
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

error.rs 列出了指令可能返回的错误:

  • 当签名者不是计数器账户中存储的授权方时,将返回 ErrorCode::Unauthorized
  • 当计数器已达到 MAX_COUNT 时,将返回 ErrorCode::CounterOverflow

instructions.rs

instructions.rs 将指令文件连接到程序 crate,使 lib.rs 能够访问 initializeincrement 指令代码。每个指令文件定义该指令所需的账户,以及在 Anchor 验证这些账户后执行的处理逻辑。

programs/my-program/src/instructions.rs
pub mod initialize;
pub mod increment;
pub use initialize::*;
pub use increment::*;

initialize.rs

initialize.rs 定义创建计数器账户所需的账户,并写入账户的初始值。#[derive(Accounts)] 结构体使用 Anchor 账户约束 来指定所需账户以及新计数器账户的创建方式。

programs/my-program/src/instructions/initialize.rs
use anchor_lang::prelude::*;
use crate::{constants::*, state::Counter};
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

账户上下文

Initialize 结构体定义了用户调用 initialize 指令时必须包含的账户。Anchor 会在处理函数运行前验证这些账户。

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

付款账户

payer 账户负责支付创建计数器账户的费用。Signer<'info> 类型表示付款方必须对交易签名,#[account(mut)] 表示付款账户可以被修改,因为将扣除 lamport。

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

计数器账户

counter 账户存储来自 state.rsCounter 数据。 init 告知 Anchor 在处理函数运行前创建该账户,payer = payer 告知 Anchor 由哪个账户支付创建费用。

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

账户大小

space 约束告诉 Anchor 需要分配多少账户数据。Anchor 首先存储一个 8 字节的鉴别器,然后存储 Counter 字段所需的字节数。鉴别器让 Anchor 在反序列化账户数据之前,将此账户识别为 Counter 账户。

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

计数器地址

seedsbump 约束定义了计数器账户的预期 PDA 地址。Anchor 验证所提供的 counter 账户是否与该地址匹配。此模板使用 PDA,以便用户可以从程序 ID 和 seed 推导出计数器地址,使计数器地址具有确定性。

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

System Program

system_program 账户是必需的,因为创建新账户需要使用 System Program。

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

处理函数

handle_initialize 函数在 Anchor 验证 Initialize 中的账户之后运行。ctx 值使处理函数可以访问这些已验证的账户。

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

初始数据

处理函数将初始值写入新的计数器账户。计数从 0 开始,付款方成为之后被允许递增计数器的权限方。

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

转账账户

此转账 CPI 仅用于演示 CPI 如何将账户传递给另一个程序。Transfer 结构体列出了 System Program 转账所使用的账户。

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

CPI 上下文

CpiContext::new 将被调用的程序与传递给该程序的账户组合在一起。这是 CPI 的基本结构:选择要调用的程序,收集该程序所需的账户,然后将两者一起传入调用。此处被调用的程序是 System Program。

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

调用转账

anchor_lang::system_program::transfer 调用 System Program 的转账指令。在此模板中,转账是从您的程序调用另一个程序的简单示例。如果转账 CPI 失败,initialize 指令也会随之失败。

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

记录日志消息

msg! 向程序日志写入一条消息。Ok(()) 表示指令已成功返回。

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

账户上下文

Initialize 结构体定义了用户调用 initialize 指令时必须包含的账户。Anchor 会在处理函数运行前验证这些账户。

付款账户

payer 账户负责支付创建计数器账户的费用。Signer<'info> 类型表示付款方必须对交易签名,#[account(mut)] 表示付款账户可以被修改,因为将扣除 lamport。

计数器账户

counter 账户存储来自 state.rsCounter 数据。 init 告知 Anchor 在处理函数运行前创建该账户,payer = payer 告知 Anchor 由哪个账户支付创建费用。

账户大小

space 约束告诉 Anchor 需要分配多少账户数据。Anchor 首先存储一个 8 字节的鉴别器,然后存储 Counter 字段所需的字节数。鉴别器让 Anchor 在反序列化账户数据之前,将此账户识别为 Counter 账户。

计数器地址

seedsbump 约束定义了计数器账户的预期 PDA 地址。Anchor 验证所提供的 counter 账户是否与该地址匹配。此模板使用 PDA,以便用户可以从程序 ID 和 seed 推导出计数器地址,使计数器地址具有确定性。

System Program

system_program 账户是必需的,因为创建新账户需要使用 System Program。

处理函数

handle_initialize 函数在 Anchor 验证 Initialize 中的账户之后运行。ctx 值使处理函数可以访问这些已验证的账户。

初始数据

处理函数将初始值写入新的计数器账户。计数从 0 开始,付款方成为之后被允许递增计数器的权限方。

转账账户

此转账 CPI 仅用于演示 CPI 如何将账户传递给另一个程序。Transfer 结构体列出了 System Program 转账所使用的账户。

CPI 上下文

CpiContext::new 将被调用的程序与传递给该程序的账户组合在一起。这是 CPI 的基本结构:选择要调用的程序,收集该程序所需的账户,然后将两者一起传入调用。此处被调用的程序是 System Program。

调用转账

anchor_lang::system_program::transfer 调用 System Program 的转账指令。在此模板中,转账是从您的程序调用另一个程序的简单示例。如果转账 CPI 失败,initialize 指令也会随之失败。

记录日志消息

msg! 向程序日志写入一条消息。Ok(()) 表示指令已成功返回。

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

increment.rs

increment.rs 定义了更新现有计数器账户所需的账户。处理程序会检查签名者是否为已存储的授权方,检查计数是否已达到指定的 MAX_COUNT,然后递增计数。

programs/my-program/src/instructions/increment.rs
use anchor_lang::prelude::*;
use crate::{constants::*, error::ErrorCode, state::Counter};
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

Account Context

Increment 结构体定义了用户调用 increment 指令时必须包含的账户。Anchor 会在处理程序运行之前验证这些账户。

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

Counter Account

counter 账户存储 Counter 数据。mut 约束允许处理程序更新已存储的计数,seedsbump 约束用于验证计数器 PDA 地址。

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

Authority Signer

authority 账户必须对交易进行签名。处理程序随后会检查该签名者是否与计数器账户中存储的授权方相匹配。

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

处理函数

handle_increment 函数在 Anchor 验证 Increment 中的账户后运行。ctx 值使处理函数能够访问这些已检查的账户。

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

权限检查

第一项检查确保签名者有权更新此计数器。如果签名者的地址与 counter.authority 不匹配,指令将以 ErrorCode::Unauthorized 终止。这展示了应用层级的授权机制:程序拥有计数器数据,但它实现了一条规则,规定哪个签名者可以修改该数据。

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

最大计数检查

第二项检查防止计数器超过 MAX_COUNT。如果计数器已达到上限,指令将以 ErrorCode::CounterOverflow 终止。此上限是模板中的一条人为规则,用于演示自定义错误如何在账户数据被修改之前终止指令。

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

更新计数

只有当两项检查均通过后,处理函数才会更新账户数据。此行将存储的计数器值加一。

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

日志消息

msg! 将更新后的计数写入程序日志。Ok(()) 表示指令已成功返回。

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

Account Context

Increment 结构体定义了用户调用 increment 指令时必须包含的账户。Anchor 会在处理程序运行之前验证这些账户。

Counter Account

counter 账户存储 Counter 数据。mut 约束允许处理程序更新已存储的计数,seedsbump 约束用于验证计数器 PDA 地址。

Authority Signer

authority 账户必须对交易进行签名。处理程序随后会检查该签名者是否与计数器账户中存储的授权方相匹配。

处理函数

handle_increment 函数在 Anchor 验证 Increment 中的账户后运行。ctx 值使处理函数能够访问这些已检查的账户。

权限检查

第一项检查确保签名者有权更新此计数器。如果签名者的地址与 counter.authority 不匹配,指令将以 ErrorCode::Unauthorized 终止。这展示了应用层级的授权机制:程序拥有计数器数据,但它实现了一条规则,规定哪个签名者可以修改该数据。

最大计数检查

第二项检查防止计数器超过 MAX_COUNT。如果计数器已达到上限,指令将以 ErrorCode::CounterOverflow 终止。此上限是模板中的一条人为规则,用于演示自定义错误如何在账户数据被修改之前终止指令。

更新计数

只有当两项检查均通过后,处理函数才会更新账户数据。此行将存储的计数器值加一。

日志消息

msg! 将更新后的计数写入程序日志。Ok(()) 表示指令已成功返回。

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

测试文件

programs/my-program/tests/test_initialize.rs 是一个 Rust 集成测试,不会启动本地 validator。它将编译后的 .so 文件加载到 LiteSVM 中,构建调用程序的交易,并在每次交易后读取计数器账户。该测试通过指定要调用程序的程序 ID、提供 instruction data 并传入所需账户来构建 Solana 交易的指令。

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
test_initialize.rs
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Initialize {}.data(),
my_program::accounts::Initialize {
payer: payer.pubkey(),
counter,
system_program: system_program::ID,
}
.to_account_metas(None),
);

Initialize 账户上下文定义了 initialize 指令所需的账户。测试将这些相同的账户传递给生成的 my_program::accounts::Initialize 辅助函数:

  • payer 作为 payer: payer.pubkey() 传入。
  • counter 作为 counter 传入。
  • system_program 作为 system_program::ID 传入。

my_program::instruction::Initialize {}.data() 创建 instruction data。这里是对指令参数进行编码的地方,但此 initialize 指令不需要任何参数。

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
test_initialize.rs
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Increment {}.data(),
my_program::accounts::Increment {
counter,
authority: payer.pubkey(),
}
.to_account_metas(None),
);

Increment 账户上下文定义了 increment 指令所需的账户。测试将这些相同的账户传递给生成的 my_program::accounts::Increment 辅助函数:

  • counter 作为 counter 传入。
  • authority 作为 authority: payer.pubkey() 传入。

my_program::instruction::Increment {}.data() 创建 instruction data。这里是对指令参数进行编码的地方,但此 increment 指令不需要任何参数。

programs/my-program/tests/test_initialize.rs
use {
anchor_lang::{
prelude::Pubkey,
solana_program::{instruction::Instruction, system_program},
AccountDeserialize, InstructionData, ToAccountMetas,
},
litesvm::LiteSVM,
solana_keypair::Keypair,
solana_message::{Message, VersionedMessage},
solana_signer::Signer,
solana_transaction::versioned::VersionedTransaction,
};
#[test]
fn test_initialize() {
let program_id = my_program::id();
let payer = Keypair::new();
let counter = Pubkey::find_program_address(
&[my_program::constants::COUNTER_SEED],
&program_id,
)
.0;
let mut svm = LiteSVM::new();
let bytes = include_bytes!(concat!(
env!("CARGO_TARGET_TMPDIR"),
"/../deploy/my_program.so"
));
svm.add_program(program_id, bytes).unwrap();
svm.airdrop(&payer.pubkey(), 1_000_000_000).unwrap();
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Initialize {}.data(),
my_program::accounts::Initialize {
payer: payer.pubkey(),
counter,
system_program: system_program::ID,
}
.to_account_metas(None),
);
let blockhash = svm.latest_blockhash();
let msg = Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &blockhash);
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &[&payer]).unwrap();
let res = svm.send_transaction(tx);
assert!(res.is_ok());
let counter_account = svm.get_account(&counter).unwrap();
let mut data: &[u8] = &counter_account.data;
let counter_state = my_program::state::Counter::try_deserialize(&mut data).unwrap();
assert_eq!(counter_state.count, 0);
assert_eq!(counter_state.authority, payer.pubkey());
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Increment {}.data(),
my_program::accounts::Increment {
counter,
authority: payer.pubkey(),
}
.to_account_metas(None),
);
let blockhash = svm.latest_blockhash();
let msg = Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &blockhash);
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &[&payer]).unwrap();
let res = svm.send_transaction(tx);
assert!(res.is_ok());
let counter_account = svm.get_account(&counter).unwrap();
let mut data: &[u8] = &counter_account.data;
let counter_state = my_program::state::Counter::try_deserialize(&mut data).unwrap();
assert_eq!(counter_state.count, 1);
assert_eq!(counter_state.authority, payer.pubkey());
}

项目配置

根目录项目文件告诉 Anchor 和 Cargo 如何构建、测试和部署程序。完整参考请参阅 Anchor 文档中的 Anchor.toml 配置Anchor CLI

Anchor.toml
skip_local_validator = true
[toolchain]
[features]
resolution = true
skip-lint = false
[programs.localnet]
my_program = "82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd"
[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"
[scripts]
test = "cargo test"
[hooks]

Is this page helpful?

Table of Contents

Edit Page
©️ 2026 Solana 基金会版权所有