---
# System prepended metadata

title: anchor框架使用

---

# 概述

本文介绍使用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`跳过开启本地验证器的过程进行测试。

