# 概述
本文介绍使用anchor框架进行开发时的一些概念以及开发流程。
# anchor框架的安装
anchor的安装流程如下:
1. 安装相关依赖
```
sudo apt-get update
sudo apt-get install -y \
build-essential \
pkg-config \
libudev-dev llvm libclang-dev \
protobuf-compiler libssl-dev
```
2. 安装rust(已有可以不安装)
```
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
```
安装成功的输出:
```
Rust is installed now. Great!
To get started you may need to restart your current shell.
This would reload your PATH environment variable to include
Cargo's bin directory ($HOME/.cargo/bin).
To configure your current shell, you need to source
the corresponding env file under $HOME/.cargo.
This is usually done by running one of the following (note the leading DOT):
. "$HOME/.cargo/env" # For sh/bash/zsh/ash/dash/pdksh
source "$HOME/.cargo/env.fish" # For fish
```
安装成功后需要重启终端。
测试是否成功安装:
```
rustc --version
输出为(有可能是其他版本):
rustc 1.84.1 (e71f9a9a9 2025-01-27)
```
3. 安装solana cli
```
sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"
```
在执行完毕后重启终端。
测试是否安装成功:
```
solana --version
输出:
solana-cli 2.0.26 (src:3dccb3e7; feat:607245837, client:Agave)
```
4. 安装anchor cli
先安装avm:
```
cargo install --git https://github.com/coral-xyz/anchor avm --force
```
再安装anchor cli:
```
avm install latest
avm use latest
```
检查是否安装成功:
```
anchor --version
输出:
anchor-cli 0.30.1
```
至此,anchor框架安装完毕。
# 生成project
执行`anchor init proj_name`生成一个anchor项目。在此过程中需要安装一系列依赖,最终如果终端输出`proj_name initialized`即说明初始化成功。
# 项目结构
此处只标识重要的文件夹:
```
programs
-proj_name:项目名称
-src:源代码
-cargo.toml:rust依赖
tests:存放test文件(ts编写)
Anchor.toml(当前项目的anchor配置)
```
# 合约
## 概念
在anchor中有一系列的marco需要了解:
1. `#[program]`(属性宏):定义包含所有指令的模块。
2. `#[account]`(属性宏):标记solana链上的一个账户。
**常用:**
```rust!
#[account(init, payer = user, space = 8 + 1024, seeds=[b"game account"], bump)]
pub game_account: Account<'info, GameRecordAccount>,
// init:代表初始化一个账户。
// payer=user:账户初始化的租金支付者(这时需要在GameRecordAccount中定义user)。
// space:账户的初始空间。
// seeds:生成账户时使用的种子。
// bump:在生成账户时确保账户有效。
```
**细节:**
`#[account]`自带序列化和反序列功能,当用户传入账户数据反序列化为account时,账户数据的前8个字节为account的鉴别符。
一个账户的鉴别符是由账户(例如GameRecordAccount)名称进行sha256并取前8字节生成的。
Zero Copy Deserialization(零拷贝反序列化)是`#[account]`的一个实验功能,适用于数据量极大的情况。
bump是一个单字节值(0-255),用于确保PDA是有效的Solana地址。
**具体来说:** Solana的地址必须落在Ed25519椭圆曲线的有效范围内。直接用程序ID和种子生成的哈希值可能不在曲线上,因此需要一个额外的“偏移”(bump),通过尝试不同的值(从255递减到0)来调整哈希,直到找到一个有效的地址。这个有效的bump值被称为canonical bump(规范bump)。
3. `#[derive(Accounts)]`(过程宏):定义了Solana程序输入中的账户集合。其自带账户验证功能,只有通过验证的账户才能真正输入到程序中。
**常用:**
```rust
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init, payer = user, space = 8 + 1024, seeds=[b"game account"], bump)]
pub game_account: Account<'info, GameRecordAccount>,
// 交易发起者
#[account(mut)]
pub user: Signer<'info>,
// 系统程序
pub system_program: Program<'info, System>,
}
// #[derive(Accounts)]定义程序输入账户集合
// #[account(...)]定义账户的约束,必须符合条件才可通过账户验证
```
4. `#[error_code]`(属性宏):自定义错误。
```rust!
// #[msg("")]定义错误信息
#[error_code]
pub enum MyError {
#[msg("My custom error message")]
MyCustomError,
#[msg("My second custom error message")]
MySecondCustomError,
}
// 抛出错误时,需要使用err!
#[program]
mod hello_anchor {
use super::*;
pub fn set_data(ctx: Context<SetData, data: MyAccount) ->Result() {
if data.data = 100 {
// 使用err!抛出错误
return err!(MyError::MySecondCustomError);
}
ctx.accounts.my_account.set_inner(data);
Ok(())
}
}
```
5. `require!`:处理错误条件。
```rust
#[program]
mod hello_anchor {
use super::*;
pub fn set_data(ctx: Context<SetData, data: MyAccount) - Result<() {
// 在data.data大于等于100时抛出错误
require!(data.data < 100, MyError::DataTooLarge);
ctx.accounts.my_account.set_inner(data);
Ok(())
}
}
#[error_code]
pub enum MyError {
#[msg("MyAccount may only hold data below 100")]
DataTooLarge
}
```
这里有一系列的`require`:

6. `declare_id!`:声明程序id。
## easy
在solana中,合约都是用rust语言编写,以下是一个简单的合约:
```rust!
// 导入anchor的相关依赖
use anchor_lang::prelude::*;
// anchor自动生成的程序地址
declare_id!("DhCfDz44APkvLNQUs51XMKDVqPATtzPrqwjxuZ9wwoza");
// #[program]代表程序的指令模块
#[program]
pub mod vote {
// 默认配置,用于导入anchor相关依赖
use super::*;
// 初始化方法
// Context<Initialize>代表执行过程中需要的账户列表
// 同时传入ctx后,指令即可在不传入参数的情况下访问这些账户
pub fn initialize(ctx: Context<Initialize>) -> Result<()>{
// 一条log
msg!("Greetings from: {:?}", ctx.program_id);
Ok(())
}
}
// #[derive(Accounts)]代表在执行某个操作时相关的账户列表
#[derive(Accounts)]
pub struct Initialize {}
```
注意:ctx必须是指令的第一个参数。
## difficult
此处实现了一个简单的石头剪刀布游戏,用户可以传入choice(剪刀、石头、布)以及value(赌注)进行游戏,play指令自动生成一个choice进行剪刀石头布。如果程序赢了则用户的赌注被程序收走,若程序数量则程序给用户转账赌注。
注意:此处生成随机数的方式是可被预测的!
```rust!
use anchor_lang::prelude::*;
declare_id!("ZbabjkUPaBtNPwJyviGHFhHaVmwnzR33M26pyg8P5qS");
#[program]
pub mod guess {
use anchor_lang::solana_program::{program::invoke, system_instruction};
use super::*;
// 初始化指令
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
// 初始化record数组
let game_account = &mut ctx.accounts.game_account;
game_account.records = Vec::<GameRecord>::new();
Ok(())
}
//
pub fn play(ctx: Context<Player>, play_choice: u8, value: u64) -> Result<()> {
// 检查choice
if play_choice >= 3 {
return err!(GuessError::InvaidChoice);
}
// 发出一条log
msg!("choice正常");
// 检查value
if value > 1000 {
return err!(GuessError::InvalidValue);
}
msg!("value正常");
// !!! 生成program choice,这里的随机数可被预测
let clock: Clock = Clock::get()?;
let program_choice = (clock.slot % 3) as u8;
msg!("program_choice {:?}", program_choice);
let result = match (program_choice, play_choice) {
(0, 2) | (1, 0) | (2, 0) => 1,
(0, 1) | (1, 2) | (2, 1) => 2,
_ => 3,
};
msg!("用户胜利?{:?}", result);
let (from, to) = if result == 1 {
// 程序赢 => 用户给程序转钱
(
ctx.accounts.game_account.to_account_info(),
ctx.accounts.user.to_account_info(),
)
} else if result == 2 {
// 用户赢 => 程序给用户转钱
(
ctx.accounts.user.to_account_info(),
ctx.accounts.game_account.to_account_info(),
)
} else {
msg!("平局!");
return Ok(());
};
msg!("转账路径 {:?} -> {:?}", from, to);
// 转账,使用invoke函数传入指令和account info
invoke(
&system_instruction::transfer(from.key, to.key, value),
&[from, to, ctx.accounts.system_program.to_account_info()],
)?;
msg!("转账完成");
// 记录
let game_account = &mut ctx.accounts.game_account;
game_account.records.push(GameRecord {
play_choice,
program_choice,
result,
player: ctx.accounts.user.key(),
value,
});
Ok(())
}
}
// 初始化结构体
// 需要初始化记录账户
#[derive(Accounts)]
pub struct Initialize<'info> {
// 游戏程序账户
#[account(init, payer = user, space = 8 + 1024, seeds=[b"game account"], bump)]
pub game_account: Account<'info, GameRecordAccount>,
// 交易发起者
#[account(mut)]
pub user: Signer<'info>,
// 系统程序
pub system_program: Program<'info, System>,
}
// 进行play的结构体
#[derive(Accounts)]
pub struct Player<'info> {
#[account(mut)]
pub game_account: Account<'info, GameRecordAccount>,
#[account(mut)]
pub user: Signer<'info>,
// 系统程序
pub system_program: Program<'info, System>,
}
// 游戏记录的账户
#[account]
pub struct GameRecordAccount {
pub records: Vec<GameRecord>,
}
// 单条游戏记录
#[derive(Debug, Clone, Copy, AnchorDeserialize, AnchorSerialize)]
pub struct GameRecord {
pub play_choice: u8,
pub program_choice: u8,
pub result: u8,
pub player: Pubkey,
pub value: u64,
}
// 错误
#[error_code]
pub enum GuessError {
#[msg("Invalid Choice:Choice must be 0,1,2")]
InvaidChoice,
#[msg("Invalid Value:Value <= 1000")]
InvalidValue,
}
```
## 测试
在`tests`目录下编写ts测试。
```ts
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Guess } from "../target/types/guess";
import BN from "bn.js";
describe("guess", () => {
// 根据当前环境获取provider
// 使用`solana config set --url ""`切换当前环境集群
const provider = anchor.AnchorProvider.env();
// 为anchor设置provider
anchor.setProvider(provider);
// 一个program对象
const program = anchor.workspace.Guess as Program<Guess>;
it("Guess!", async () => {
try {
// 初始化指令的执行,必须加上rpc()
const tx = await program.methods.initialize().rpc();
console.log("Your transaction signature", tx);
} catch (e) {
console.error("init ", e);
}
// 根据seed寻找pda地址
const [gameAccountPda, bump] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("game account")], program.programId
);
console.log("Game Account PDA:", gameAccountPda.toString());
// 进行游戏
const tx2 = await program.methods.play(1, new BN(100)).accounts({
gameAccount: gameAccountPda, user: provider.publicKey,
}).rpc();
console.log("Play", tx2);
// 获取game account账户,并输出其数据
const gameAccount = await program.account.gameRecordAccount.fetch(gameAccountPda);
console.log("Records:", gameAccount.records);
});
});
```
### 本地测试
在anchor框架中进行本地测试,需要先执行以下步骤:
1. `anchor build`:编译solana合约。
2. `solana-keygen new`生成一个新的keypair,该keypair全局可用(不只是在当前项目)。
3. `solana-test-validator --reset`启动本地验证者,并且重置账本状态。
4. `solana airdrop <sol数量> <用户pubkey>`获取sol余额(在部署合约时需要使用)。
5. `anchor test --skip-local-validator`跳过开启本地验证器的过程进行测试。