新着:HeliusがLight Protocolを買収
Solanaスマートコントラクトを記述するためのSteelフレームワーク
ブログ/開発

SteelでSolanaプログラムを書く方法

Solana開発者、Assylm LabsXのPerelynLinkedInのPerelyn
読了時間:15分

Steelは、最小限のボイラープレートと最大限の制御性でネイティブSolanaプログラムを記述できる、軽量でモジュール式のフレームワークです。OreのHardhat Chadによって構築されたSteelは、開発者体験を犠牲にせず、ネイティブRustのパフォーマンスを求める開発者向けに設計されています。

この記事では、以下について学びます。

  • Steelとは何か、AnchorやPinocchioとどのような関係にあるか
  • 命令の定義方法とSteelプロジェクトの構成方法
  • Steelを使用してカスタムSPLトークンを作成する方法
  • solana-program-testでプログラムをテストする方法

前提条件

このガイドでは、以下の知識があることを前提としています。

  • Rustの基本構文とツールチェーン
  • Solana開発の基礎(アカウント、命令、プログラム)
  • CLIの使用方法(cargo、solana、curlなど)

基本的なSolanaまたはRustプログラムを記述できれば、Steelを使う準備は整っています。

Steelとは?

Steelは、Solana上でプログラムを構築するための新しいモジュール式フレームワークです。開発者は少ないボイラープレートでプログラムを記述でき、Anchorよりも規約に縛られません。

Steelは、ネイティブに近い方法(フレームワークを使わない方法)でSolanaプログラムの開発を加速するマクロとCross-Program Invocation(CPI)ヘルパーを提供します。これにより、優れた開発者体験とネイティブに近いパフォーマンスを両立できます。

Steelが提供するマクロとヘルパーをいくつか見てみましょう。

Steelのマクロ

Steelが提供するマクロには、以下が含まれます。

account!

account!マクロは、SteelでAccount型を定義し、AccountValidationトレイトへのアクセスも付与します。このトレイトは、開発中にアカウントの状態を検証するためのヘルパーを提供します。

instruction!

instruction!マクロは、SteelでInstruction型を定義し、to_bytes関数へのアクセスも付与します。この関数はapi/src/sdkで使用されます。

Steelのほかのマクロには、errorとeventがあります。その名前が示すとおり、それぞれエラーとイベントに使用されます。

SteelのCPIヘルパー

Steelは、開発者がプログラム開発中に必要とする多くのCross-Program Invocation(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プログラムにほとんど、またはまったくオーバーヘッドを追加しません。そのため、ネイティブRustで記述されたSolanaプログラムと同等に最適化されています。さらに、デフォルトのデータシリアライザーとしてbytemuckを使用することで、より高い効率を実現します。

SteelとAnchorの比較

Anchorは、安全なSolanaプログラムを迅速に構築するために設計された、規約重視の強力なフレームワークです。アカウントのシリアライズとデシリアライズ、命令データなどに必要なボイラープレートの削減、重要なセキュリティチェックの実行、クライアントライブラリの自動生成、包括的なテスト環境の提供により、開発プロセスを効率化します。

SteelとAnchorの主な違いは?

Anchorは初心者にも使いやすいスマートコントラクトフレームワークで、経験レベルを問わずSolana開発者がSolanaプログラムを迅速に記述できます。Anchorは直感的で使いやすい開発者体験を重視しているため、多くのSolana開発者に利用されています。

ただし、このシンプルさには代償があります。

Anchorには蓄積されたオーバーヘッドがあり、Solanaプログラムのバイナリが肥大化して、オンチェーンでのパフォーマンスに悪影響を与えます。たとえば、Solanaプログラムのデプロイや命令の呼び出しにかかるコストが増加します。

Solanaは高速かつ効率的なので、AnchorがSolanaプログラムにオーバーヘッドを追加しても、ほとんどの人は気づきません。ただし、OreやCode-vmのような複雑なプログラムでは、このオーバーヘッドによってオンチェーンで使用できなくなるため、例外となります。

通常、このようなSolanaプログラムはネイティブRustで構築されます。しかし、メンテナーはその難しさを理解しており、Anchorと同等に使いやすく、ネイティブRustのように高性能なフレームワークを必要としています。

SteelとAnchorの利点とトレードオフ

AnchorはSolanaプログラムにオーバーヘッドを追加するものの、Solanaエコシステムで最高の開発者体験を提供しており、今でもSolanaの新規開発者に推奨されるフレームワークです。

Anchorの構文は理解しやすく、Interface Definition Language(IDL)を提供します。これにより、JavaScriptなどのほかの言語でSolanaプログラムを簡単にテストできるほか、Solanaプログラムと通信するクライアント側アプリケーションも開発できます。

AnchorのIDLは非常に強力で、Codamaのようなツールを使用して、Solanaプログラムのクライアント、コマンドラインインターフェース(CLI)、ドキュメントを自動生成できます。

現在、SteelフレームワークにはIDL機能がありません。構文は開発者にとって使いやすいものの、Steelを使用するにはRustに十分習熟している必要があります。

Anchorは新規開発者に推奨されますが、Solanaプログラム開発の内部動作をマクロと独自構文で隠すため、より技術力の高い開発者には制約となる場合があります。 

一方、Steelでは、Solanaプログラムの最も基本的なレベルにあるすべての要素へ開発者がアクセスできます。この細かさはテストプロセスで特に役立ちます。テストはデフォルトでRustを使用して記述されるため、直接的なデバッグ体験が得られます。

Steelが優れたスマートコントラクトフレームワークである理由は、それが何であるか(ネイティブRustの最小限のラッパー)と、何でないか(オーバーヘッドを生む追加構文)にあります。

簡単に言えば、SteelはネイティブRustの能力を維持し、効率性を損なわずに開発者が使いやすくしたものです。

SteelとPinocchioの比較

Pinocchioは、RustでSolanaプログラムを作成するための依存関係のないライブラリです。Feboがサイドプロジェクトとして作成し、後に本格的なAnzaプロジェクトとなりました。SVMローダーがプログラムの入力パラメーターをバイト配列にシリアライズする仕組みを利用しています。この配列はプログラムのエントリーポイントに渡され、入力を読み取るためのゼロコピー型の定義に使用されます。

簡単に言えば、Pinocchioは外部クレートに依存せず、動的型の使用を避けたsolana_programの軽量版です。

Pinocchioのリリース以降、その正体について多くの誤解が生まれています。Pinocchioライブラリはsolana_programライブラリを置き換えるためのもので、AnchorやSteelの競合ではありません。これらのフレームワークを軽量化するため、補完関係にあります。

多くの人がPinocchioプログラムと呼んでいるものは、solana_programではなくpinocchioに依存するネイティブRustコードにすぎません。

Steelを使用してトークンを作成する方法

Steelの仕組みを示すため、SPLトークンを作成するシンプルなSolanaプログラムを記述します。映像で学びたい場合は、次の動画をご覧ください。

前提条件

  • Rust/Cargo
  • Solana
  • Steel

Rustのインストール

RustはRust公式サイトまたはCLIからインストールできます。

コード
curl --proto '=https' --tlsv1.2 -sSf <https://sh.rustup.rs> | sh

Solana Tool Suiteのインストール

SteelにはSolana Tool Suiteも必要です。この記事の執筆時点での最新リリース(2.2.15)は、macOSとLinuxで次のコマンドを使用してインストールできます。

コード
sh -c "$(curl -sSfL <https://release.anza.xyz/v2.2.14/install>)"

Windowsユーザーは、次のコマンドでSolana Tool Suiteをインストールできます。

コード
cmd /c "curl <https://release.anza.xyz/v2.2.14/agave-install-init-x86_64-pc-windows-msvc.exe> --output C:\\agave-install-tmp\\agave-install-init.exe --create-dirs"

ただし、代わりにWindows Subsystem for Linux(WSL)を使用することを強く推奨します。これにより、デュアルブートや別の仮想マシンをセットアップせずに、Windowsマシン上でLinux環境を実行できます。この方法を選ぶ場合は、Linux向けのインストール手順(curlコマンド)を参照してください。

開発者は、v2.2.15をダウンロードしたいバージョンのリリースタグに置き換えるか、stable、beta、edgeのチャンネル名を使用できます。 

インストール後にsolana –-versionを実行し、目的のバージョンのsolanaがインストールされていることを確認します。

Steelのインストール

次のコマンドを実行すると、CargoでSteelをインストールできます。

コード
cargo install steel-cli

Steelプロジェクトの作成

Steelプロジェクトの作成は、次のコマンドを実行するだけです。

コード
// creates a new Steel project named `create-token`
steel new token

// enter directory
cd create-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という2つのフォルダーがあります。

apiには、Solanaプログラムの実装時に使用するstateやerrorsなどの型が含まれ、programフォルダーにはプログラムロジックが含まれます。 

Steelでプログラムを開発する場合、programフォルダーはapiフォルダーに依存しているため、apiフォルダーから始めることを推奨します。

state、const、errorモジュールの削除

apiフォルダーには、state、const、errorなど、create-tokenプロジェクトで使用しないモジュールがあるため、削除します。 

次のコマンドを実行すると、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プログラムのすべての命令はenumで定義され、各命令はstructです。

すべての命令を含むenumは次のようになります。

コード
#[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に対応します。

命令のenumと命令のstructを定義したら、それらをinstruction!マクロに渡す必要があります。第1引数は命令のenum、第2引数は命令のstructです。

コード
instruction!(CreateTokenInstruction, Initialize);
instruction!(CreateTokenInstruction, Add);

create-tokenプログラムには4つの引数を取る命令が1つあるため、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文字にはより多くのバイトが必要になる場合があります)。

トークンの小数桁数は1バイトに収まるため、decimalsは単純なu8です。

SDKの更新

api/srcには、sdk.rsというファイルがあります。プログラムロジックの実装時には使用しませんが、テストやRustクライアントコードの実行時に使用します。このファイルには、Steelプログラムの各命令を個別に構築する関数が含まれています。このプログラムには命令が1つしかないため、必要なSDK関数も1つだけです。したがって、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という関数があり、5つの引数を取ります。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;

このコードブロックでは、トークンのメタデータを保存するProgram Derived Address(PDA)を取得しようとしています。必要なアドレスを導出するには、次のシードが必要です。

  • 文字列「metadata」のバイト表現("metadata".as_bytes())
  • スライスとしてのメタデータプログラムのプログラムID(mpl_token_metadata::ID.as_ref())
  • スライスとしてのミント公開鍵(mint.as_ref())

これらの入力をすべて合わせたものがシードになります。Pubkey::find_program::addressの第2引数には、MetadataプログラムのプログラムIDのみが必要です。

最後のコードブロックでは、この命令を表すInstruction型を返します。

Instruction型は次のようになります。

コード
 Instruction {
        program_id: crate::ID,
        accounts: vec![
            AccountMeta::new(user, true),
            AccountMeta::new(mint, true),
            AccountMeta::new(metadata, false),
            AccountMeta::new_readonly(spl_token::ID, false),
            AccountMeta::new_readonly(mpl_token_metadata::ID, false),
            AccountMeta::new_readonly(system_program::ID, false),
            AccountMeta::new_readonly(sysvar::rent::ID, false),
        ],
        data: Create {
            name,
            symbol,
            uri,
            decimals,
        }
        .to_bytes(),
    }

 Instruction型は、次の3つのフィールドを持つstructです。

  • 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の依存関係の追加

ここで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を実行すると依存関係のエラーは解消されるはずです。

ただし、programフォルダーが依存するコードをapiフォルダーから削除したため、引き続き次のようなエラーが表示されます。

コード
error[E0599]: no variant or associated item named `Initialize` found for enum `create_token_api::instruction::CreateTokenInstruction` in the current scope
  --> program/src/lib.rs:18:33
   |
18 |         CreateTokenInstruction::Initialize => process_initialize(accounts, data)?,
   |                                 ^^^^^^^^^^ variant or associated item not found in `CreateTokenInstruction`

error[E0599]: no variant or associated item named `Add` found for enum `create_token_api::instruction::CreateTokenInstruction` in the current scope
  --> program/src/lib.rs:19:33
   |
19 |         CreateTokenInstruction::Add => process_add(accounts, data)?,
   |                                 ^^^ variant or associated item not found in `CreateTokenInstruction`

心配はいりません。次のセクションでこれらのエラーを修正します。

Steelを使用したプログラムロジックの実装

Steelプロジェクトには、デフォルトでapiとprogramの2つのフォルダーがあります。先ほど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関数内には、説明が必要な重要なコードブロックが2つあります。

コード
 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を使用して呼び出す命令を選択します。このプログラムには命令が1つしかないため、matchアームも1つだけです。 

これで、呼び出されたときに適切な命令を呼び出すプログラムロジックが整いました。ただし、process_createとcreate modはまだ存在しないため、作成します。

ターミナルで次を実行します。

コード
// you should be at the root of your project 

 // enter the program/src directory
 cd program/src
 
 // delete add.rs and initialize.rs
 rm -rf add.rs initialize.rs
 
 // create create.rs
 touch create.rs 

次に、program/src/create.rsを以下の内容で更新します。

コード
use create_token_api::prelude::*;
use solana_program::{msg, program_pack::Pack};
use steel::*;

pub fn process_create(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult {
    // Load accounts.
    let [user_info, mint_info, metadata_info, token_program, token_metadata_program, system_program, rent_sysvar] =
        accounts
    else {
        return Err(ProgramError::NotEnoughAccountKeys);
    };

    // validate
    user_info.is_signer()?;
    mint_info.is_empty()?.is_signer()?;
    metadata_info.is_empty()?.is_writable()?;
    token_program.is_program(&spl_token::ID)?;
    token_metadata_program.is_program(&mpl_token_metadata::ID)?;
    system_program.is_program(&system_program::ID)?;
    rent_sysvar.is_sysvar(&sysvar::rent::ID)?;

    // create mint account
    create_account(
        user_info,
        mint_info,
        system_program,
        spl_token::state::Mint::LEN,
        &token_program.key,
    )?;

    msg!("create account");

    let args = Create::try_from_bytes(data)?;
    let name = bytes_to_string::<32>(&args.name)?;
    let symbol = bytes_to_string::<8>(&args.symbol)?;
    let uri = bytes_to_string::<128>(&args.uri)?;
    let decimals = args.decimals;
   
    // initialize mint
    initialize_mint(
        mint_info,
        user_info,
        Some(user_info),
        token_program,
        rent_sysvar,
        decimals,
    )?;

    msg!("initialize mint");

    // create metadata account
    mpl_token_metadata::instructions::CreateMetadataAccountV3Cpi {
        __program: token_metadata_program,
        metadata: metadata_info,
        mint: mint_info,
        mint_authority: user_info,
        payer: user_info,
        update_authority: (user_info, true),
        system_program,
        rent: Some(rent_sysvar),
        __args: mpl_token_metadata::instructions::CreateMetadataAccountV3InstructionArgs {
            data: mpl_token_metadata::types::DataV2 {
                name,
                symbol,
                uri,
                seller_fee_basis_points: 0,
                creators: None,
                collection: None,
                uses: None,
            },
            is_mutable: true,
            collection_details: None,
        },
    }
    .invoke()?;
    msg!("metadata account created");

    Ok(())
}

ここで何が行われているのかを順に見ていきましょう。

コード
// Load accounts.
    let [user_info, mint_info, metadata_info, token_program, token_metadata_program, system_program, rent_sysvar] =
        accounts
    else {
        return Err(ProgramError::NotEnoughAccountKeys);
    };

このコードブロックでは、この命令に必要なアカウントを読み込みます。渡されたアカウントが定義済みのアカウントと一致しない場合、このブロックはProgramError::NotEnoughAccountKeysエラーをスローします。

よく見ると、アカウントの命名方法にパターンがあることが分かります。

  • 「通常」のアカウントはinfoで終わります
  • プログラムアカウントはprogramで終わります
  • Sysvarはsysvarで終わります

これはSteelにおける規約重視のアカウント命名方法です。プログラムには実質的な影響がないため、別の方法を選んでも構いません。

次に、このコードブロックでアカウントを検証します。

コード
// validate
user_info.is_signer()?; // user is a signer
mint_info.is_empty()?.is_signer()?; // mint is empty and is a signer
metadata_info.is_empty()?.is_writable()?; // metadata is empty and is writable
token_program.is_program(&spl_token::ID)?; // token program == spl_token::ID
token_metadata_program.is_program(&mpl_token_metadata::ID)?; // token meatadata == mpl_token_metadata::ID
system_program.is_program(&system_program::ID)?; // system program == system_program::ID
rent_sysvar.is_sysvar(&sysvar::rent::ID)?; // rent sysvar == sysvar::rent::ID

次に、create_accountヘルパーを使用してmintアカウントを作成します。

コード
// create mint account
    create_account(
        user_info,
        mint_info,
        system_program,
        spl_token::state::Mint::LEN,
        &token_program.key,
    )?;

mintアカウントを作成したら、命令データをバイトからRust型にデシリアライズします。

コード
    let args = Create::try_from_bytes(data)?;
    let name = bytes_to_string::<32>(&args.name)?;
    let symbol = bytes_to_string::<8>(&args.symbol)?;
    let uri = bytes_to_string::<128>(&args.uri)?;
    let decimals = args.decimals;

最初の行では、&[u8]型の命令データをapi/instructions.rs/Createに変換します。続く3行では、バイトであるCreateのフィールドを、bytes_to_stringヘルパーで文字列に変換します。

また、bytes_to_stringはconstジェネリックパラメーター(::<32>)を取ることにも注目してください。これにより、正確な長さの文字列を生成してコンピュートユニットを節約できます。

コード
// initialize mint
    initialize_mint(
        mint_info,
        user_info,
        Some(user_info),
        token_program,
        rent_sysvar,
        decimals,
    )?;

次に、initialize_mintヘルパー関数でmintアカウントを初期化します。

コード
// create metadata account
    mpl_token_metadata::instructions::CreateMetadataAccountV3Cpi {
        __program: token_metadata_program,
        metadata: metadata_info,
        mint: mint_info,
        mint_authority: user_info,
        payer: user_info,
        update_authority: (user_info, true),
        system_program,
        rent: Some(rent_sysvar),
        __args: mpl_token_metadata::instructions::CreateMetadataAccountV3InstructionArgs {
            data: mpl_token_metadata::types::DataV2 {
                name,
                symbol,
                uri,
                seller_fee_basis_points: 0,
                creators: None,
                collection: None,
                uses: None,
            },
            is_mutable: true,
            collection_details: None,
        },
    }
    .invoke()?;

ここでは、トークンミント用のmetadataアカウントを作成しました。このアカウントには、コレクションの名前、シンボル、作成者などの情報が含まれます。

create.rsファイルの作業が完了したので、steel buildを実行します。 

次のエラーが表示されるはずです。

コード
error[E0433]: failed to resolve: use of undeclared crate or module `spl_token`
  --> program/src/create.rs:28:9
   |
28 |         spl_token::state::Mint::LEN,
   |         ^^^^^^^^^ use of undeclared crate or module `spl_token`

error[E0433]: failed to resolve: use of undeclared crate or module `mpl_token_metadata`
  --> program/src/create.rs:69:5
   |
69 |     mpl_token_metadata::instructions::CreateMetadataAccountV3Cpi {
   |     ^^^^^^^^^^^^^^^^^^ use of undeclared crate or module `mpl_token_metadata`

error[E0433]: failed to resolve: use of undeclared crate or module `mpl_token_metadata`
  --> program/src/create.rs:78:17
   |
78 |         __args: mpl_token_metadata::instructions::CreateMetadataAccountV3Instru...
   |                 ^^^^^^^^^^^^^^^^^^ use of undeclared crate or module `mpl_token_metadata`

error[E0433]: failed to resolve: use of undeclared crate or module `mpl_token_metadata`
  --> program/src/create.rs:79:19
   |
79 |             data: mpl_token_metadata::types::DataV2 {
   |                   ^^^^^^^^^^^^^^^^^^ use of undeclared crate or module `mpl_token_metadata`

これらは依存関係のエラーが発生していることを示します。/program/Cargo.tomlファイルを次の内容に編集して更新できます。

コード
[dependencies]
...
...
mpl-token-metadata.workspace = true
spl-token.workspace = true

ここでsteel buildを再度実行すると、最後のエラーが1つ発生します。

コード
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という2つの関数で構成されます。

setup関数では、次の3つの重要な処理を行います。

  1. ProgramTestのインスタンスを作成します。デフォルトでcreate_token_programプログラムが追加されています
  2. 使用するMetaplexトークンプログラムはデフォルトではProgramTestに含まれないため、ProgramTestのインスタンスにtoken_metadataプログラムを追加します
  3. startメソッドでProgramTestのインスタンスを起動します。このメソッドは(BanksClient、Keypair、Hash)のタプルを返します
コード
async fn setup() -> (BanksClient, Keypair, Hash) {
    let mut program_test = ProgramTest::new(
        "create_token_program",
        create_token_api::ID,
        processor!(create_token_program::process_instruction),
    );

    program_test.add_program("token_metadata", mpl_token_metadata::ID, None);

    program_test.prefer_bpf(true);
    program_test.start().await
}

run_testの最初の部分では、setup関数を呼び出し、トークンミント用のKeypairを作成します。

コード
// Setup test
 let (mut banks, payer, blockhash) = setup().await;
 let mint_keypair = Keypair::new();

次に、命令データを準備します。

create命令はバイト表現を期待するため、string_to_bytesヘルパーを使用して文字列をバイトに変換します。

コード
   let name = string_to_bytes::<32>("ANATOLY").unwrap();
    let symbol = string_to_bytes::<8>("MERT").unwrap();
    let uri = string_to_bytes::<128>("blah blah blah").unwrap();
    let decimals = 9;

先ほどapiフォルダーで、プログラムロジックでは使用しなかった関数を実装したことを覚えていますか? api/src/sdk.rsのcreate関数です。

これは、次のコードブロックで最初に呼び出すものと同じ関数です。この関数でInstructionのインスタンスを作成し、Transaction::new_signed_with_payerを使用してTransactionインスタンスに渡します。その後、処理のためにトランザクションをbanks.process_transaction(tx).await;へ渡します。

assert!(res.is_ok());は、トランザクションが処理されたことを確認します。

コード
// Submit create transaction.
    let ix = create(
        payer.pubkey(),
        mint_keypair.pubkey(),
        name,
        symbol,
        uri,
        decimals,
    );
    
    let tx = Transaction::new_signed_with_payer(
        &[ix],
        Some(&payer.pubkey()),
        &[&payer, &mint_keypair],
        blockhash,
    );
    
    let res = banks.process_transaction(tx).await;
    assert!(res.is_ok());

ここまでで、テスト環境(ProgramTest)で命令を実行しました。

次に、正しく実行されたかをテストします。

コード
// get serialized data of mint account 
let serialized_mint_data = banks
        .get_account(mint_keypair.pubkey())
        .await
        .unwrap()
        .unwrap()
        .data;

// unpack the mint account data to get the SPL Mint information
let mint_data = spl_token::state::Mint::unpack(&serialized_mint_data).unwrap();

// check if the mint account was initilized 
assert!(mint_data.is_initialized);

// check if the mint authority matches the one we set
assert_eq!(mint_data.mint_authority.unwrap(), payer.pubkey());

// check if the decimals match
assert_eq!(mint_data.decimals, decimals);

metadataアカウントに保存されたデータなど、ほかの項目を確認するアサーションも追加できますが、ここでは簡潔にするため省略します。

余裕があれば、自分で追加してみてください。サポートが必要な場合は、こちらのSteelテストに関するSolana開発者ガイドを参照してください。

テストファイルが完成したので、テストコマンドsteel testを実行します。 

残念ながら、mpl_token_metadataプログラムのソースコードまたはELFファイルがないため、失敗します。

心配はいりません。次を実行すれば修正できます。

コード
// you have to be at the root of your project 

// create a folder called fixtures in program/tests 
// ProgramTest is going to check this folder for the ELF file for token metadata
mkdir program/tests/fixtures

// dump the ELF file for the Metaplex metadata program in the fixtures folder
solana program dump metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s program/tests/fixtures/token_metadata.so

ここでsteel testを実行すると、次の結果が得られるはずです。

コード
running 1 test
[2025-06-08T14:46:12.240628000Z INFO  solana_program_test] "create_token_program" SBF program from /Users/perelyn/helius/create-token/target/deploy/create_token_program.so, modified 3 seconds, 112 ms, 833 µs and 660 ns ago
[2025-06-08T14:46:12.242336000Z INFO  solana_program_test] "token_metadata" SBF program from tests/fixtures/token_metadata.so, modified 1 minute, 49 seconds, 247 ms, 367 µs and 400 ns ago
[2025-06-08T14:46:12.381492000Z DEBUG solana_runtime::message_processor::stable_log] Program z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35 invoke [1]
[2025-06-08T14:46:12.382734000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 invoke [2]
[2025-06-08T14:46:12.383272000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 success
[2025-06-08T14:46:12.383298000Z DEBUG solana_runtime::message_processor::stable_log] Program log: create account
[2025-06-08T14:46:12.383562000Z DEBUG solana_runtime::message_processor::stable_log] Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]
[2025-06-08T14:46:12.383783000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Instruction: InitializeMint
[2025-06-08T14:46:12.386049000Z DEBUG solana_runtime::message_processor::stable_log] Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 2968 of 192320 compute units
[2025-06-08T14:46:12.386068000Z DEBUG solana_runtime::message_processor::stable_log] Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success
[2025-06-08T14:46:12.386099000Z DEBUG solana_runtime::message_processor::stable_log] Program log: initialize mint
[2025-06-08T14:46:12.386409000Z DEBUG solana_runtime::message_processor::stable_log] Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s invoke [2]
[2025-06-08T14:46:12.387342000Z DEBUG solana_runtime::message_processor::stable_log] Program log: IX: Create Metadata Accounts v3
[2025-06-08T14:46:12.387576000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 invoke [3]
[2025-06-08T14:46:12.387588000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 success
[2025-06-08T14:46:12.387999000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Allocate space for the account
[2025-06-08T14:46:12.388226000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 invoke [3]
[2025-06-08T14:46:12.388264000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 success
[2025-06-08T14:46:12.388306000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Assign the account to the owning program
[2025-06-08T14:46:12.388851000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 invoke [3]
[2025-06-08T14:46:12.388873000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 success
[2025-06-08T14:46:12.392769000Z DEBUG solana_runtime::message_processor::stable_log] Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s consumed 37330 of 185782 compute units
[2025-06-08T14:46:12.392790000Z DEBUG solana_runtime::message_processor::stable_log] Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s success
[2025-06-08T14:46:12.392842000Z DEBUG solana_runtime::message_processor::stable_log] Program log: metadata account created
[2025-06-08T14:46:12.395012000Z DEBUG solana_runtime::message_processor::stable_log] Program z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35 consumed 51973 of 200000 compute units
[2025-06-08T14:46:12.395031000Z DEBUG solana_runtime::message_processor::stable_log] Program z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35 success
test run_test ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.16s

改めて、お疲れさまでした!

同じ出力が得られれば、プログラムはテストに合格しています。

まとめ

Steelは、高性能に最適化されたSolanaプログラムを構築するための、モジュール式で軽量な開発フレームワークです。この記事では、Steelの仕組みを説明し、AnchorおよびPinocchioと比較したうえで、Steelを使用して新しいトークンを作成する例を順を追って解説しました。

その他のリソース

SteelとSolanaプログラム開発についてさらに学ぶには、次のリソースをご覧ください。

Heliusを購読

Solana開発の最新情報や新しい記事の公開通知を受け取れます