BARU: Helius mengakuisisi Light Protocol
framework Steel untuk menulis smart contract Solana
Blog/Pengembangan

Cara Menulis Program Solana dengan Steel

Developer Solana, Assylm LabsPerelyn di XPerelyn di LinkedIn
Bacaan 15 menit

Steel adalah framework ringan dan modular untuk menulis program native Solana dengan boilerplate minimal dan kontrol maksimal. Dibuat oleh Hardhat Chad (dari Ore), Steel dirancang bagi developer yang menginginkan performa native Rust tanpa mengorbankan pengalaman developer.

Dalam artikel ini, Anda akan mempelajari:

  • Apa itu Steel dan kaitannya dengan Anchor serta Pinocchio
  • Cara mendefinisikan instruction dan menyusun proyek Steel
  • Cara membuat token SPL khusus menggunakan Steel
  • Cara menguji program Anda dengan solana-program-test

Prasyarat

Panduan ini mengasumsikan Anda memahami:

  • Sintaks dan toolchain dasar Rust
  • Dasar-dasar pengembangan Solana (account, instruction, program)
  • Penggunaan CLI (misalnya cargo, solana, curl)

Jika Anda sudah terbiasa menulis program dasar Solana atau Rust, Anda siap membangun dengan Steel.

Apa itu Steel?

Steel adalah framework modular baru untuk membangun program di Solana yang memungkinkan developer menulis program dengan lebih sedikit boilerplate dan tidak terlalu preskriptif dibandingkan Anchor.

Steel menyediakan macro dan helper Cross-Program Invocation (CPI) yang membantu mempercepat pengembangan program Solana seperti secara native (tanpa framework). Artinya, Anda memperoleh performa seperti native dengan pengalaman developer yang lebih baik.

Mari pelajari beberapa macro dan helper yang ditawarkan Steel.

Macro Steel

Beberapa macro yang ditawarkan Steel meliputi:

account!

Macro account! mendefinisikan tipe Account di Steel dan juga memberinya akses ke trait AccountValidation, yang menyediakan helper untuk memvalidasi status account selama pengembangan.

instruction!

Macro instruction! mendefinisikan tipe Instruction di Steel dan juga memberinya akses ke fungsi to_bytes, yang akan digunakan dalam api/src/sdk.

Macro lain di Steel mencakup error dan event; seperti namanya, masing-masing digunakan untuk error dan event.

Helper CPI Steel

Steel menyediakan fungsi helper yang dibutuhkan sebagian besar developer untuk Cross-Program Invocation (CPI) ketika mengembangkan program, seperti instruction dari system_program, termasuk create_account, transfer, dan lainnya. 

Steel juga menyertakan instruction dari spl_token_program / spl_associated_token_program, termasuk mint_to, burn, create_associated_token_account, dan lainnya.

Optimalisasi CU

Anda mungkin mengira Steel efisien dalam penggunaan CU karena apa yang dilakukannya—tetapi sebenarnya Steel efisien karena apa yang tidak dilakukannya. Karena framework Steel ringan dan menambahkan sangat sedikit atau bahkan tanpa overhead ke program Solana, optimalisasinya setara dengan program Solana yang ditulis dalam native Rust, bahkan lebih baik berkat penggunaan bytemuck sebagai serializer data default.

Steel vs. Anchor

Anchor adalah framework preskriptif dan andal yang dirancang untuk membangun program Solana yang aman dengan cepat. Framework ini menyederhanakan proses pengembangan dengan mengurangi boilerplate untuk area seperti (de)serialisasi account dan data instruction, menjalankan pemeriksaan keamanan penting, membuat library klien secara otomatis, serta menyediakan lingkungan pengujian yang lengkap.

Apa perbedaan utama antara Steel dan Anchor?

Anchor adalah framework smart contract yang ramah bagi pemula dan memungkinkan developer Solana dengan tingkat keahlian apa pun menulis program Solana dengan cepat. Anchor berfokus pada pengalaman developer yang intuitif dan mudah digunakan. Karena itu, banyak developer Solana mengandalkannya.

Namun, kemudahan ini memiliki konsekuensi.

Anchor telah mengakumulasi overhead yang membuat binary program Solana lebih besar, sehingga berdampak negatif pada performanya secara on-chain. Misalnya, dengan meningkatkan biaya deployment program Solana dan pemanggilan instruction.

Berkat kecepatan dan efisiensi Solana, sebagian besar orang tidak menyadari overhead yang ditambahkan Anchor ke program Solana. Pengecualiannya adalah developer program yang lebih kompleks seperti Ore dan Code-vm, yang akan menjadi tidak dapat digunakan secara on-chain akibat overhead tersebut.

Biasanya, program Solana seperti ini akan dibuat dengan Native Rust. Namun, pengelolanya memahami betapa sulitnya pendekatan tersebut dan membutuhkan framework yang lebih mudah digunakan, sebanding dengan Anchor, tetapi tetap berperforma tinggi seperti Native Rust.

Manfaat dan Kompromi antara Steel dan Anchor

Meski menambahkan overhead ke program Solana, Anchor menawarkan pengalaman developer terbaik di ekosistem Solana dan tetap menjadi framework yang direkomendasikan bagi developer Solana baru.

Sintaks Anchor mudah dipahami dan menyediakan Interface Definition Language (IDL), yang memudahkan pengujian program Solana dalam bahasa lain seperti JavaScript, serta pengembangan aplikasi sisi klien yang berkomunikasi dengan program Solana.

IDL Anchor begitu andal sehingga dapat digunakan oleh alat seperti Codama untuk membuat klien, command line interface (CLI), dan dokumentasi program Solana secara otomatis.

IDL adalah fitur yang saat ini belum dimiliki framework Steel. Meski sintaksnya ramah bagi developer, Steel mengharuskan developer memiliki pemahaman Rust yang memadai.

Meskipun Anchor direkomendasikan bagi developer baru, framework ini dapat membatasi developer yang lebih teknis karena menyembunyikan cara kerja internal pengembangan program Solana di balik macro dan sintaksnya. 

Sebaliknya, Steel memberi developer akses ke seluruh aspek program Solana pada tingkat paling dasar. Granularitas ini sangat membantu selama pengujian karena pengujian secara default ditulis dalam Rust, sehingga memberikan pengalaman debugging langsung.

Steel merupakan framework smart contract yang hebat karena apa adanya (yaitu wrapper minimal di atas Native Rust) dan karena apa yang tidak dimilikinya (yaitu sintaks tambahan yang menimbulkan overhead).

Sederhananya, Steel adalah versi Native Rust yang lebih ramah bagi developer, dengan tetap mempertahankan kemampuannya tanpa mengorbankan efisiensi.

Steel vs. Pinocchio

Pinocchio adalah library tanpa dependensi untuk membuat program Solana dalam Rust. Library ini ditulis oleh Febo sebagai proyek sampingan, lalu berkembang menjadi proyek penuh Anza. Pinocchio memanfaatkan cara loader SVM menserialisasi parameter input program menjadi array byte, yang kemudian diteruskan ke entry point program untuk mendefinisikan tipe zero-copy guna membaca input.

Sederhananya, Pinocchio adalah versi solana_program yang lebih ramping, tidak bergantung pada crate eksternal, dan menghindari penggunaan tipe dinamis.

Sejak Pinocchio dirilis, muncul banyak kesalahpahaman tentang fungsinya. Library Pinocchio ditujukan untuk menggantikan library solana_program—bukan sebagai pesaing Anchor atau Steel. Library ini melengkapi framework tersebut karena membuatnya lebih ringan.

Apa yang dianggap kebanyakan orang sebagai program Pinocchio sebenarnya hanyalah kode native Rust yang bergantung pada pinocchio, bukan solana_program.

Cara Membuat Token Menggunakan Steel

Untuk menunjukkan cara kerja Steel, kita akan menulis program Solana sederhana yang membuat token SPL. Jika Anda lebih mudah belajar secara visual, Anda dapat menonton video berikut.

Prasyarat

  • Rust/Cargo
  • Solana
  • Steel

Instal Rust

Rust dapat diinstal dari situs web resmi Rust atau melalui CLI:

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

Instal Solana Tool Suite

Steel juga memerlukan Solana Tool Suite. Rilis terbaru (yaitu 2.2.15, saat artikel ini ditulis) dapat diinstal dengan perintah berikut untuk macOS dan Linux:

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

Pengguna Windows dapat menginstal Solana Tool Suite dengan perintah berikut:

Kode
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"

Namun, Anda sangat disarankan untuk menggunakan Windows Subsystem for Linux (WSL). Dengan ini, Anda dapat menjalankan lingkungan Linux di komputer Windows tanpa dual boot atau menyiapkan mesin virtual terpisah. Jika memilih cara ini, gunakan petunjuk instalasi untuk Linux di atas (yaitu perintah curl).

Developer dapat mengganti v2.2.15 dengan tag rilis versi yang ingin diunduh, atau menggunakan nama channel stable, beta, atau edge. 

Setelah terinstal, jalankan solana –-version untuk memastikan versi solana yang diinginkan telah terinstal.

Instal Steel

Kita dapat menginstal Steel dengan Cargo menggunakan perintah:

Kode
cargo install steel-cli

Buat Proyek Steel

Membuat proyek Steel semudah menjalankan:

Kode
// creates a new Steel project named `create-token`
steel new token

// enter directory
cd create-token

Direktori token kita akan terlihat seperti ini:

Kode
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

Tata letak default proyek Steel berisi dua folder bernama api dan program.

api berisi tipe seperti state dan errors yang akan kita gunakan ketika mengimplementasikan program Solana. Folder program berisi logika program. 

Saat mengembangkan program dengan Steel, sebaiknya mulai dari folder api karena folder program bergantung padanya.

Menghapus Modul state, const, dan error

Di folder api, terdapat beberapa modul yang tidak akan digunakan untuk proyek create-token kita, seperti state, const, dan error. Jadi, mari hapus modul tersebut. 

Kita dapat menghapus modul Steel dengan menjalankan perintah berikut:

Kode
# 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/>)

Setelah menghapus modul tersebut, kita harus memperbarui file api/src/lib.rs karena file itu memanggil modul-modul tersebut.

Perbarui api/src/lib.rs menjadi seperti ini:

Kode
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");

Mendefinisikan Instruction di Steel

Di Steel, instruction didefinisikan dalam api/src/instructions.rs. Semua instruction program Steel didefinisikan dalam sebuah enum, dan setiap instruction merupakan struct.

Enum yang berisi semua instruction terlihat seperti ini:

Kode
#[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]
}

Jika sebuah instruction tidak memerlukan argumen, seperti Initialize, instruction tersebut tidak memiliki field. 

Instruction yang memerlukan data menggunakan representasi byte. Contohnya adalah Add::amount is [u8; 8], yang dipetakan ke u64.

Setelah mendefinisikan enum instruction dan struct instruction, kita harus meneruskannya ke macro instruction!, dengan enum instruction sebagai argumen pertama dan struct instruction sebagai argumen kedua:

Kode
instruction!(CreateTokenInstruction, Initialize);
instruction!(CreateTokenInstruction, Add);

Program create-token kita memiliki satu instruction yang menerima empat argumen, sehingga api/src/instructions akan terlihat seperti ini:

Kode
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);

Dalam Create, field name, symbol, dan uri merupakan string yang direpresentasikan sebagai array byte berukuran tetap:

  • name: [u8; 16] — untuk nama hingga 16 byte
  • symbol: [u8; 8] — simbol biasanya pendek
  • uri: [u8; 128] — URI biasanya lebih panjang

Ukuran ini bergantung pada panjang maksimum dalam byte, bukan karakter (misalnya, karakter UTF-8 multibyte mungkin memerlukan lebih banyak ruang).

decimals cukup menggunakan u8 karena jumlah desimal token dapat ditampung dalam satu byte.

Memperbarui SDK

Di api/src, terdapat file bernama sdk.rs. File ini tidak digunakan saat mengimplementasikan logika program, tetapi akan digunakan untuk menjalankan pengujian atau kode klien Rust. File tersebut berisi fungsi yang membangun setiap instruction dalam program Steel. Karena program ini hanya memiliki satu instruction, kita hanya memerlukan satu fungsi SDK. Jadi, api/src/sdk.rs akan terlihat seperti ini:

Kode
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(),
    }
}

Kita memiliki satu fungsi bernama create yang menerima lima argumen: user adalah public key account yang akan memanggil instruction ini, mint adalah public key account yang akan mewakili token mint, sedangkan name, symbol, uri, dan decimals adalah data yang akan digunakan ketika mengimplementasikan logika program yang telah kita definisikan dalam api/src/instructions::Create.

Kita perlu menyimpan metadata token dan akan menggunakan program Metaplex Metadata untuk melakukannya. Pertama, kita akan menambahkan:

Kode
let metadata = Pubkey::find_program_address(
        &[
            "metadata".as_bytes(),
            mpl_token_metadata::ID.as_ref(),
            mint.as_ref(),
        ],
        &mpl_token_metadata::ID,
    )
    .0;

Dalam blok kode ini, kita mencoba mendapatkan Program Derived Address (PDA) tempat metadata token akan disimpan. Untuk mendapatkan address yang diperlukan, kita membutuhkan seed berikut:

  • String “metadata” sebagai byte (yaitu "metadata".as_bytes())
  • ID program dari program metadata sebagai slice (yaitu mpl_token_metadata::ID.as_ref())
  • Public key mint sebagai slice (yaitu mint.as_ref())

Gabungan semua input ini membentuk seed. Untuk argumen kedua Pubkey::find_program::address, kita hanya memerlukan ID program dari program Metadata.

Dalam blok kode terakhir, kita mengembalikan tipe Instruction yang merepresentasikan instruction ini.

Tipe Instruction terlihat seperti ini:

Kode
 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(),
    }

 Tipe Instruction adalah struct dengan tiga field:

  • program_id 
  • accounts
  • data

Dalam blok ini, kita mendeklarasikan instance Instruction yang sesuai dengan instruction program kita.

Untuk mendapatkan program_id dari api/src/lib.rs, gunakan:

Kode
program_id: crate::ID 

Field accounts adalah vektor metadata Account (yaitu Vec<AccountMeta>), sehingga kita harus mendeklarasikan semua account yang akan digunakan dalam instruction ini:

Kode
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),
        ],

Terakhir, field data merepresentasikan argumen yang akan digunakan untuk instruction ini dalam bentuk byte:

Kode
data: Create {
            name,
            symbol,
            uri,
            decimals,
        }
        .to_bytes(),

Sekarang, pekerjaan kita di folder api telah selesai.

Selanjutnya, mari tambahkan dependensi yang diperlukan, lalu lanjutkan ke folder program.

Menambahkan Dependensi Steel

Saat ini, jika Anda menjalankan steel build untuk mengompilasi program, prosesnya seharusnya gagal dengan error berikut:

Kode
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`

Ini menunjukkan bahwa crate spl_token dan mpl_token_metadata yang diperlukan program belum tersedia.

Untuk menambahkan crate yang belum tersedia, tambahkan ini ke file /Cargo.toml:

Kode
// /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

Di /api/Cargo.toml, tambahkan: 

Kode
// /api/Cargo.toml

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

Sekarang, jika kita menjalankan steel build, error dependensi seharusnya sudah hilang.

Namun, karena kita menghapus kode dalam folder api yang menjadi dependensi folder program, kita masih akan melihat beberapa error seperti ini:

Kode
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`

Jangan khawatir, kita akan memperbaiki error ini di bagian berikutnya.

Mengimplementasikan Logika Program dengan Steel

Secara default, proyek Steel memiliki dua folder: api dan program. Kita baru saja mendefinisikan tipe yang diperlukan program dalam folder api. Sekarang, kita harus mengimplementasikan logika program dalam folder program.

Untuk memulai, perbarui /program/lib.rs dengan:

Kode
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);

Dalam file ini, kita mendefinisikan fungsi utama process_instruction, lalu meneruskannya ke macro entrypoint!. Macro tersebut menghasilkan boilerplate yang diperlukan runtime Solana untuk memanggil logika program kita.

Di dalam fungsi process_instruction, terdapat dua blok kode penting yang perlu dibahas.

Kode
 let (ix, data) = parse_instruction(&create_token_api::ID, program_id, data)?;

parse_instruction mengurai instruction dari data instruction. Artinya, dari data yang diteruskan ke program, kita dapat menentukan instruction mana yang akan dipanggil. 

Fungsi ini mengembalikan tuple instruction(ix) dan instruction data(data) dalam kasus Ok().

Kode
match ix {
        CreateTokenInstruction::Create => process_create(accounts, data)?,
    }

Setelah mendapatkan instruction(ix) dari parse_instruction, kita menggunakan match untuk memilih instruction yang akan dipanggil. Hanya ada satu match arm karena program kita hanya memiliki satu instruction. 

Sekarang, logika program telah disiapkan untuk memanggil instruction yang tepat. Namun, process_create dan mod create belum tersedia, jadi mari kita buat keduanya.

Di terminal, jalankan:

Kode
// 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 

Sekarang, perbarui program/src/create.rs dengan:

Kode
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(())
}

Mari kita bahas apa yang terjadi di sini.

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

Dalam blok kode ini, kita memuat account yang diperlukan untuk instruction tersebut. Jika account yang diteruskan tidak cocok dengan account yang didefinisikan, blok ini akan menghasilkan error ProgramError::NotEnoughAccountKeys.

Jika diperhatikan, Anda akan melihat pola penamaan account:

  • Account “reguler” diakhiri dengan info
  • Account program diakhiri dengan program
  • Sysvar diakhiri dengan sysvar

Ini adalah cara preskriptif untuk menamai account di Steel—Anda dapat memilih cara lain karena penamaan tersebut tidak benar-benar memengaruhi program.

Selanjutnya, blok kode ini memvalidasi account kita:

Kode
// 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

Selanjutnya, kita membuat account mint menggunakan helper create_account:

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

Setelah membuat account mint, kita melakukan deserialisasi data instruction dari byte menjadi tipe Rust:

Kode
    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;

Baris pertama mengonversi data instruction bertipe &[u8] menjadi api/instructions.rs/Create. Tiga baris berikutnya mengonversi field dalam Create yang berupa byte menjadi string dengan helper bytes_to_string.

Perhatikan juga bahwa bytes_to_string menerima parameter generik const (yaitu ::<32>) yang membantu menghasilkan string dengan panjang tepat untuk menghemat compute unit.

Kode
// initialize mint
    initialize_mint(
        mint_info,
        user_info,
        Some(user_info),
        token_program,
        rent_sysvar,
        decimals,
    )?;

Selanjutnya, kita menginisialisasi account mint dengan fungsi helper initialize_mint.

Kode
// 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()?;

Di sini, kita membuat account metadata untuk mint token. Account ini berisi informasi seperti nama, simbol, dan pembuat koleksi.

Setelah menyelesaikan file create.rs, mari jalankan steel build. 

Kita akan melihat error berikut:

Kode
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`

Error tersebut menunjukkan adanya masalah dependensi. Kita dapat memperbaikinya dengan mengedit file /program/Cargo.toml menjadi:

Kode
[dependencies]
...
...
mpl-token-metadata.workspace = true
spl-token.workspace = true

Sekarang, jika kita kembali menjalankan steel build, akan muncul satu error terakhir:

Kode
error[E0425]: cannot find function `initialize_mint` in this scope
  --> program/src/create.rs:57:5
   |
57 |     initialize_mint(
   |     ^^^^^^^^^^^^^^^ not found in this scope

Error ini muncul karena fungsi helper initialize_mint membutuhkan feature spl di Steel agar dapat diakses. Jadi, kita harus memperbarui impornya di file /Cargo.toml:

Kode
[workspace.dependencies]
...
...
steel = { version = "3.0", features = ["spl"] }

Sekarang, jika kita menjalankan steel build, program seharusnya dapat dikompilasi tanpa error. 

Selamat, Anda telah berhasil sampai sejauh ini!

Satu langkah terakhir: kita harus menguji program.

Menguji Program Steel Anda

Secara default, pengujian di Steel ditulis dalam Rust. Steel menggunakan solana-program-test untuk pengujian, tetapi Anda dapat menggunakan liteSVM atau mollusk jika lebih menyukainya.

Pengujian ditulis dalam /program/tests/test.rs.

Mari mulai dengan memperbaruinya menggunakan:

Kode
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);
}

File pengujian kita terdiri dari dua fungsi, setup dan run_test.

Kita melakukan tiga hal penting dalam fungsi setup:

  1. Kita membuat instance ProgramTest, yang secara default sudah berisi program create_token_program
  2. Kita menambahkan program token_metadata ke instance ProgramTest karena program token Metaplex yang kita gunakan secara default bukan bagian dari ProgramTest
  3. Kita memulai instance ProgramTest dengan metode start, yang mengembalikan tuple (BanksClient, Keypair, Hash)
Kode
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
}

Pada bagian pertama run_test, kita memanggil fungsi setup dan membuat Keypair untuk mint token.

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

Selanjutnya, kita menyiapkan data instruction.

Karena instruction create memerlukan representasi byte, kita menggunakan helper string_to_bytes untuk mengonversi string menjadi byte.

Kode
   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;

Ingat, di folder api kita mengimplementasikan fungsi yang tidak digunakan dalam logika program, yaitu fungsi create dalam api/src/sdk.rs?

Itulah fungsi yang pertama kali kita panggil dalam blok kode di bawah untuk membuat instance Instruction. Kita meneruskannya ke instance Transaction dengan Transaction::new_signed_with_payer, lalu meneruskan transaksi ke banks.process_transaction(tx).await; untuk diproses.

assert!(res.is_ok()); mengonfirmasi bahwa transaksi telah diproses.

Kode
// 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());

Sejauh ini, kita telah mengeksekusi instruction dalam lingkungan pengujian (ProgramTest).

Sekarang, mari uji apakah instruction tersebut dieksekusi dengan benar:

Kode
// 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);

Kita dapat menulis lebih banyak assertion untuk memeriksa hal lain, seperti data yang disimpan dalam account metadata, tetapi demi kesederhanaan kita akan berhenti di sini.

Jika tertarik, Anda dapat menambahkannya sendiri. Jika membutuhkan bantuan, lihat panduan developer Solana untuk pengujian Steel ini.

Setelah menyelesaikan file pengujian, mari jalankan perintah pengujian—steel test. 

Sayangnya, pengujian akan gagal karena kita tidak memiliki kode sumber/file ELF untuk program mpl_token_metadata.

Jangan khawatir, kita dapat memperbaikinya dengan menjalankan:

Kode
// 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

Sekarang, jika menjalankan steel test, kita akan mendapatkan hasil berikut:

Kode
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

Sekali lagi, selamat!

Jika mendapatkan output yang sama, program Anda telah lulus pengujian.

Kesimpulan

Steel adalah framework pengembangan modular dan ringan untuk membangun program Solana yang cerdas dan dioptimalkan untuk performa. Artikel ini menjelaskan cara kerja Steel, membandingkan Steel dengan Anchor dan Pinocchio, serta memandu Anda melalui contoh pembuatan token baru menggunakan Steel.

Referensi Tambahan

Untuk terus mempelajari Steel dan pengembangan program Solana, jelajahi referensi berikut:

Berlangganan Helius

Ikuti perkembangan terbaru dalam pengembangan Solana dan dapatkan pembaruan saat kami memublikasikan postingan