
如何使用 Steel 编写 Solana 程序
目录
Steel 是一个轻量级、模块化的框架,用于以最少的样板代码和最大的控制力编写原生 Solana 程序。Steel 由 Ore 的 Hardhat Chad 构建,面向既希望获得原生 Rust 性能,又不想牺牲开发者体验的开发者。
本文将介绍:
- Steel 是什么,以及它与 Anchor 和 Pinocchio 的关系
- 如何定义指令并组织 Steel 项目结构
- 如何使用 Steel 创建自定义 SPL 代币
- 如何使用
solana-program-test测试程序
前置条件
本指南假设你熟悉:
- 基本的 Rust 语法和工具链
- Solana 开发基础(账户、指令、程序)
- CLI 用法(例如 cargo、solana、curl)
如果你能编写基础的 Solana 或 Rust 程序,就可以开始使用 Steel 构建项目。
什么是 Steel?
Steel 是一个用于在 Solana 上构建程序的新型模块化框架。与 Anchor 相比,它的样板代码更少,约束也更少。
Steel 提供宏和跨程序调用(CPI)辅助函数,帮助开发者以接近原生开发(不使用框架)的方式快速构建 Solana 程序。这意味着你可以在改善开发者体验的同时,获得接近原生的性能。
下面来看看 Steel 提供的一些宏和辅助函数。
Steel 宏
Steel 提供的宏包括:
account!
account! 宏在 Steel 中定义 Account 类型,并让这些类型可以使用 AccountValidation trait。该 trait 提供了用于在开发期间验证账户状态的辅助函数。
instruction!
instruction! 宏在 Steel 中定义 Instruction 类型,并让这些类型可以使用 to_bytes 函数,该函数会在 api/src/sdk 中使用。
Steel 中的其他宏包括 error 和 event;顾名思义,它们分别用于错误和事件。
Steel CPI 辅助函数
Steel 提供了开发者在开发程序时进行大多数跨程序调用(CPI)所需的辅助函数,例如来自 system_program 的指令,包括 create_account、transfer 等。
它还包括来自 spl_token_program / spl_associated_token_program 的指令,其中包括 mint_to、burn、create_associated_token_account 等。
要在 Steel 中使用来自 spl_token_program 和 spl_associated_token_program 的 CPI 辅助函数,你需要启用 spl 功能标志。
CU 优化
你可能认为 Steel 的 CU 效率高是因为它做了什么,但实际上是因为它没有做什么。Steel 框架非常轻量,几乎不会给 Solana 程序增加额外开销,因此它与使用原生 Rust 编写的 Solana 程序一样高效。并且,由于默认使用 bytemuck 作为数据序列化器,其效率甚至更高。
Steel 与 Anchor 对比
Anchor 是一个功能强大且约定明确的框架,旨在快速构建安全的 Solana 程序。它减少了账户序列化与反序列化、指令数据等方面的样板代码,并执行必要的安全检查、自动生成客户端库以及提供完整的测试环境,从而简化开发流程。
Steel 和 Anchor 的主要区别是什么?
Anchor 是一个适合初学者的智能合约框架,让任何经验水平的 Solana 开发者都能快速编写 Solana 程序。Anchor 注重直观、友好的开发者体验,因此许多 Solana 开发者都依赖它。
然而,这种简单易用也有代价。
Anchor 积累的额外开销会使 Solana 程序二进制文件更加臃肿,进而影响链上性能。例如,它会增加部署 Solana 程序和调用指令的成本。
得益于 Solana 的速度和效率,即使 Anchor 为 Solana 程序增加了额外开销,大多数人也不会注意到。只有少数开发 Ore 和 Code-vm 等复杂程序的开发者会受到影响,因为这些开销可能让程序无法在链上使用。
通常,这类 Solana 程序会使用原生 Rust 构建。但维护者知道这样做难度很大,因此需要一个像 Anchor 一样易用、同时又具备原生 Rust 高性能的框架。
Steel 与 Anchor 的优势和取舍
尽管 Anchor 会给 Solana 程序增加额外开销,但它仍然拥有 Solana 生态中最好的开发者体验,也仍是推荐给 Solana 新开发者的框架。
Anchor 的语法易于理解,并提供接口定义语言(IDL),让开发者可以轻松使用 JavaScript 等其他语言测试 Solana 程序,还能开发与 Solana 程序通信的客户端应用。
Anchor IDL 功能强大,可以配合 Codama 等工具,自动为 Solana 程序生成客户端、命令行界面(CLI)和文档。
Steel 框架目前还不支持 IDL。虽然它的语法对开发者友好,但开发者仍需充分熟悉 Rust。
虽然 Anchor 推荐给新开发者使用,但它通过宏和自身语法隐藏了 Solana 程序开发的内部机制,因此可能会限制技术水平更高的开发者。
相比之下,Steel 让开发者可以在最底层访问 Solana 程序的全部内容。这种细粒度控制在测试过程中尤其有用,因为测试默认使用 Rust 编写,可以提供一一对应的调试体验。
Steel 是一个出色的智能合约框架,既因为它是什么(即原生 Rust 的最小封装),也因为它不是什么(即不会引入造成额外开销的附加语法)。
简单来说,Steel 是对开发者更友好的原生 Rust。它保留了原生 Rust 的能力,同时不牺牲效率。
Steel 与 Pinocchio 对比
Pinocchio 是一个使用 Rust 创建 Solana 程序的零依赖库。它最初由 Febo 作为业余项目编写,后来成为正式的 Anza 项目。它利用 SVM 加载器将程序输入参数序列化为字节数组的方式,并将该数组传递给程序入口点,从而定义用于读取输入的零拷贝类型。
简单来说,Pinocchio 是 solana_program 的精简版本,不依赖任何外部 crate,也避免使用动态类型。
自 Pinocchio 发布以来,人们对它存在许多误解。Pinocchio 库旨在取代 solana_program 库,并不是 Anchor 或 Steel 的竞争对手。它能让这些框架更加轻量,因此与这些框架形成互补。
大多数人所说的 Pinocchio 程序,其实只是依赖 pinocchio 而非 solana_program 的原生 Rust 代码。
如何使用 Steel 创建代币
为了演示 Steel 的工作原理,我们将编写一个创建 SPL 代币的简单 Solana 程序。如果你更喜欢通过视频学习,可以观看以下视频。
前置条件
- Rust/Cargo
- Solana
- Steel
安装 Rust
可以从 Rust 官方网站安装 Rust,也可以通过 CLI 安装:
curl --proto '=https' --tlsv1.2 -sSf <https://sh.rustup.rs> | sh安装 Solana 工具套件
Steel 还需要 Solana 工具套件。撰写本文时的最新版本(即 2.2.15)可以在 macOS 和 Linux 上使用以下命令安装:
sh -c "$(curl -sSfL <https://release.anza.xyz/v2.2.14/install>)"Windows 用户可以使用以下命令安装 Solana 工具套件:
cmd /c "curl <https://release.anza.xyz/v2.2.14/agave-install-init-x86_64-pc-windows-msvc.exe> --output C:\\agave-install-tmp\\agave-install-init.exe --create-dirs"但强烈建议改用 Windows Subsystem for Linux(WSL)。这样无需双系统启动或设置单独的虚拟机,就能在 Windows 计算机上运行 Linux 环境。采用这种方式时,请参考前面的 Linux 安装说明(即 curl 命令)。
开发者可以将 v2.2.15 替换为所需版本的发布标签进行下载,也可以使用 stable、beta 或 edge 渠道名称。
安装完成后,运行 solana –-version,确认已安装所需版本的 solana。
安装 Steel
可以运行以下命令,通过 Cargo 安装 Steel:
cargo install steel-cli创建 Steel 项目
只需运行以下命令即可创建 Steel 项目:
// creates a new Steel project named `create-token`
steel new token
// enter directory
cd create-tokentoken 目录应如下所示:
Cargo.toml (workspace)
⌙ api
⌙ Cargo.toml
⌙ src
⌙ consts.rs
⌙ error.rs
⌙ instruction.rs
⌙ lib.rs
⌙ sdk.rs
⌙ state
⌙ mod.rs
⌙ account_1.rs
⌙ account_2.rs
⌙ program
⌙ Cargo.toml
⌙ src
⌙ lib.rs
⌙ instruction_1.rs
⌙ instruction_2.rs
Steel 项目的默认结构包含两个文件夹,分别名为 api 和 program。
api 包含 state、errors 等类型,我们会在实现 Solana 程序时使用这些类型。program 文件夹则包含程序逻辑。
使用 Steel 开发程序时,最好从 api 文件夹开始,因为 program 文件夹依赖它。
移除 state、const 和 error 模块
api 文件夹中有一些模块不会用于我们的 create-token 项目,例如 state、const 和 error,因此我们将它们移除。
可以运行以下命令移除 Steel 模块:
# you should be at the root of the `create-token` project
# enter the api/src directory
cd api/src
# delete the modules we don't need
rm -rf state [consts.rs](<http://consts.rs/>) [error.rs](<http://error.rs/>)删除这些模块后,必须更新 api/src/lib.rs 文件,因为它调用了这些模块。
更新 api/src/lib.rs,使其如下所示:
pub mod instruction;
pub mod sdk;
pub mod prelude {
pub use crate::instruction::*;
pub use crate::sdk::*;
}
use steel::*;
// TODO Set program id
declare_id!("z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35");在 Steel 中定义指令
在 Steel 中,指令在 api/src/instructions.rs 中定义。Steel 程序的所有指令都定义在一个枚举中,每条指令则是一个结构体。
包含所有指令的枚举如下所示:
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)]
pub enum CreateTokenInstruction {
Initialize = 0,
Add = 1
}
While each instruction typically looks like this:
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct Initialize {}
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct Add {
pub amount: [u8; 8]
}如果一条指令不需要参数,例如 Initialize,它就没有字段。
需要数据的指令使用字节表示。例如,Add::amount is [u8; 8] 会映射到 u64。
定义指令枚举和指令结构体后,必须将它们传入 instruction! 宏。第一个参数是指令枚举,第二个参数是指令结构体:
instruction!(CreateTokenInstruction, Initialize);
instruction!(CreateTokenInstruction, Add);我们的 create-token 程序有一条指令,该指令接受四个参数,因此 api/src/instructions 应如下所示:
use steel::*;
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)]
pub enum CreateTokenInstruction {
Create = 0,
}
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct Create {
pub name: [u8; 32],
pub symbol: [u8; 8],
pub uri: [u8; 128],
pub decimals: u8,
}
instruction!(CreateTokenInstruction, Create);在 Create 中,name、symbol 和 uri 字段是以固定大小字节数组表示的字符串:
name:[u8; 16] — 用于不超过 16 字节的名称symbol:[u8; 8] — 符号通常较短uri:[u8; 128] — URI 通常较长
这些大小取决于预期的最大字节长度,而不是字符数(例如,多字节 UTF-8 字符可能需要更多空间)。
decimals 只是一个 u8,因为代币的小数位数可以存入一个字节。
更新 SDK
在 api/src 中,有一个名为 sdk.rs 的文件。实现程序逻辑时不会使用它,但运行测试或 Rust 客户端代码时会用到它。该文件包含用于分别构建 Steel 程序中所有指令的函数。由于这个程序只有一条指令,因此只需要一个 SDK 函数,api/src/sdk.rs 应如下所示:
use steel::*;
use crate::prelude::*;
pub fn create(
user: Pubkey,
mint: Pubkey,
name: [u8; 32],
symbol: [u8; 8],
uri: [u8; 128],
decimals: u8,
) -> Instruction {
let metadata = Pubkey::find_program_address(
&[
"metadata".as_bytes(),
mpl_token_metadata::ID.as_ref(),
mint.as_ref(),
],
&mpl_token_metadata::ID,
)
.0;
Instruction {
program_id: crate::ID,
accounts: vec![
AccountMeta::new(user, true),
AccountMeta::new(mint, true),
AccountMeta::new(metadata, false),
AccountMeta::new_readonly(spl_token::ID, false),
AccountMeta::new_readonly(mpl_token_metadata::ID, false),
AccountMeta::new_readonly(system_program::ID, false),
AccountMeta::new_readonly(sysvar::rent::ID, false),
],
data: Create {
name,
symbol,
uri,
decimals,
}
.to_bytes(),
}
}我们有一个名为 create 的函数,它接受五个参数:user 是调用该指令的账户公钥,mint 是代表 token mint 的账户公钥,而 name、symbol、uri 和 decimals 都是实现程序逻辑时使用的数据,这些数据已在 api/src/instructions::Create 中定义。
我们需要存储代币的元数据,并将使用 Metaplex Metadata 程序完成此操作。首先添加:
let metadata = Pubkey::find_program_address(
&[
"metadata".as_bytes(),
mpl_token_metadata::ID.as_ref(),
mint.as_ref(),
],
&mpl_token_metadata::ID,
)
.0;在这个代码块中,我们尝试获取用于存储代币元数据的程序派生地址(PDA)。要派生所需地址,需要使用以下种子:
- 字符串“metadata”的字节形式(即
"metadata".as_bytes()) - 元数据程序 ID 的切片形式(即
mpl_token_metadata::ID.as_ref()) - 铸币账户公钥的切片形式(即
mint.as_ref())
所有这些输入共同组成种子。对于 Pubkey::find_program::address 的第二个参数,只需传入 Metadata 程序的程序 ID。
在最后一个代码块中,我们返回表示该指令的 Instruction 类型。
Instruction 类型如下所示:
Instruction {
program_id: crate::ID,
accounts: vec![
AccountMeta::new(user, true),
AccountMeta::new(mint, true),
AccountMeta::new(metadata, false),
AccountMeta::new_readonly(spl_token::ID, false),
AccountMeta::new_readonly(mpl_token_metadata::ID, false),
AccountMeta::new_readonly(system_program::ID, false),
AccountMeta::new_readonly(sysvar::rent::ID, false),
],
data: Create {
name,
symbol,
uri,
decimals,
}
.to_bytes(),
} Instruction 类型是一个包含三个字段的结构体:
program_idaccountsdata
在这个代码块中,我们声明一个符合程序指令要求的 Instruction 实例。
要从 api/src/lib.rs 获取 program_id,请使用:
program_id: crate::ID accounts 字段是账户元数据(即 Vec<AccountMeta>)的向量,因此必须声明该指令将使用的所有账户:
accounts: vec![
AccountMeta::new(user, true),
AccountMeta::new(mint, true),
AccountMeta::new(metadata, false),
AccountMeta::new_readonly(spl_token::ID, false),
AccountMeta::new_readonly(mpl_token_metadata::ID, false),
AccountMeta::new_readonly(system_program::ID, false),
AccountMeta::new_readonly(sysvar::rent::ID, false),
],最后,data 字段以字节形式表示这些指令所使用的参数:
data: Create {
name,
symbol,
uri,
decimals,
}
.to_bytes(),至此,api 文件夹的工作就完成了。
接下来添加必要的依赖项,然后处理 program 文件夹。
添加 Steel 依赖项
此时,如果运行 steel build 编译程序,应该会出现以下错误:
error[E0433]: failed to resolve: use of undeclared crate or module `mpl_token_metadata`
--> api/src/sdk.rs:16:13
|
16 | mpl_token_metadata::ID.as_ref(),
| ^^^^^^^^^^^^^^^^^^ use of undeclared crate or module `mpl_token_metadata`
error[E0433]: failed to resolve: use of undeclared crate or module `mpl_token_metadata`
--> api/src/sdk.rs:19:10
|
19 | &mpl_token_metadata::ID,
| ^^^^^^^^^^^^^^^^^^ use of undeclared crate or module `mpl_token_metadata`
error[E0433]: failed to resolve: use of undeclared crate or module `spl_token`
--> api/src/sdk.rs:29:39
|
29 | AccountMeta::new_readonly(spl_token::ID, false),
| ^^^^^^^^^ use of undeclared crate or module `spl_token`
error[E0433]: failed to resolve: use of undeclared crate or module `mpl_token_metadata`
--> api/src/sdk.rs:30:39
|
30 | AccountMeta::new_readonly(mpl_token_metadata::ID, false),
| ^^^^^^^^^^^^^^^^^^ use of undeclared crate or module `mpl_token_metadata`
这表明缺少程序所需的 spl_token 和 mpl_token_metadata crate。
要添加缺少的 crate,请将以下内容添加到 /Cargo.toml 文件:
// /Cargo.toml
[workspace.dependencies]
...
...
mpl-token-metadata = "5.1.0"
spl-token = { version = "8.0.0", features = ["no-entrypoint"] }
In /api/Cargo.toml add:
// /api/Cargo.toml
[dependencies]
...
...
mpl-token-metadata.workspace = true
spl-token.workspace = true在 /api/Cargo.toml 中添加:
// /api/Cargo.toml
[dependencies]
...
...
mpl-token-metadata.workspace = true
spl-token.workspace = true现在运行 steel build,依赖项错误应该已经消失。
但是,由于我们删除了 program 文件夹所依赖的 api 文件夹中的代码,仍会看到如下错误:
error[E0599]: no variant or associated item named `Initialize` found for enum `create_token_api::instruction::CreateTokenInstruction` in the current scope
--> program/src/lib.rs:18:33
|
18 | CreateTokenInstruction::Initialize => process_initialize(accounts, data)?,
| ^^^^^^^^^^ variant or associated item not found in `CreateTokenInstruction`
error[E0599]: no variant or associated item named `Add` found for enum `create_token_api::instruction::CreateTokenInstruction` in the current scope
--> program/src/lib.rs:19:33
|
19 | CreateTokenInstruction::Add => process_add(accounts, data)?,
| ^^^ variant or associated item not found in `CreateTokenInstruction`
不用担心,我们将在下一节修复这些错误。
使用 Steel 实现程序逻辑
Steel 项目默认包含两个文件夹:api 和 program。我们刚刚在 api 文件夹中定义了程序所需的类型,现在必须在 program 文件夹中实现程序逻辑。
首先,使用以下内容更新 /program/lib.rs:
mod create;
use create::*;
use create_token_api::prelude::*;
use steel::*;
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
data: &[u8],
) -> ProgramResult {
let (ix, data) = parse_instruction(&create_token_api::ID, program_id, data)?;
match ix {
CreateTokenInstruction::Create => process_create(accounts, data)?,
}
Ok(())
}
entrypoint!(process_instruction);在这个文件中,我们定义主要的 process_instruction 函数,并将其传入 entrypoint! 宏。该宏会生成 Solana 运行时调用程序逻辑所需的样板代码。
process_instruction 函数中有两个重要的代码块需要说明。
let (ix, data) = parse_instruction(&create_token_api::ID, program_id, data)?;parse_instruction 从指令数据中解析指令。这意味着我们可以根据传给程序的数据确定要调用的指令。
在 Ok() 情况下,它会返回由 instruction(ix) 和 instruction data(data) 组成的元组。
match ix {
CreateTokenInstruction::Create => process_create(accounts, data)?,
}通过 parse_instruction 获取 instruction(ix) 后,我们使用 match 选择要调用的指令。这里只有一个匹配分支,因为程序只有一条指令。
现在,我们已经设置好程序逻辑,可以在调用时执行正确的指令。但是,process_create 和 create mod 尚不存在,因此需要创建它们。
在终端中运行:
// you should be at the root of your project
// enter the program/src directory
cd program/src
// delete add.rs and initialize.rs
rm -rf add.rs initialize.rs
// create create.rs
touch create.rs 现在使用以下内容更新 program/src/create.rs:
use create_token_api::prelude::*;
use solana_program::{msg, program_pack::Pack};
use steel::*;
pub fn process_create(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult {
// Load accounts.
let [user_info, mint_info, metadata_info, token_program, token_metadata_program, system_program, rent_sysvar] =
accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
};
// validate
user_info.is_signer()?;
mint_info.is_empty()?.is_signer()?;
metadata_info.is_empty()?.is_writable()?;
token_program.is_program(&spl_token::ID)?;
token_metadata_program.is_program(&mpl_token_metadata::ID)?;
system_program.is_program(&system_program::ID)?;
rent_sysvar.is_sysvar(&sysvar::rent::ID)?;
// create mint account
create_account(
user_info,
mint_info,
system_program,
spl_token::state::Mint::LEN,
&token_program.key,
)?;
msg!("create account");
let args = Create::try_from_bytes(data)?;
let name = bytes_to_string::<32>(&args.name)?;
let symbol = bytes_to_string::<8>(&args.symbol)?;
let uri = bytes_to_string::<128>(&args.uri)?;
let decimals = args.decimals;
// initialize mint
initialize_mint(
mint_info,
user_info,
Some(user_info),
token_program,
rent_sysvar,
decimals,
)?;
msg!("initialize mint");
// create metadata account
mpl_token_metadata::instructions::CreateMetadataAccountV3Cpi {
__program: token_metadata_program,
metadata: metadata_info,
mint: mint_info,
mint_authority: user_info,
payer: user_info,
update_authority: (user_info, true),
system_program,
rent: Some(rent_sysvar),
__args: mpl_token_metadata::instructions::CreateMetadataAccountV3InstructionArgs {
data: mpl_token_metadata::types::DataV2 {
name,
symbol,
uri,
seller_fee_basis_points: 0,
creators: None,
collection: None,
uses: None,
},
is_mutable: true,
collection_details: None,
},
}
.invoke()?;
msg!("metadata account created");
Ok(())
}下面逐步了解这里发生了什么。
// Load accounts.
let [user_info, mint_info, metadata_info, token_program, token_metadata_program, system_program, rent_sysvar] =
accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
};在这个代码块中,我们加载该指令所需的账户。如果传入的账户与定义的账户不匹配,该代码块会抛出 ProgramError::NotEnoughAccountKeys 错误。
仔细观察,你会发现账户命名遵循以下模式:
- “常规”账户以
info结尾 - 程序账户以
program结尾 - Sysvar 以
sysvar结尾
这是 Steel 对账户命名的一种约定。你也可以采用其他命名方式,因为它对程序没有实际影响。
接下来,这个代码块会验证账户:
// validate
user_info.is_signer()?; // user is a signer
mint_info.is_empty()?.is_signer()?; // mint is empty and is a signer
metadata_info.is_empty()?.is_writable()?; // metadata is empty and is writable
token_program.is_program(&spl_token::ID)?; // token program == spl_token::ID
token_metadata_program.is_program(&mpl_token_metadata::ID)?; // token meatadata == mpl_token_metadata::ID
system_program.is_program(&system_program::ID)?; // system program == system_program::ID
rent_sysvar.is_sysvar(&sysvar::rent::ID)?; // rent sysvar == sysvar::rent::IDSteel 提供了简单且可链式调用的账户验证辅助函数。
接下来,使用 create_account 辅助函数创建 mint 账户:
// create mint account
create_account(
user_info,
mint_info,
system_program,
spl_token::state::Mint::LEN,
&token_program.key,
)?;创建 mint 账户后,将指令数据从字节反序列化为 Rust 类型:
let args = Create::try_from_bytes(data)?;
let name = bytes_to_string::<32>(&args.name)?;
let symbol = bytes_to_string::<8>(&args.symbol)?;
let uri = bytes_to_string::<128>(&args.uri)?;
let decimals = args.decimals;第一行将类型为 &[u8] 的指令数据转换为 api/instructions.rs/Create,接下来的三行则使用 bytes_to_string 辅助函数,将 Create 中以字节表示的字段转换为字符串。
另外请注意,bytes_to_string 接受一个 const 泛型参数(即 ::<32>),它可以生成长度精确的字符串,从而节省计算单元。
// initialize mint
initialize_mint(
mint_info,
user_info,
Some(user_info),
token_program,
rent_sysvar,
decimals,
)?;接下来,使用 initialize_mint 辅助函数初始化 mint 账户。
// create metadata account
mpl_token_metadata::instructions::CreateMetadataAccountV3Cpi {
__program: token_metadata_program,
metadata: metadata_info,
mint: mint_info,
mint_authority: user_info,
payer: user_info,
update_authority: (user_info, true),
system_program,
rent: Some(rent_sysvar),
__args: mpl_token_metadata::instructions::CreateMetadataAccountV3InstructionArgs {
data: mpl_token_metadata::types::DataV2 {
name,
symbol,
uri,
seller_fee_basis_points: 0,
creators: None,
collection: None,
uses: None,
},
is_mutable: true,
collection_details: None,
},
}
.invoke()?;这里为代币铸币账户创建了 metadata 账户。它包含集合的名称、符号和创建者等信息。
完成 create.rs 文件后,运行 steel build。
此时应该会看到以下错误:
error[E0433]: failed to resolve: use of undeclared crate or module `spl_token`
--> program/src/create.rs:28:9
|
28 | spl_token::state::Mint::LEN,
| ^^^^^^^^^ use of undeclared crate or module `spl_token`
error[E0433]: failed to resolve: use of undeclared crate or module `mpl_token_metadata`
--> program/src/create.rs:69:5
|
69 | mpl_token_metadata::instructions::CreateMetadataAccountV3Cpi {
| ^^^^^^^^^^^^^^^^^^ use of undeclared crate or module `mpl_token_metadata`
error[E0433]: failed to resolve: use of undeclared crate or module `mpl_token_metadata`
--> program/src/create.rs:78:17
|
78 | __args: mpl_token_metadata::instructions::CreateMetadataAccountV3Instru...
| ^^^^^^^^^^^^^^^^^^ use of undeclared crate or module `mpl_token_metadata`
error[E0433]: failed to resolve: use of undeclared crate or module `mpl_token_metadata`
--> program/src/create.rs:79:19
|
79 | data: mpl_token_metadata::types::DataV2 {
| ^^^^^^^^^^^^^^^^^^ use of undeclared crate or module `mpl_token_metadata`这些错误表明存在依赖项问题。可以通过编辑 /program/Cargo.toml 文件来更新依赖项:
[dependencies]
...
...
mpl-token-metadata.workspace = true
spl-token.workspace = true现在再次运行 steel build,会遇到最后一个错误:
error[E0425]: cannot find function `initialize_mint` in this scope
--> program/src/create.rs:57:5
|
57 | initialize_mint(
| ^^^^^^^^^^^^^^^ not found in this scope出现此错误是因为必须启用 Steel 中的 spl 功能才能访问 initialize_mint 辅助函数,因此需要在 /Cargo.toml 文件中更新其导入:
[workspace.dependencies]
...
...
steel = { version = "3.0", features = ["spl"] }现在运行 steel build,程序应该可以正常编译,不再出现错误。
恭喜你完成到这里!
还有最后一步:我们必须测试程序。
测试 Steel 程序
Steel 中的测试默认使用 Rust 编写。Steel 使用 solana-program-test 进行测试,但你也可以根据偏好使用 liteSVM 或 mollusk。
测试编写在 /program/tests/test.rs 中。
首先,使用以下内容更新该文件:
use create_token_api::prelude::*;
use solana_program::hash::Hash;
use solana_program_test::{processor, BanksClient, ProgramTest};
use solana_sdk::{
program_pack::Pack, signature::Keypair, signer::Signer, transaction::Transaction,
};
use steel::*;
async fn setup() -> (BanksClient, Keypair, Hash) {
let mut program_test = ProgramTest::new(
"create_token_program",
create_token_api::ID,
processor!(create_token_program::process_instruction),
);
program_test.add_program("token_metadata", mpl_token_metadata::ID, None);
program_test.prefer_bpf(true);
program_test.start().await
}
#[tokio::test]
async fn run_test() {
// Setup test
let (mut banks, payer, blockhash) = setup().await;
let mint_keypair = Keypair::new();
let name = string_to_bytes::<32>("ANATOLY").unwrap();
let symbol = string_to_bytes::<8>("MERT").unwrap();
let uri = string_to_bytes::<128>("blah blah blah").unwrap();
let decimals = 9;
// Submit create transaction.
let ix = create(
payer.pubkey(),
mint_keypair.pubkey(),
name,
symbol,
uri,
decimals,
);
let tx = Transaction::new_signed_with_payer(
&[ix],
Some(&payer.pubkey()),
&[&payer, &mint_keypair],
blockhash,
);
let res = banks.process_transaction(tx).await;
assert!(res.is_ok());
let serialized_mint_data = banks
.get_account(mint_keypair.pubkey())
.await
.unwrap()
.unwrap()
.data;
let mint_data = spl_token::state::Mint::unpack(&serialized_mint_data).unwrap();
assert!(mint_data.is_initialized);
assert_eq!(mint_data.mint_authority.unwrap(), payer.pubkey());
assert_eq!(mint_data.decimals, decimals);
}
测试文件包含两个函数:setup 和 run_test。
在 setup 函数中,我们会执行三项重要操作:
- 创建
ProgramTest实例,其中默认添加了create_token_program程序 - 将
token_metadata程序添加到ProgramTest实例,因为我们使用的 Metaplex 代币程序默认不属于ProgramTest - 使用
start方法启动ProgramTest实例,该方法返回一个由(BanksClient、Keypair、Hash)组成的元组
async fn setup() -> (BanksClient, Keypair, Hash) {
let mut program_test = ProgramTest::new(
"create_token_program",
create_token_api::ID,
processor!(create_token_program::process_instruction),
);
program_test.add_program("token_metadata", mpl_token_metadata::ID, None);
program_test.prefer_bpf(true);
program_test.start().await
}在 run_test 的第一部分,我们调用 setup 函数,并为代币铸币账户创建 Keypair。
// Setup test
let (mut banks, payer, blockhash) = setup().await;
let mint_keypair = Keypair::new();接下来准备指令数据。
由于 create 指令需要字节表示,因此使用 string_to_bytes 辅助函数将字符串转换为字节。
let name = string_to_bytes::<32>("ANATOLY").unwrap();
let symbol = string_to_bytes::<8>("MERT").unwrap();
let uri = string_to_bytes::<128>("blah blah blah").unwrap();
let decimals = 9;还记得我们在 api 文件夹中实现了一个未在程序逻辑中使用的函数吗?也就是 api/src/sdk.rs 中的 create 函数。
这正是我们在下面的代码块中首先调用的函数。它会创建 Instruction 实例,我们使用 Transaction::new_signed_with_payer 将该实例传给 Transaction 实例,然后将交易传给 banks.process_transaction(tx).await; 进行处理。
assert!(res.is_ok()); 会确认交易已处理。
// Submit create transaction.
let ix = create(
payer.pubkey(),
mint_keypair.pubkey(),
name,
symbol,
uri,
decimals,
);
let tx = Transaction::new_signed_with_payer(
&[ix],
Some(&payer.pubkey()),
&[&payer, &mint_keypair],
blockhash,
);
let res = banks.process_transaction(tx).await;
assert!(res.is_ok());到目前为止,我们已经在测试环境(ProgramTest)中执行了指令。
现在测试它是否正确执行:
// get serialized data of mint account
let serialized_mint_data = banks
.get_account(mint_keypair.pubkey())
.await
.unwrap()
.unwrap()
.data;
// unpack the mint account data to get the SPL Mint information
let mint_data = spl_token::state::Mint::unpack(&serialized_mint_data).unwrap();
// check if the mint account was initilized
assert!(mint_data.is_initialized);
// check if the mint authority matches the one we set
assert_eq!(mint_data.mint_authority.unwrap(), payer.pubkey());
// check if the decimals match
assert_eq!(mint_data.decimals, decimals);我们还可以编写更多断言,检查存储在 metadata 账户中的数据等其他内容,但为了简单起见,这里就不再继续。
如果你愿意,可以自行添加。如果需要帮助,请查看这份 Steel 测试 Solana 开发者指南。
完成测试文件后,运行测试命令 steel test。
遗憾的是,测试会失败,因为我们没有 mpl_token_metadata 程序的源代码/ELF 文件。
不用担心,可以运行以下命令修复:
// you have to be at the root of your project
// create a folder called fixtures in program/tests
// ProgramTest is going to check this folder for the ELF file for token metadata
mkdir program/tests/fixtures
// dump the ELF file for the Metaplex metadata program in the fixtures folder
solana program dump metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s program/tests/fixtures/token_metadata.so现在运行 steel test,应该会得到以下结果:
running 1 test
[2025-06-08T14:46:12.240628000Z INFO solana_program_test] "create_token_program" SBF program from /Users/perelyn/helius/create-token/target/deploy/create_token_program.so, modified 3 seconds, 112 ms, 833 µs and 660 ns ago
[2025-06-08T14:46:12.242336000Z INFO solana_program_test] "token_metadata" SBF program from tests/fixtures/token_metadata.so, modified 1 minute, 49 seconds, 247 ms, 367 µs and 400 ns ago
[2025-06-08T14:46:12.381492000Z DEBUG solana_runtime::message_processor::stable_log] Program z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35 invoke [1]
[2025-06-08T14:46:12.382734000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 invoke [2]
[2025-06-08T14:46:12.383272000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 success
[2025-06-08T14:46:12.383298000Z DEBUG solana_runtime::message_processor::stable_log] Program log: create account
[2025-06-08T14:46:12.383562000Z DEBUG solana_runtime::message_processor::stable_log] Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]
[2025-06-08T14:46:12.383783000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Instruction: InitializeMint
[2025-06-08T14:46:12.386049000Z DEBUG solana_runtime::message_processor::stable_log] Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 2968 of 192320 compute units
[2025-06-08T14:46:12.386068000Z DEBUG solana_runtime::message_processor::stable_log] Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success
[2025-06-08T14:46:12.386099000Z DEBUG solana_runtime::message_processor::stable_log] Program log: initialize mint
[2025-06-08T14:46:12.386409000Z DEBUG solana_runtime::message_processor::stable_log] Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s invoke [2]
[2025-06-08T14:46:12.387342000Z DEBUG solana_runtime::message_processor::stable_log] Program log: IX: Create Metadata Accounts v3
[2025-06-08T14:46:12.387576000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 invoke [3]
[2025-06-08T14:46:12.387588000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 success
[2025-06-08T14:46:12.387999000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Allocate space for the account
[2025-06-08T14:46:12.388226000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 invoke [3]
[2025-06-08T14:46:12.388264000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 success
[2025-06-08T14:46:12.388306000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Assign the account to the owning program
[2025-06-08T14:46:12.388851000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 invoke [3]
[2025-06-08T14:46:12.388873000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 success
[2025-06-08T14:46:12.392769000Z DEBUG solana_runtime::message_processor::stable_log] Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s consumed 37330 of 185782 compute units
[2025-06-08T14:46:12.392790000Z DEBUG solana_runtime::message_processor::stable_log] Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s success
[2025-06-08T14:46:12.392842000Z DEBUG solana_runtime::message_processor::stable_log] Program log: metadata account created
[2025-06-08T14:46:12.395012000Z DEBUG solana_runtime::message_processor::stable_log] Program z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35 consumed 51973 of 200000 compute units
[2025-06-08T14:46:12.395031000Z DEBUG solana_runtime::message_processor::stable_log] Program z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35 success
test run_test ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.16s再次恭喜!
如果你得到相同的输出,说明程序已通过测试。
总结
Steel 是一个模块化、轻量级的开发框架,用于构建智能且经过性能优化的 Solana 程序。本文介绍了 Steel 的工作原理,将 Steel 与 Anchor 和 Pinocchio 进行了比较,并通过示例演示了如何使用 Steel 创建新代币。
其他资源
要继续学习 Steel 和 Solana 程序开发,请参阅以下资源:
- Steel GitHub 仓库
- Solana 开发训练营(GitHub)
- Solana 开发训练营(视频)
- Blueshift — 学习如何编写自己的链上程序
相关文章
订阅 Helius
及时了解 Solana 开发的最新动态,并在我们发布新内容时收到更新


