(五)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 模块包含 initialize 和 guess 两个子模块:
.
├── 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 框架提供的一个宏,用于定义自定义的错误码。这个宏的主要作用是生成一个枚举类型,并为每个枚举变体分配一个唯一的错误码。这样可以让你在智能合约中更方便地处理和返回错误信息。
主要用途
- 定义自定义错误:通过
#[error_code]宏,你可以定义自己的错误类型,每个错误类型都有一个唯一的错误码和可选的错误消息。 - 提高可读性和可维护性:使用自定义错误码可以使代码更清晰,更容易理解和维护。
- 提供详细的错误信息:每个自定义错误可以附带一个描述性的错误消息,帮助调试和用户理解错误原因。
详情请参考自定义错误
然后在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框架中是一个非常强大的工具,它可以自动实现对给定结构体数据的反序列化,并自动生成账户相关的操作。具体来说,这个派生宏具有以下几个主要功能:
-
自动反序列化:使用
#[derive(Accounts)]后,框架会自动生成代码来处理账户的反序列化工作。这意味着在获取账户时,开发者不再需要手动迭代账户列表并进行反序列化操作,大大简化了代码编写过程。 -
安全检查:该派生宏还会自动执行一系列安全检查,确保账户满足程序安全运行的要求。例如,它会检查账户的所有权、签名要求等,确保账户在使用前已经正确初始化并且具有正确的权限。
-
代码生成:
#[derive(Accounts)]还会生成必要的代码来管理账户的生命周期,包括账户的创建、更新和销毁等操作。
#[account(..)] 属性宏
#[account(..)] 属性宏是Anchor框架中的另一个重要特性,它提供了一种声明式的方式来指定账户的各种属性。通过使用这个属性宏,开发者可以轻松地定义账户的初始化、权限、存储空间、是否可变等属性,从而简化与Solana程序交互的代码。具体功能包括:
-
账户初始化:使用
init属性可以指定这是一个需要初始化的新账户。此时,需要提供payer(支付者)和space(存储空间大小)参数。例如:#[account( init, payer = payer, space = GuessingAccount::INIT_SPACE + 8, seeds = [b"guessing_account"], bump )] pub guessing_account: Account<'info, GuessingAccount>, -
账户权限:使用
signer属性可以标记账户为签名者,确保该账户必须签署交易。使用mut属性可以标记账户为可变的,允许在事务中修改账户数据。 -
存储空间:使用
space属性可以指定账户的存储空间大小(以字节为单位),确保账户有足够的空间来存储数据。 -
账户派生:使用
seeds和bump属性可以派生账户的公钥(PDA),确保公钥的唯一性和确定性。这对于需要特定公钥的应用场景非常有用。 -
账户关闭:使用
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框架能够正确地处理和调用程序中的指令。
主要作用
-
定义程序入口:
#[program]宏标记的模块是整个智能合约的入口点。在这个模块中,你可以定义处理不同指令的函数。
-
生成元数据:
- Anchor框架会自动生成必要的元数据,包括指令ID、账户约束等,这些元数据用于在Solana网络中正确地解析和执行指令。
-
处理指令:
- 在
#[program]标记的模块中,你可以定义多个处理不同指令的函数。每个函数对应一个特定的指令,处理相应的业务逻辑。
- 在
-
简化开发:
#[program]宏简化了智能合约的开发过程,使得开发者可以专注于业务逻辑的实现,而不需要关心底层的细节,如账户验证、指令解析等。
在上述代码中,initialize 和 guess 是两条定义在程序(智能合约)中的指令。
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框架有一个初步的认识。如果您觉得这个教程系列对您有帮助,欢迎收藏并继续关注我们,这样您可以第一时间获得最新的更新和更多的支持。感谢您的阅读!