# 概述 本文介绍使用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`: ![image](https://hackmd.io/_uploads/rkX8MaBskl.png) 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`跳过开启本地验证器的过程进行测试。