НОВОЕ: Helius приобретает Light Protocol
фреймворк Steel для написания смарт-контрактов Solana
Блог/Разработка

Как писать программы Solana с помощью Steel

Разработчик Solana, Assylm LabsPerelyn в XPerelyn в LinkedIn
Чтение: 15 мин

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 и другие.

Оптимизация 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_id 
  • accounts
  • data

В этом блоке мы объявляем экземпляр 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::ID

Затем создаем аккаунт 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 мы выполняем три важных действия:

  1. Создаем экземпляр ProgramTest, в который по умолчанию добавлена программа create_token_program
  2. Добавляем программу token_metadata в экземпляр ProgramTest, поскольку используемая нами программа токенов Metaplex по умолчанию не входит в ProgramTest
  3. Запускаем экземпляр 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, изучите следующие ресурсы:

Подпишитесь на Helius

Следите за новостями разработки на Solana и получайте уведомления о наших публикациях