(五)Solana 入门指南——猜数游戏

475 阅读9分钟

(五)Solana 入门指南——猜数游戏

在本节中,我们将探讨如何利用 Anchor 框架来构建一个简单的程序——猜数游戏。

1. 初始化项目

首先,我们需要创建一个新的项目。打开终端,运行以下命令来初始化一个名为 guessing_games 的新项目:

anchor init guessing_games

这条命令会基于 Anchor 框架生成一个基础项目结构,包括必要的配置文件和目录,为接下来的开发工作做好准备。

2. 新建 State 模块

项目初始化完成后,你的项目目录结构应该类似于下面的样子:

.
├── Anchor.toml
├── Cargo.lock
├── Cargo.toml
├── app
├── migrations
│   └── deploy.ts
├── package.json
├── programs
│   └── guessing_game
│       ├── Cargo.toml
│       ├── Xargo.toml
│       └── src
│           ├── lib.rs
│           └── state
│               ├── guessing_account.rs
│               └── mod.rs
├── tests
│   └── guessing_game.ts
├── tsconfig.json
└── yarn.lock

接下来,我们需要定义游戏的状态信息。在 state/guessing_account.rs 文件中添加以下代码:

use anchor_lang::prelude::*;

#[account]
#[derive(InitSpace)]
pub struct GuessingAccount {
    pub number: u8,
}

这里的 #[account] 属性标记了 GuessingAccount 结构体为账户类型。这意味着该结构体代表了一个区块链上的账户,可以用来存储游戏的状态(如本次游戏的目标数字)。InitSpace 派生宏则负责计算初始化这个账户时需要的空间大小,这对于优化存储成本和提高性能至关重要。

3. 编写指令

为了实现猜数游戏的核心功能,我们需要创建几个指令来处理不同的操作,例如初始化游戏和进行猜测。因此,我们将在项目中添加error模块和 instructions 模块,其中 instructions 模块包含 initializeguess 两个子模块:

.
├── Anchor.toml
├── Cargo.lock
├── Cargo.toml
├── app
├── migrations
│   └── deploy.ts
├── package.json
├── programs
│   └── guessing_game
│       ├── Cargo.toml
│       ├── Xargo.toml
│       └── src
│           ├── errors.rs
│           ├── instructions
│           │   ├── guess.rs
│           │   ├── initialize.rs
│           │   └── mod.rs
│           ├── lib.rs
│           └── state
│               ├── guessing_account.rs
│               └── mod.rs
├── tests
│   └── guessing_game.ts
├── tsconfig.json
└── yarn.lock
  • initialize 模块:负责设置游戏的初始状态,比如生成一个随机数字作为玩家需要猜测的目标。
  • guess 模块:接收玩家的猜测,并根据与目标数字的比较结果返回相应的反馈,如“太高”、“太低”或“恭喜,猜对了”。

error.rs中添加下面代码

use anchor_lang::prelude::*;

#[error_code]
pub enum MyError {
    #[msg("Number is too high")]
    NumberTooHigh,

    #[msg("Number is too low")]
    NumberTooLow,
}

这里#[error_code] 是 Anchor 框架提供的一个宏,用于定义自定义的错误码。这个宏的主要作用是生成一个枚举类型,并为每个枚举变体分配一个唯一的错误码。这样可以让你在智能合约中更方便地处理和返回错误信息。

主要用途

  1. 定义自定义错误:通过 #[error_code] 宏,你可以定义自己的错误类型,每个错误类型都有一个唯一的错误码和可选的错误消息。
  2. 提高可读性和可维护性:使用自定义错误码可以使代码更清晰,更容易理解和维护。
  3. 提供详细的错误信息:每个自定义错误可以附带一个描述性的错误消息,帮助调试和用户理解错误原因。

详情请参考自定义错误

然后在instructions/initialize.rs中添加下面代码。

use anchor_lang::prelude::*;

// 引用本项目中定义的状态结构体
use crate::state::guessing_account::GuessingAccount;

#[derive(Accounts)]
pub struct InitializeContext<'info> {
    // 标记为可变的账户,表示此账户的数据可以在事务中被修改
    #[account(mut)]
    pub payer: Signer<'info>, // 账户所有者,即支付交易费用的人

    // 引用系统程序,用于创建新账户
    pub system_program: Program<'info, System>,

    // 初始化一个新的GuessingAccount实例
    #[account(
        init,              // 表示这是一个需要初始化的新账户
        payer = payer,     // 指定谁支付创建账户所需的Lamports
        space = GuessingAccount::INIT_SPACE + 8, // 分配存储空间大小
        seeds = [b"guessing_account"], // 使用种子来确定账户的公钥
        bump              // 自动生成或使用提供的nonce值以确保账户公钥唯一
    )]
    pub guessing_account: Account<'info, GuessingAccount>, // 新创建的账户
}

// 实现InitializeContext结构体的方法
impl<'info> InitializeContext<'info> {
    // 生成随机数的辅助函数
    fn get_random_number() -> u8 {
        // 获取当前时钟信息
        let clock = Clock::get().expect("Failed to get clock");
        // 计算Unix时间戳的最后一位数字,并加1以避免0
        let last_digit = (clock.unix_timestamp % 10) as u8;
        last_digit + 1
    }

    // 初始化方法,设置GuessingAccount的初始状态
    pub fn initialize(&mut self) -> Result<()> {
        // 通过调用get_random_number方法生成一个随机数
        let random_number = Self::get_random_number();
        // 将生成的随机数赋值给guessing_account的number字段
        self.guessing_account.number = random_number;
        // 返回Ok结果,表示操作成功完成
        Ok(())
    }
}

#[derive(Accounts)] 派生宏

#[derive(Accounts)] 派生宏在Anchor框架中是一个非常强大的工具,它可以自动实现对给定结构体数据的反序列化,并自动生成账户相关的操作。具体来说,这个派生宏具有以下几个主要功能:

  1. 自动反序列化:使用 #[derive(Accounts)] 后,框架会自动生成代码来处理账户的反序列化工作。这意味着在获取账户时,开发者不再需要手动迭代账户列表并进行反序列化操作,大大简化了代码编写过程。

  2. 安全检查:该派生宏还会自动执行一系列安全检查,确保账户满足程序安全运行的要求。例如,它会检查账户的所有权、签名要求等,确保账户在使用前已经正确初始化并且具有正确的权限。

  3. 代码生成#[derive(Accounts)] 还会生成必要的代码来管理账户的生命周期,包括账户的创建、更新和销毁等操作。

#[account(..)] 属性宏

#[account(..)] 属性宏是Anchor框架中的另一个重要特性,它提供了一种声明式的方式来指定账户的各种属性。通过使用这个属性宏,开发者可以轻松地定义账户的初始化、权限、存储空间、是否可变等属性,从而简化与Solana程序交互的代码。具体功能包括:

  1. 账户初始化:使用 init 属性可以指定这是一个需要初始化的新账户。此时,需要提供 payer(支付者)和 space(存储空间大小)参数。例如:

    #[account(
        init,
        payer = payer,
        space = GuessingAccount::INIT_SPACE + 8,
        seeds = [b"guessing_account"],
        bump
    )]
    pub guessing_account: Account<'info, GuessingAccount>,
    
  2. 账户权限:使用 signer 属性可以标记账户为签名者,确保该账户必须签署交易。使用 mut 属性可以标记账户为可变的,允许在事务中修改账户数据。

  3. 存储空间:使用 space 属性可以指定账户的存储空间大小(以字节为单位),确保账户有足够的空间来存储数据。

  4. 账户派生:使用 seedsbump 属性可以派生账户的公钥(PDA),确保公钥的唯一性和确定性。这对于需要特定公钥的应用场景非常有用。

  5. 账户关闭:使用 close 属性可以关闭账户,将账户中的Lamports转移给另一个账户,并删除账户数据。

详细内容请参考Account Constraints

然后在instructions/guess.rs中添加下面代码。

use std::cmp::Ordering;

use anchor_lang::prelude::*;

use crate::{errors::MyError, state::guessing_account::GuessingAccount};

#[derive(Accounts)]
pub struct GuessContext<'info> {
    pub guessing_account: Account<'info, GuessingAccount>,
}

impl<'info> GuessContext<'info> {
    pub fn guess(&self, guess: u8) -> Result<()> {
        match guess.cmp(&self.guessing_account.number) {
            Ordering::Less => err!(MyError::NumberTooLow),
            Ordering::Equal => Ok(()),
            Ordering::Greater => err!(MyError::NumberTooHigh),
        }
    }
}

这里使用Anchor框架提供的err!宏来真正抛出错误。

4. 导出使用

instructions/mod.rs 文件中添加以下代码:

mod guess;
mod initialize;

pub use guess::*;
pub use initialize::*;

请注意,必须对每个子模块使用 pub use,因为 #[derive(Accounts)] 宏会生成额外的代码,这些代码也需要一并导出。

lib.rs中添加下面代码

use anchor_lang::prelude::*;

// 声明程序的公钥
declare_id!("8LxRyKYi3YYLgNkFAVykxuqhZQ2aruaxn548asQiyLpw");

// 引入指令、状态和错误模块
mod instructions;
mod state;
mod errors;

// 使用指令模块中的内容
use instructions::*;
use errors::*;

#[program]
pub mod guessing_game {
    use super::*;

    // 初始化函数
    pub fn initialize(ctx: Context<InitializeContext>) -> Result<()> {
        msg!("Greetings from: {:?}", ctx.program_id);
        ctx.accounts.initialize()?;
        Ok(())
    }

    // 猜数字函数
    pub fn guess(ctx: Context<GuessContext>, guess: u8) -> Result<()> {
        ctx.accounts.guess(guess)
    }
}

#[program] 宏介绍

在Anchor框架中,#[program] 是一个非常重要的宏,用于程序(定义智能合约)的入口模块。这个宏的主要作用是将一个Rust模块标记为Solana程序的主模块,并生成必要的元数据和代码,以便Anchor框架能够正确地处理和调用程序中的指令。

主要作用

  1. 定义程序入口

    • #[program] 宏标记的模块是整个智能合约的入口点。在这个模块中,你可以定义处理不同指令的函数。
  2. 生成元数据

    • Anchor框架会自动生成必要的元数据,包括指令ID、账户约束等,这些元数据用于在Solana网络中正确地解析和执行指令。
  3. 处理指令

    • #[program] 标记的模块中,你可以定义多个处理不同指令的函数。每个函数对应一个特定的指令,处理相应的业务逻辑。
  4. 简化开发

    • #[program] 宏简化了智能合约的开发过程,使得开发者可以专注于业务逻辑的实现,而不需要关心底层的细节,如账户验证、指令解析等。

在上述代码中,initializeguess 是两条定义在程序(智能合约)中的指令。

  • initialize 函数处理初始化指令,负责设置初始状态或创建必要的账户。
  • guess 函数处理猜数字指令,负责处理用户的猜测并返回相应的结果。

5. 编写测试

首先,执行构建命令以生成接口描述语言(IDL)文件:

anchor build

接下来,在 test/guessing_game.ts 文件中添加以下代码:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { GuessingGame } from "../target/types/guessing_game";
import { PublicKey } from "@solana/web3.js";

describe("guessing_game", () => {
  // 配置客户端使用本地集群
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);

  // 获取支付者的钱包
  const payer = provider.wallet;

  // 获取智能合约的实例
  const program = anchor.workspace.GuessingGame as Program<GuessingGame>;

  // 计算 guessing_account 的程序派生地址(PDA)
  const [pda] = PublicKey.findProgramAddressSync(
    [Buffer.from("guessing_account")],
    program.programId
  );

  // 测试初始化功能
  it("初始化", async () => {
    // 调用 initialize 方法初始化智能合约
    await program.methods
      .initialize()
      .accounts({
        payer: payer.publicKey, // 指定支付者账户
      })
      .rpc();

    // 获取并打印初始化后的 guessing_account 状态
    const currentCount = await program.account.guessingAccount.fetch(pda);
    console.log(currentCount);
  });

  // 测试猜数字功能(第一次尝试)
  it("猜数字1", async () => {
    try {
      // 调用 guess 方法尝试猜数字6
      await program.methods
        .guess(6)
        .accounts({
          guessingAccount: pda, // 指定 guessing_account 的 PDA 地址
        })
        .rpc();
    } catch (error) {
      // 捕获并打印任何错误
      console.warn(`${error.error.errorMessage}`);
    }
  });

  // 测试猜数字功能(第二次尝试)
  it("猜数字2", async () => {
    try {
      // 调用 guess 方法尝试猜数字8
      await program.methods
        .guess(8)
        .accounts({
          guessingAccount: pda, // 指定 guessing_account 的 PDA 地址
        })
        .rpc();
    } catch (error) {
      // 捕获并打印任何错误
      console.warn(`${error.error.errorMessage}`);
    }
  });

  // 测试猜数字功能(第三次尝试)
  it("猜数字3", async () => {
    try {
      // 调用 guess 方法尝试猜数字9
      await program.methods
        .guess(9)
        .accounts({
          guessingAccount: pda, // 指定 guessing_account 的 PDA 地址
        })
        .rpc();
    } catch (error) {
      // 捕获并打印任何错误
      console.warn(`${error.error.errorMessage}`);
    }
  });
});

完成以上步骤后,运行测试命令来执行这些测试案例:

anchor test

注意:此智能合约在初始化时只会生成一个随机数,并且“猜数字”账户也仅有一个。任何用户都可以无限制地调用 guess 函数来尝试猜测数字。

最后

通过这一节的学习,希望能帮助您对Anchor框架有一个初步的认识。如果您觉得这个教程系列对您有帮助,欢迎收藏并继续关注我们,这样您可以第一时间获得最新的更新和更多的支持。感谢您的阅读!