本文介绍了Steel,一个轻量级的、模块化的框架,用于编写原生的Solana程序。通过使用Steel,开发者可以用最少的样板代码和最大的控制力来构建Solana程序,还对比了Steel与Anchor、Pinocchio的异同,并演示了如何使用Steel创建一个自定义的SPL代币,以及如何使用 solana-program-test 对程序进行测试。
15 分钟阅读
2025年7月24日
Steel 是一个轻量级、模块化的框架,用于编写原生 Solana 程序,它具有最少的样板代码和最大的控制权。Steel 由 Hardhat Chad(Ore的作者)构建,专为希望获得原生 Rust 性能,同时又不牺牲开发者体验的开发者而设计。
在本文中,你将学习:
solana-program-test
测试你的程序本指南假定你熟悉:
如果你可以轻松编写基本的 Solana 或 Rust 程序,那么你就可以开始使用 Steel 构建了。
Steel 是一个用于在 Solana 上构建程序的新型模块化框架,与 Anchor 相比,它让开发者可以使用更少的样板代码编写程序,并且更少预设。
Steel 提供了宏和跨程序调用(CPI)助手,可以帮助快速跟踪 Solana 程序的开发,使其具有类似原生的特性(没有框架),这意味着你将获得类似原生的性能和更好的开发者体验。
让我们来探索 Steel 提供的一些宏和助手。
Steel 提供的一些宏 包括:
account!
宏在 Steel 中定义 Account
类型,并使它们能够访问 AccountValidation
trait,该 trait 提供了在开发过程中验证账户状态的助手。
instruction!
宏在 Steel 中定义 Instruction
类型,并使它们能够访问 to_bytes
函数,该函数将在 api/src/sdk
中使用。
Steel 中的其他宏包括 error
和 event
;顾名思义,它们分别用于错误和事件。
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 feature flag。
你可能期望 Steel 在 CU 方面效率很高,因为它所做的事情——但实际上,它的效率来自于它 不 做的事情。由于 Steel 框架是轻量级的,并且几乎没有给 Solana 程序增加开销,因此它与用原生 Rust 编写的 Solana 程序一样优化,甚至更优,因为它默认使用 bytemuck
作为其数据序列化器。
Anchor 是一个有主张且强大的框架,旨在快速构建安全的 Solana 程序。它通过减少账户(反)序列化和指令数据等领域的样板代码、执行必要的安全检查、自动生成客户端库以及提供广泛的测试环境来简化开发过程。
Anchor 是一个对初学者友好的智能合约框架,它允许任何专业水平的 Solana 开发者快速编写 Solana 程序。Anchor 专注于直观友好的开发者体验,这也是为什么如此多的 Solana 开发者依赖它的原因。
然而,这种简单性是有代价的。
Anchor 积累了开销,使得 Solana 程序二进制文件更加臃肿,这对其链上性能产生了负面影响。例如,增加了部署 Solana 程序和调用指令的成本。
由于 Solana 的速度和效率,即使有 Anchor 给 Solana 程序增加的开销,大多数人也不会注意到它,只有少数开发更复杂的程序,比如 Ore 和 Code-vm,它们的开销会使它们在链上无法使用。
通常,像这样的 Solana 程序会用原生 Rust 构建,但它们的维护者明白这将是多么艰巨的任务,并且需要使用一个更友好的框架,可以与 Anchor 相媲美,并且像原生 Rust 一样具有高性能。
Anchor 即使给 Solana 程序增加了开销,仍然在 Solana 生态系统中拥有最佳的开发者体验,并且仍然是推荐给 Solana 新开发者的框架。
Anchor 的语法易于理解,并且它提供了接口定义语言(IDL),这使得使用其他语言(例如 JavaScript)测试 Solana 程序以及开发现与 Solana 程序通信的客户端应用程序变得容易。
Anchor IDL 是如此强大,以至于可以被 Codama 等工具使用,以自动生成 Solana 程序的客户端、命令行界面(CLI)和文档。
IDL 是 Steel 框架目前没有的功能,并且虽然它的语法对开发者友好,但 Steel 要求开发者对 Rust 有很好的熟悉程度。
虽然 Anchor 推荐给新开发者,但它可能会限制更资深的开发者,因为它在宏和语法中掩盖了 Solana 程序开发的内部工作原理。
另一方面,Steel 允许开发者在最原始的层面上访问 Solana 程序的所有内容。这种粒度级别在测试过程中特别有用,因为测试默认是用 Rust 编写的,提供了一对一的调试体验。
Steel 是一个伟大的智能合约框架,因为它是什么(即原生 Rust 周围的一个最小包装器)和它不是什么(即导致开销的额外语法)。
简而言之,Steel 是一个更友好的原生 Rust 版本,它保留了它的力量,同时又不损害它的效率。
Pinocchio 是一个零依赖的库,用于用 Rust 创建 Solana 程序。它是由 Febo 作为一个副项目编写的,后来成为了一个完整的 Anza 项目。它利用 SVM 加载器将程序输入参数序列化为字节数组的方式,然后将其传递给程序的入口点,以定义用于读取输入的零拷贝类型。
简而言之,Pinocchio 是一个更精简的 solana_program
版本,它不依赖任何外部 crate,并避免使用动态类型。
自从 Pinocchio 发布以来,人们对它是什么存在很多误解。Pinocchio 库 的目的是取代 solana_program
库——它不是 Anchor 或 Steel 的竞争对手。它补充了这些框架,因为它使它们更轻。
大多数人认为的 Pinocchio 程序只是依赖于 pinocchio
而不是 solana_program
的原生 Rust 代码。
为了演示 Steel 如何工作,我们将编写一个简单的 Solana 程序来创建 SPL token。如果你是视觉学习者,可以观看以下视频。
Perelyn
165 位订阅者
Perelyn
可以从 官方 Rust 网站 或通过 CLI 安装 Rust:
curl --proto '=https' --tlsv1.2 -sSf <https://sh.rustup.rs> | sh
Steel 还需要 Solana 工具套件。可以使用以下命令为 macOS 和 Linux 安装最新版本(即,在撰写本文时为 2.2.15):
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"
但是,强烈建议你使用 适用于 Linux 的 Windows 子系统(WSL)。这将使你能够在 Windows 计算机上运行 Linux 环境,而无需双启动或设置单独的虚拟机。如果选择此方法,请参考 Linux 的安装说明(即 curl 命令)。
开发者可以将 v2.2.15
替换为所需版本的发布标签来下载,或使用 stable
、beta
或 edge
通道名称。
安装完成后,运行 solana –-version
以确认已安装所需版本的 solana
。
我们可以通过运行以下命令,使用 Cargo 安装 Steel:
cargo install steel-cli
创建 Steel 项目就像运行以下命令一样简单:
// 创建一个名为 `create-token` 的新 Steel 项目
steel new token
// 进入目录
cd create-token
我们的 token
目录应如下所示:
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
文件夹依赖于它。
在 api
文件夹中,有一些模块我们不会用于我们的 create-token
项目,例如 state
、const
和 error
,所以让我们删除它们。
我们可以通过运行以下命令删除 Steel 模块:
## 你应该在 `create-token` 项目的根目录中
## 进入 api/src 目录
cd api/src
## 删除我们不需要的模块
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 设置程序 ID
declare_id!("z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35");
在 Steel 中,指令在 api/src/instructions.rs
中定义。Steel 程序的所有指令都定义在一个枚举中,每个指令都是一个结构体。
包含所有指令的枚举如下所示:
##[repr(u8)]
##[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)]
pub enum CreateTokenInstruction {
Initialize = 0,
Add = 1
}
而每个指令通常看起来像这样:
##[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,因为 token 的小数位数将适合一个字节。
在 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
中定义了这些数据。
我们需要存储我们 token 的元数据,我们将使用 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;
在此代码块中,我们尝试获取我们将要存储 token 元数据的 程序派生地址(PDA)。为了派生我们需要的地址,我们将需要以下种子:
"metadata".as_bytes()
)mpl_token_metadata::ID.as_ref()
)mint.as_ref()
)所有这些输入加在一起构成了种子,对于 Pubkey::find_program::address
的第二个参数,我们只需要元数据程序的程序 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_id
accounts
data
在此块中,我们声明一个 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 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 项目默认带有两个文件夹: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
来选择要调用的指令。这里只有一个 match arm,因为我们的程序只有一个指令。
现在,我们已经设置了我们的程序逻辑,以便在调用时调用正确的指令。但是,process_create
和 create
mod 尚不存在,所以让我们创建它们。
在终端中,运行:
// 你应该在你的项目的根目录中
// 进入 program/src 目录
cd program/src
// 删除 add.rs 和 initialize.rs
rm -rf add.rs initialize.rs
// 创建 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 {
// 加载账户。
let [user_info, mint_info, metadata_info, token_program, token_metadata_program, system_program, rent_sysvar] =
accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
};
// 验证
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)?;
// 创建 mint 账户
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;
// 初始化 mint
initialize_mint(
mint_info,
user_info,
Some(user_info),
token_program,
rent_sysvar,
decimals,
)?;
msg!("initialize mint");
// 创建元数据账户
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(())
}
让我们逐步了解这里发生的事情。
// 加载账户。
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
结尾这是一种在 Steel 中命名账户的固执己见的方式——你可以决定做不同的事情,因为它对程序没有实际影响。
接下来,此代码块验证我们的账户:
// 验证
user_info.is_signer()?; // user 是签名者
mint_info.is_empty()?.is_signer()?; // mint 是空的并且是签名者
metadata_info.is_empty()?.is_writable()?; // 元数据是空的并且是可写的
token_program.is_program(&spl_token::ID)?; // token 程序 == 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::ID
rent_sysvar.is_sysvar(&sysvar::rent::ID)?; // rent sysvar == sysvar::rent::ID
Steel 具有简单的助手 用于验证可链接的账户。
接下来,我们使用 create_account
助手函数创建 mint
账户:
// 创建 mint 账户
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>
),该参数有助于生成具有精确长度的字符串,以节省计算单元。
// 初始化 mint
initialize_mint(
mint_info,
user_info,
Some(user_info),
token_program,
rent_sysvar,
decimals,
)?;
接下来,我们使用 initialize_mint
助手函数初始化 mint
账户。
// 创建元数据账户
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()?;
在这里,我们为我们的 token mint 创建了 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
我们得到这个错误是因为 initialize_mint
辅助函数需要 Steel 中的 spl
特性才能访问,所以我们必须更新 /Cargo.toml
文件中的导入:
[workspace.dependencies]
...
...
steel = { version = "3.0", features = ["spl"] }
现在,如果我们运行 steel build
,我们的程序应该可以编译而没有错误。
恭喜你走到这一步!
最后一步:我们必须测试我们的程序。
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 token 程序默认情况下不属于 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
函数并为我们的 token mint 创建一个 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
实例,通过 Transaction::new_signed_with_payer
,然后我们将我们的 transaction 传递给 banks.process_transaction(tx).await;
进行处理。
assert!(res.is_ok());
确认 transaction 已被处理。
// 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
账户中的数据等其他内容,但为了简单起见,我们将在这里停止。
如果你有兴趣,你可以自己添加它们,如果你需要帮助,请查看这个 Solana 开发者指南,了解 steel 测试。
现在我们已经完成了我们的测试文件,让我们运行测试命令 - 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
// 在 program/tests 中创建一个名为 fixtures 的文件夹
// ProgramTest 将检查此文件夹中的 token metadata 的 ELF 文件
mkdir program/tests/fixtures
// dump the ELF file for the Metaplex metadata program in the fixtures folder
// 将 Metaplex metadata 程序的 ELF 文件转储到 fixtures 文件夹中
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 创建一个新 token。
要继续学习有关 Steel 和 Solana 程序开发的知识,请浏览以下资源:
- 原文链接: helius.dev/blog/steel...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!