
Как писать программы Solana с помощью Steel
Содержание
Steel — легковесный модульный фреймворк для написания нативных программ Solana с минимумом шаблонного кода и максимальным контролем. Steel создан Hardhat Chad, автором Ore, для разработчиков, которым нужна производительность нативного Rust без ущерба для удобства разработки.
В этой статье вы узнаете:
- Что такое Steel и как он связан с Anchor и Pinocchio
- Как определять инструкции и структурировать проект Steel
- Как создать собственный токен SPL с помощью Steel
- Как тестировать программу с помощью
solana-program-test
Предварительные требования
Предполагается, что вы знакомы со следующим:
- Базовый синтаксис и инструментарий Rust
- Основы разработки на Solana (аккаунты, инструкции, программы)
- Работа с CLI (например, cargo, solana, curl)
Если вы умеете писать простые программы на Solana или Rust, то готовы работать со Steel.
Что такое Steel?
Steel — новый модульный фреймворк для создания программ на Solana. Он позволяет писать меньше шаблонного кода и навязывает меньше соглашений, чем Anchor.
Steel предлагает макросы и вспомогательные средства для межпрограммных вызовов (CPI), которые ускоряют разработку программ Solana почти как без фреймворка. Вы получаете производительность, близкую к нативной, и более удобную разработку.
Рассмотрим некоторые макросы и вспомогательные средства Steel.
Макросы Steel
В число макросов Steel входят:
account!
Макрос account! определяет типы Account в Steel и предоставляет им доступ к трейту AccountValidation. Этот трейт содержит вспомогательные средства для проверки состояния аккаунтов во время разработки.
instruction!
Макрос instruction! определяет типы Instruction в Steel и предоставляет им доступ к функции to_bytes, которая будет использоваться в api/src/sdk.
Среди других макросов Steel — error и event. Как следует из названий, они используются для ошибок и событий соответственно.
Вспомогательные средства CPI в Steel
Steel предоставляет вспомогательные функции для большинства межпрограммных вызовов (CPI), необходимых при разработке программ. Например, инструкции из system_program, включая create_account, transfer и другие.
Также доступны инструкции из spl_token_program / spl_associated_token_program, включая mint_to, burn, create_associated_token_account и другие.
Чтобы получить в Steel доступ к вспомогательным средствам CPI из spl_token_program и spl_associated_token_program, необходимо включить флаг функции spl.
Оптимизация CU
Можно предположить, что Steel эффективно расходует CU благодаря своим возможностям, но на самом деле его эффективность обусловлена тем, чего он не делает. Steel легковесен и почти не создает дополнительных расходов для программ Solana. Поэтому он столь же эффективен, как программы Solana на нативном Rust, а сериализатор данных bytemuck по умолчанию дополнительно повышает эффективность.
Steel и Anchor
Anchor — мощный фреймворк с заданными соглашениями для быстрого создания безопасных программ Solana. Он упрощает разработку: сокращает шаблонный код для (де)сериализации аккаунтов и данных инструкций, выполняет необходимые проверки безопасности, автоматически генерирует клиентские библиотеки и предоставляет развитую среду тестирования.
В чем основное различие между Steel и Anchor?
Anchor — удобный для начинающих фреймворк смарт-контрактов, который позволяет разработчикам Solana с любым уровнем опыта быстро писать программы. Anchor ориентирован на понятный и удобный процесс разработки, поэтому им пользуется так много разработчиков Solana.
Однако у этой простоты есть своя цена.
В Anchor накопились накладные расходы, из-за которых бинарные файлы программ Solana становятся объемнее, что отрицательно влияет на их производительность в сети. Например, это повышает стоимость развертывания программ Solana и вызова инструкций.
Благодаря скорости и эффективности Solana большинство пользователей не замечают накладных расходов Anchor. Исключение — разработчики более сложных программ, таких как Ore и Code-vm, для которых эти расходы сделали бы работу в сети невозможной.
Обычно такие программы Solana создают на нативном Rust. Но их разработчики понимают, насколько это сложно, и нуждаются в более удобном фреймворке, сопоставимом с Anchor по удобству и с нативным Rust по производительности.
Преимущества и компромиссы Steel и Anchor
Несмотря на накладные расходы, Anchor предлагает лучший опыт разработки в экосистеме Solana и остается рекомендуемым фреймворком для новых разработчиков Solana.
Синтаксис Anchor прост для понимания. Фреймворк также предоставляет языки описания интерфейсов (IDL), которые упрощают тестирование программ Solana на других языках, например JavaScript, и разработку клиентских приложений для взаимодействия с ними.
IDL Anchor настолько функциональны, что такие инструменты, как Codama, могут использовать их для автоматической генерации клиентов, интерфейсов командной строки (CLI) и документации программ Solana.
Сейчас фреймворк Steel не поддерживает IDL. Хотя его синтаксис удобен для разработчиков, Steel требует хорошего знания Rust.
Anchor рекомендуется начинающим, но может ограничивать более опытных разработчиков, поскольку скрывает внутренние механизмы разработки программ Solana за макросами и собственным синтаксисом.
Steel, напротив, предоставляет разработчику доступ ко всем элементам программы Solana на самом низком уровне. Такая детализация особенно полезна при тестировании: тесты по умолчанию пишутся на Rust, что позволяет отлаживать код напрямую.
Steel — отличный фреймворк смарт-контрактов благодаря тому, чем он является — минимальной оболочкой над нативным Rust, — и тому, чем не является: дополнительным синтаксисом, создающим накладные расходы.
Проще говоря, Steel — более удобная для разработчиков версия нативного Rust, которая сохраняет его возможности без ущерба для эффективности.
Steel и Pinocchio
Pinocchio — библиотека без зависимостей для создания программ Solana на Rust. Febo написал ее как сторонний проект, а позже она стала полноценным проектом Anza. Библиотека использует способ, которым загрузчики SVM сериализуют входные параметры программы в массив байтов. Затем этот массив передается в точку входа программы для определения типов с нулевым копированием при чтении входных данных.
Проще говоря, Pinocchio — более компактная версия solana_program без зависимостей от внешних крейтов и динамических типов.
После выпуска Pinocchio появилось много заблуждений о его назначении. Библиотека Pinocchio предназначена для замены библиотеки solana_program, а не для конкуренции с Anchor или Steel. Она дополняет эти фреймворки и делает их легче.
То, что большинство считает программой Pinocchio, — это просто код на нативном Rust, который зависит от pinocchio вместо solana_program.
Как создать токен с помощью Steel
Чтобы показать принцип работы Steel, мы напишем простую программу Solana для создания токена SPL. Если вы предпочитаете видео, посмотрите следующий ролик.
Предварительные требования
- 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). Так вы сможете запустить среду Linux на компьютере с Windows без двойной загрузки или отдельной виртуальной машины. В этом случае используйте инструкции по установке для Linux, то есть команду curl.
Разработчики могут заменить v2.2.15 тегом нужной версии или использовать названия каналов stable, beta либо edge.
После установки выполните solana –-version, чтобы убедиться, что установлена нужная версия solana.
Установка Steel
Steel можно установить с помощью Cargo, выполнив:
cargo install steel-cliСоздание проекта Steel
Чтобы создать проект Steel, достаточно выполнить:
// creates a new Steel project named `create-token`
steel new token
// enter directory
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 зависит от нее.
Удаление модулей 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 нужен только ID программы Metadata.
В последнем блоке кода мы возвращаем тип 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, соответствующий инструкции нашей программы.
Чтобы получить program_id из api/src/lib.rs, используйте:
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, необходимых нашей программе.
Чтобы добавить недостающие крейты, вставьте в файл /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 ошибки зависимостей должны исчезнуть.
Однако мы удалили из папки api код, от которого зависит папка program, поэтому продолжим получать такие ошибки:
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)?,
}Получив instruction(ix) из parse_instruction, мы используем match для выбора вызываемой инструкции. Здесь только одна ветвь сопоставления, поскольку в программе одна инструкция.
Теперь логика программы настроена для вызова нужной инструкции. Однако process_create и модуль create еще не существуют. Создадим их.
Выполните в терминале:
// 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
Это принятое в 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 предоставляет простые вспомогательные средства для цепочной проверки аккаунтов.
Затем создаем аккаунт mint с помощью create_account:
// 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. Следующие три строки преобразуют байтовые поля Create в строки с помощью bytes_to_string.
Обратите внимание, что bytes_to_string принимает константный обобщенный параметр, то есть ::<32>. Он помогает создать строку точной длины и сэкономить вычислительные единицы.
// initialize mint
initialize_mint(
mint_info,
user_info,
Some(user_info),
token_program,
rent_sysvar,
decimals,
)?;Затем инициализируем аккаунт mint с помощью функции 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()?;Здесь мы создали аккаунт 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
По умолчанию тесты 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 - Запускаем экземпляр
ProgramTestметодомstart, который возвращает кортеж (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 мы реализовали функцию, которую не использовали в логике программы: функцию create в api/src/sdk.rs?
Именно ее мы сначала вызываем в блоке кода ниже, чтобы создать экземпляр Instruction. Мы передаем его экземпляру Transaction с помощью Transaction::new_signed_with_payer, а затем передаем транзакцию в 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, но для простоты остановимся здесь.
Вы можете добавить их самостоятельно. Если понадобится помощь, изучите это руководство для разработчиков Solana по тестам Steel.
Файл тестов готов. Выполним команду тестирования — steel test.
К сожалению, тест завершится ошибкой, поскольку у нас нет исходного кода или ELF-файла программы mpl_token_metadata.
Это можно исправить следующей командой:
// 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, сравнили его с Anchor и Pinocchio и пошагово создали новый токен с помощью Steel.
Дополнительные ресурсы
Чтобы узнать больше о Steel и разработке программ Solana, изучите следующие ресурсы:
- Репозиторий Steel на GitHub
- Учебный курс по разработке на Solana (GitHub)
- Учебный курс по разработке на Solana (видео)
- Blueshift — научитесь писать собственные ончейн-программы
Похожие статьи
Подпишитесь на Helius
Следите за новостями разработки на Solana и получайте уведомления о наших публикациях


