SC Day 35
Solana/Anchor - 框架入门 - #[program]/#[derive(Accounts)]/#[account] + Hello World
### 一、为什么需要 Anchor?
2026-05-05
第二阶段:框架实战SolanaAnchorFrameworkHelloWorldRust
日期: 2026-05-05 方向: Rust/Solana 阶段: 第二阶段:框架实战 标签: #Solana #Anchor #Framework #HelloWorld #Rust
今日目标
- 安装 Anchor 开发框架并理解其设计理念
- 掌握 Anchor 项目结构(programs/tests/migrations)
- 深入理解三个核心宏:
#[program]、#[derive(Accounts)]、#[account] - 实现一个完整的 Hello World Anchor 程序(initialize + greet)并编写 TypeScript 测试
- 对比 Anchor 与原生 Solana 开发的差异
核心概念
一、为什么需要 Anchor?
原生 Solana 程序开发非常繁琐和容易出错:
// 原生 Solana: 手动序列化/反序列化 + 手动账户验证
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
// 1. 手动解析 instruction_data (哪个指令?什么参数?)
let instruction = MyInstruction::try_from_slice(instruction_data)?;
// 2. 手动获取和验证每个账户
let account_iter = &mut accounts.iter();
let user = next_account_info(account_iter)?;
let data_account = next_account_info(account_iter)?;
let system_program = next_account_info(account_iter)?;
// 3. 手动验证签名
if !user.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// 4. 手动验证 owner
if data_account.owner != program_id {
return Err(ProgramError::IncorrectProgramId);
}
// 5. 手动序列化/反序列化数据
let mut data = MyData::try_from_slice(&data_account.data.borrow())?;
data.value += 1;
data.serialize(&mut *data_account.data.borrow_mut())?;
Ok(())
}
Anchor 通过 Rust 宏自动处理上述所有样板代码:
// Anchor: 声明式,安全,简洁
#[program]
pub mod my_program {
use super::*;
pub fn increment(ctx: Context<Increment>) -> Result<()> {
ctx.accounts.counter.value += 1;
Ok(())
}
}
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(mut)]
pub counter: Account<'info, Counter>,
}
#[account]
pub struct Counter {
pub value: u64,
}
二、Anchor 安装与项目初始化
安装 Anchor
# 方式 1: 使用 avm (Anchor Version Manager,推荐)
cargo install --git https://github.com/coral-xyz/anchor avm --force
avm install latest
avm use latest
# 验证安装
anchor --version
# anchor-cli 0.30.1
# 方式 2: 使用 cargo 直接安装
cargo install --git https://github.com/coral-xyz/anchor --tag v0.30.1 anchor-cli
创建项目
# 初始化新项目
anchor init hello-anchor
cd hello-anchor
# 项目结构:
hello-anchor/
├── Anchor.toml # Anchor 配置文件(类似 Foundry 的 foundry.toml)
├── Cargo.toml # Rust workspace 配置
├── app/ # 前端应用(可选)
├── migrations/ # 部署脚本
│ └── deploy.ts
├── programs/ # Solana 程序源码
│ └── hello-anchor/
│ ├── Cargo.toml
│ └── src/
│ └── lib.rs # 主程序文件
├── tests/ # TypeScript 测试
│ └── hello-anchor.ts
└── tsconfig.json
Anchor.toml 配置
[features]
seeds = false
skip-lint = false
[programs.localnet]
hello_anchor = "你的程序ID"
[programs.devnet]
hello_anchor = "你的程序ID"
[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"
三、Anchor 三大核心宏
3.1 #[program] — 程序定义
#[program] 宏标记一个模块为 Solana 程序的入口。模块中的每个 public 函数都会成为一个可调用的指令(instruction)。
use anchor_lang::prelude::*;
declare_id!("你的程序ID"); // 程序的链上地址
#[program]
pub mod hello_anchor {
use super::*;
// 每个函数 = 一个 Instruction
// 第一个参数必须是 Context<T>
// 返回类型必须是 Result<()>
pub fn initialize(ctx: Context<Initialize>, name: String) -> Result<()> {
let greeting = &mut ctx.accounts.greeting;
greeting.name = name;
greeting.count = 0;
greeting.authority = ctx.accounts.user.key();
msg!("Greeting account initialized for: {}", greeting.name);
Ok(())
}
pub fn greet(ctx: Context<Greet>) -> Result<()> {
let greeting = &mut ctx.accounts.greeting;
greeting.count += 1;
msg!("Hello {}! You've been greeted {} times.",
greeting.name, greeting.count);
Ok(())
}
}
#[program] 宏自动生成的内容:
1. process_instruction 入口函数
2. Instruction 路由(根据函数名的 hash 分发)
3. 参数的 Borsh 反序列化
4. 账户的验证和加载
5. 错误处理和转换
3.2 #[derive(Accounts)] — 账户验证
#[derive(Accounts)] 宏定义一个指令需要的所有账户,以及每个账户的约束条件。这是 Anchor 安全性的核心。
#[derive(Accounts)]
pub struct Initialize<'info> {
// #[account(init, ...)] — 创建新账户
#[account(
init, // 创建新账户
payer = user, // 谁支付 rent
space = 8 + Greeting::INIT_SPACE, // 账户大小(8 字节 discriminator + 数据)
)]
pub greeting: Account<'info, Greeting>,
// #[account(mut)] — 标记为可写(会修改 lamports)
#[account(mut)]
pub user: Signer<'info>, // Signer 类型自动验证签名
// System Program 用于创建账户
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Greet<'info> {
// 已存在的账户,标记为可写(会修改数据)
#[account(mut)]
pub greeting: Account<'info, Greeting>,
}
常用账户约束:
| 约束 | 说明 | 示例 |
|---|---|---|
init | 创建新账户 | #[account(init, payer = user, space = 100)] |
mut | 可写账户 | #[account(mut)] |
has_one | 验证字段匹配 | #[account(has_one = authority)] |
constraint | 自定义约束 | #[account(constraint = amount > 0)] |
seeds | PDA 种子 | #[account(seeds = [b"vault"], bump)] |
close | 关闭账户退还 rent | #[account(mut, close = user)] |
realloc | 调整账户大小 | #[account(realloc = new_size, ...)] |
Anchor 账户类型:
| 类型 | 说明 | 自动验证 |
|---|---|---|
Account<'info, T> | 拥有数据 T 的账户 | owner、discriminator |
Signer<'info> | 必须签名的账户 | is_signer |
Program<'info, T> | 程序账户 | executable、program_id |
SystemAccount<'info> | 系统账户 | owner = System Program |
UncheckedAccount<'info> | 不做检查的账户 | 无(需手动验证) |
3.3 #[account] — 数据结构定义
#[account] 宏定义程序的数据结构(存储在 Data Account 中)。
#[account]
#[derive(InitSpace)] // 自动计算 space
pub struct Greeting {
#[max_len(50)] // String 需要指定最大长度
pub name: String,
pub count: u64,
pub authority: Pubkey,
}
#[account] 宏自动做的事:
1. 添加 8 字节 discriminator(前缀,用于识别账户类型)
discriminator = sha256("account:<AccountName>")[..8]
2. 实现 Borsh 序列化/反序列化 (BorshSerialize + BorshDeserialize)
3. 实现 AccountSerialize + AccountDeserialize trait
4. 实现 Owner trait(绑定到当前程序)
Space 计算规则:
总 space = 8 (discriminator) + 数据大小
基本类型:
bool → 1 byte
u8/i8 → 1 byte
u16/i16 → 2 bytes
u32/i32 → 4 bytes
u64/i64 → 8 bytes
u128/i128 → 16 bytes
Pubkey → 32 bytes
复合类型:
String → 4 (length prefix) + content bytes
Vec<T> → 4 (length prefix) + n * size_of::<T>()
Option<T> → 1 + size_of::<T>()
示例: Greeting
name (max 50 chars) → 4 + 50 = 54 bytes
count (u64) → 8 bytes
authority (Pubkey) → 32 bytes
total data → 94 bytes
total space → 8 + 94 = 102 bytes
四、Anchor 开发工作流
1. anchor build → 编译程序(生成 IDL 和 TypeScript 类型)
2. anchor test → 运行测试(自动启动 localnet)
3. anchor deploy → 部署到指定网络
4. anchor verify → 验证链上代码与本地一致
代码实战
完整的 Hello World 程序
程序代码
// programs/hello-anchor/src/lib.rs
use anchor_lang::prelude::*;
declare_id!("HeLLo11111111111111111111111111111111111111"); // 临时 ID,部署时会更新
#[program]
pub mod hello_anchor {
use super::*;
/// 初始化一个 Greeting 账户
pub fn initialize(ctx: Context<Initialize>, name: String) -> Result<()> {
require!(name.len() <= 50, HelloError::NameTooLong);
let greeting = &mut ctx.accounts.greeting;
greeting.name = name.clone();
greeting.count = 0;
greeting.authority = ctx.accounts.user.key();
greeting.bump = ctx.bumps.greeting;
msg!("Greeting account initialized!");
msg!("Name: {}", name);
msg!("Authority: {}", greeting.authority);
Ok(())
}
/// 问候并递增计数器
pub fn greet(ctx: Context<Greet>) -> Result<()> {
let greeting = &mut ctx.accounts.greeting;
greeting.count = greeting.count.checked_add(1)
.ok_or(HelloError::Overflow)?;
msg!("Hello, {}! Greeted {} times.", greeting.name, greeting.count);
Ok(())
}
/// 更新名字(只有 authority 可以操作)
pub fn update_name(ctx: Context<UpdateName>, new_name: String) -> Result<()> {
require!(new_name.len() <= 50, HelloError::NameTooLong);
let greeting = &mut ctx.accounts.greeting;
let old_name = greeting.name.clone();
greeting.name = new_name.clone();
msg!("Name updated: {} -> {}", old_name, new_name);
Ok(())
}
/// 关闭账户(退还 rent 给 authority)
pub fn close_greeting(_ctx: Context<CloseGreeting>) -> Result<()> {
msg!("Greeting account closed!");
Ok(())
}
}
// ===== Accounts Structs =====
#[derive(Accounts)]
#[instruction(name: String)] // 允许在约束中使用指令参数
pub struct Initialize<'info> {
#[account(
init,
payer = user,
space = 8 + Greeting::INIT_SPACE,
seeds = [b"greeting", user.key().as_ref()], // PDA
bump,
)]
pub greeting: Account<'info, Greeting>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Greet<'info> {
#[account(mut)]
pub greeting: Account<'info, Greeting>,
}
#[derive(Accounts)]
pub struct UpdateName<'info> {
#[account(
mut,
has_one = authority, // 验证 greeting.authority == authority.key()
)]
pub greeting: Account<'info, Greeting>,
pub authority: Signer<'info>,
}
#[derive(Accounts)]
pub struct CloseGreeting<'info> {
#[account(
mut,
has_one = authority,
close = authority, // 关闭账户,rent 退还给 authority
)]
pub greeting: Account<'info, Greeting>,
#[account(mut)]
pub authority: Signer<'info>,
}
// ===== Data Account =====
#[account]
#[derive(InitSpace)]
pub struct Greeting {
#[max_len(50)]
pub name: String, // 4 + 50 = 54 bytes
pub count: u64, // 8 bytes
pub authority: Pubkey, // 32 bytes
pub bump: u8, // 1 byte
}
// Total: 8 (discriminator) + 54 + 8 + 32 + 1 = 103 bytes
// ===== Custom Errors =====
#[error_code]
pub enum HelloError {
#[msg("Name is too long (max 50 characters)")]
NameTooLong,
#[msg("Counter overflow")]
Overflow,
}
TypeScript 测试
// tests/hello-anchor.ts
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { HelloAnchor } from "../target/types/hello_anchor";
import { expect } from "chai";
describe("hello-anchor", () => {
// 配置 provider(连接到 localnet)
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.HelloAnchor as Program<HelloAnchor>;
const user = provider.wallet;
// 派生 PDA 地址
const [greetingPDA, bump] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("greeting"), user.publicKey.toBuffer()],
program.programId
);
it("Initializes a greeting account", async () => {
const tx = await program.methods
.initialize("Alice")
.accounts({
greeting: greetingPDA,
user: user.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
console.log("Initialize tx:", tx);
// 获取账户数据
const greetingAccount = await program.account.greeting.fetch(greetingPDA);
expect(greetingAccount.name).to.equal("Alice");
expect(greetingAccount.count.toNumber()).to.equal(0);
expect(greetingAccount.authority.toString()).to.equal(
user.publicKey.toString()
);
console.log("Greeting account:", greetingAccount);
});
it("Greets and increments counter", async () => {
// 第一次问候
await program.methods
.greet()
.accounts({
greeting: greetingPDA,
})
.rpc();
let greetingAccount = await program.account.greeting.fetch(greetingPDA);
expect(greetingAccount.count.toNumber()).to.equal(1);
// 第二次问候
await program.methods
.greet()
.accounts({
greeting: greetingPDA,
})
.rpc();
greetingAccount = await program.account.greeting.fetch(greetingPDA);
expect(greetingAccount.count.toNumber()).to.equal(2);
});
it("Updates name (only authority)", async () => {
await program.methods
.updateName("Bob")
.accounts({
greeting: greetingPDA,
authority: user.publicKey,
})
.rpc();
const greetingAccount = await program.account.greeting.fetch(greetingPDA);
expect(greetingAccount.name).to.equal("Bob");
});
it("Fails to update name with wrong authority", async () => {
// 创建一个不同的密钥对
const wrongUser = anchor.web3.Keypair.generate();
try {
await program.methods
.updateName("Hacker")
.accounts({
greeting: greetingPDA,
authority: wrongUser.publicKey,
})
.signers([wrongUser])
.rpc();
// 不应该执行到这里
expect.fail("Should have thrown an error");
} catch (err) {
// 预期会失败(has_one 约束验证)
expect(err).to.be.instanceOf(Error);
console.log("Error (expected):", err.message.substring(0, 100));
}
});
it("Closes greeting account", async () => {
// 记录关闭前的余额
const balanceBefore = await provider.connection.getBalance(user.publicKey);
await program.methods
.closeGreeting()
.accounts({
greeting: greetingPDA,
authority: user.publicKey,
})
.rpc();
// 验证账户已被关闭
const greetingAccount = await provider.connection.getAccountInfo(
greetingPDA
);
expect(greetingAccount).to.be.null;
// 验证 rent 已退还
const balanceAfter = await provider.connection.getBalance(user.publicKey);
expect(balanceAfter).to.be.greaterThan(balanceBefore);
console.log("Rent refunded:", (balanceAfter - balanceBefore) / 1e9, "SOL");
});
});
运行测试
# 构建程序
anchor build
# 运行测试(自动启动本地验证器)
anchor test
# 如果已经有运行中的本地验证器
anchor test --skip-local-validator
# 部署到 devnet
anchor deploy --provider.cluster devnet
五、Anchor vs 原生 Solana 开发对比
| 维度 | 原生 Solana | Anchor |
|---|---|---|
| 代码量 | 300+ 行 | 100 行 |
| 账户验证 | 手动逐一检查 | 声明式约束 |
| 序列化 | 手动 Borsh | 自动 |
| 错误处理 | ProgramError | 自定义 error_code |
| IDL 生成 | 无 | 自动生成(类似 ABI) |
| TypeScript 类型 | 手动写 | 从 IDL 自动生成 |
| 学习曲线 | 陡峭 | 相对平缓 |
| 灵活性 | 完全控制 | 有宏的约束 |
| Gas 效率 | 更优(手动优化) | 略高开销 |
| 生态使用率 | 少数高性能项目 | 主流选择 |
Anchor 生成的 IDL 示例(类似 Solidity 的 ABI):
{
"version": "0.1.0",
"name": "hello_anchor",
"instructions": [
{
"name": "initialize",
"accounts": [
{ "name": "greeting", "isMut": true, "isSigner": false },
{ "name": "user", "isMut": true, "isSigner": true },
{ "name": "systemProgram", "isMut": false, "isSigner": false }
],
"args": [
{ "name": "name", "type": "string" }
]
}
],
"accounts": [
{
"name": "Greeting",
"type": {
"kind": "struct",
"fields": [
{ "name": "name", "type": "string" },
{ "name": "count", "type": "u64" },
{ "name": "authority", "type": "publicKey" },
{ "name": "bump", "type": "u8" }
]
}
}
]
}
关键要点总结
Anchor 三大核心宏
#[program] → 定义程序入口和所有指令
每个 pub fn = 一个 Instruction
参数自动反序列化
#[derive(Accounts)] → 定义每个指令需要的账户和约束
init/mut/has_one/seeds/close
自动验证签名、owner、discriminator
#[account] → 定义链上数据结构
自动添加 8 字节 discriminator
自动实现 Borsh 序列化
自动绑定 owner
开发心智模型
设计程序时的思考顺序:
1. 定义数据结构 (#[account])
→ 需要存储什么数据?
→ 计算 space
2. 定义指令 (#[program])
→ 用户可以做哪些操作?
→ 每个操作修改什么数据?
3. 定义账户约束 (#[derive(Accounts)])
→ 每个指令需要哪些账户?
→ 谁需要签名?谁需要付钱?
→ 有什么安全约束?
常见误区
-
误区:Anchor 和 Hardhat 一样是测试框架
- 事实:Anchor 是一个完整的开发框架,包括程序编写、编译、测试、部署的全套工具
-
误区:
#[account(init)]创建的账户可以重复创建- 事实:
init约束检查账户是否已存在。如果已初始化,会报错
- 事实:
-
误区:Space 计算不需要加 8
- 事实:每个 Anchor 账户都有 8 字节 discriminator 前缀,必须加上
-
误区:任何人都可以修改 Account 的数据
- 事实:只有 Account 的 owner 程序才能修改。Anchor 通过
#[account]宏自动设置 owner
- 事实:只有 Account 的 owner 程序才能修改。Anchor 通过
-
误区:
declare_id!中的地址不重要- 事实:它必须匹配链上部署的程序地址,否则 Anchor 的安全检查会失败
面试关联
Q1: Anchor 框架的核心价值是什么?
简短回答:Anchor 通过 Rust 宏自动处理账户验证、序列化和安全检查,让开发者用声明式代码取代大量手动样板代码,同时生成 IDL 方便客户端集成。
详细回答:
- 安全性:
#[derive(Accounts)]的约束系统自动验证账户 owner、signer、可写性等,避免了原生开发中常见的安全遗漏 - 开发效率:减少约 70% 的样板代码
- 类型安全:从 IDL 自动生成 TypeScript 类型,前后端类型一致
- 标准化:统一的项目结构和开发流程,降低团队协作成本
Q2: 解释 Anchor 中 PDA 的创建流程
答案:
- 在
#[derive(Accounts)]中使用seeds和bump约束声明 PDA init+seeds= Anchor 自动调用create_account+ 设置 PDA- PDA 地址由 seeds + program_id + bump 确定性生成
- bump 会自动存储在
ctx.bumps中,建议保存到账户数据里供后续使用
Q3: Anchor 的 discriminator 是什么?有什么作用?
答案:
- Discriminator 是每个 Anchor 账户数据的前 8 字节
- 计算方式:
sha256("account:<AccountStructName>")[..8] - 作用:当程序读取账户数据时,首先验证 discriminator 是否匹配,防止传入错误类型的账户
- 这是 Anchor 类型安全的关键保障
参考资源
- Anchor 官方文档 — 框架文档
- Anchor Book — 深度教程
- Anchor GitHub — 源码和示例
- Anchor by Example — 代码示例集
- Solana Cookbook - Anchor — 实用参考
- Solana Playground — 浏览器内 Anchor 开发
- Coral Anchor Examples — 官方测试用例