MỚI: Helius mua lại Light Protocol
framework Steel để viết hợp đồng thông minh Solana
Blog/Phát triển

Cách viết chương trình Solana bằng Steel

Nhà phát triển Solana, Assylm LabsPerelyn trên XPerelyn trên LinkedIn
Đọc trong 15 phút

Steel là framework gọn nhẹ, có tính mô-đun để viết các chương trình Solana native với lượng mã soạn sẵn tối thiểu và khả năng kiểm soát tối đa. Được Hardhat Chad (từ Ore) xây dựng, Steel dành cho các nhà phát triển muốn có hiệu năng của Rust native mà không phải đánh đổi trải nghiệm phát triển.

Trong bài viết này, bạn sẽ tìm hiểu:

  • Steel là gì và có mối liên hệ như thế nào với Anchor và Pinocchio
  • Cách định nghĩa instruction và cấu trúc một dự án Steel
  • Cách tạo token SPL tùy chỉnh bằng Steel
  • Cách kiểm thử chương trình bằng solana-program-test

Điều kiện tiên quyết

Hướng dẫn này giả định rằng bạn đã quen thuộc với:

  • Cú pháp và bộ công cụ Rust cơ bản
  • Kiến thức nền tảng về phát triển Solana (account, instruction, chương trình)
  • Cách sử dụng CLI (ví dụ: cargo, solana, curl)

Nếu đã có thể viết các chương trình Solana hoặc Rust cơ bản, bạn đã sẵn sàng xây dựng với Steel.

Steel là gì?

Steel là framework mô-đun mới để xây dựng chương trình trên Solana, cho phép nhà phát triển viết chương trình với ít mã soạn sẵn hơn và ít áp đặt hơn so với Anchor.

Steel cung cấp các macro và trình trợ giúp Cross-Program Invocation (CPI) giúp đẩy nhanh quá trình phát triển chương trình Solana theo cách gần với native (không dùng framework), nghĩa là bạn có được hiệu năng gần như native cùng trải nghiệm phát triển tốt hơn.

Hãy khám phá một số macro và trình trợ giúp mà Steel cung cấp.

Macro của Steel

Một số macro do Steel cung cấp gồm:

account!

Macro account! định nghĩa các kiểu Account trong Steel, đồng thời cho phép chúng truy cập trait AccountValidation, trait này cung cấp các trình trợ giúp để xác thực trạng thái account trong quá trình phát triển.

instruction!

Macro instruction! định nghĩa các kiểu Instruction trong Steel, đồng thời cho phép chúng truy cập hàm to_bytes sẽ được sử dụng trong api/src/sdk.

Các macro khác trong Steel gồm error và event; như tên gọi, chúng lần lượt được dùng cho lỗi và sự kiện.

Trình trợ giúp CPI của Steel

Steel cung cấp các hàm trợ giúp mà hầu hết nhà phát triển cần khi thực hiện Cross-Program Invocation (CPI) trong chương trình, chẳng hạn như các instruction từ system_program, bao gồm create_account, transfer và nhiều instruction khác. 

Steel cũng bao gồm các instruction từ spl_token_program / spl_associated_token_program, trong đó có mint_to, burn, create_associated_token_account và nhiều instruction khác.

Tối ưu hóa CU

Bạn có thể cho rằng Steel tiết kiệm CU nhờ những gì nó thực hiện — nhưng thực tế, Steel hiệu quả nhờ những gì nó không thực hiện. Vì framework Steel gọn nhẹ và hầu như không tạo thêm chi phí cho chương trình Solana, nó tối ưu ngang với các chương trình Solana viết bằng Rust native, thậm chí còn hơn nhờ sử dụng bytemuck làm trình tuần tự hóa dữ liệu mặc định.

Steel so với Anchor

Anchor là một framework mạnh mẽ, có tính định hướng cao, được thiết kế để nhanh chóng xây dựng các chương trình Solana an toàn. Framework này đơn giản hóa quá trình phát triển bằng cách giảm mã soạn sẵn cho các phần như tuần tự hóa và giải tuần tự hóa account cũng như dữ liệu instruction, thực hiện các bước kiểm tra bảo mật thiết yếu, tự động tạo thư viện client và cung cấp môi trường kiểm thử toàn diện.

Điểm khác biệt chính giữa Steel và Anchor là gì?

Anchor là framework hợp đồng thông minh thân thiện với người mới bắt đầu, cho phép nhà phát triển Solana ở mọi cấp độ nhanh chóng viết chương trình Solana. Anchor tập trung vào trải nghiệm phát triển trực quan và thân thiện, vì vậy rất nhiều nhà phát triển Solana dựa vào framework này.

Tuy nhiên, sự đơn giản này cũng đi kèm cái giá nhất định.

Anchor đã tích lũy thêm chi phí khiến binary của chương trình Solana trở nên cồng kềnh hơn, ảnh hưởng tiêu cực đến hiệu năng on-chain. Ví dụ, chi phí triển khai chương trình Solana và gọi instruction sẽ tăng lên.

Nhờ tốc độ và hiệu quả của Solana, ngay cả khi Anchor làm tăng chi phí cho các chương trình Solana, hầu hết mọi người đều không nhận thấy. Ngoại lệ là những người phát triển các chương trình phức tạp hơn như Ore và Code-vm, nơi chi phí này có thể khiến chúng không thể sử dụng on-chain.

Thông thường, các chương trình Solana như vậy sẽ được xây dựng bằng Rust native, nhưng đội ngũ duy trì hiểu rằng việc này khó khăn đến mức nào. Họ cần một framework thân thiện hơn, tương tự Anchor, nhưng vẫn có hiệu năng cao như Rust native.

Lợi ích và sự đánh đổi giữa Steel và Anchor

Dù làm tăng chi phí cho các chương trình Solana, Anchor vẫn mang lại trải nghiệm phát triển tốt nhất trong hệ sinh thái Solana và vẫn là framework được khuyến nghị cho các nhà phát triển Solana mới.

Cú pháp của Anchor dễ hiểu và framework này cung cấp Interface Definition Language (IDL), giúp dễ dàng kiểm thử các chương trình Solana bằng những ngôn ngữ khác như JavaScript, đồng thời phát triển các ứng dụng phía client giao tiếp với chương trình Solana.

IDL của Anchor mạnh đến mức các công cụ như Codama có thể dùng chúng để tự động tạo client, giao diện dòng lệnh (CLI) và tài liệu cho chương trình Solana.

IDL là một tính năng mà framework Steel hiện chưa có. Dù cú pháp thân thiện với nhà phát triển, Steel đòi hỏi nhà phát triển phải khá thành thạo Rust.

Mặc dù Anchor được khuyến nghị cho nhà phát triển mới, framework này có thể hạn chế những nhà phát triển có chuyên môn kỹ thuật cao hơn vì nó che giấu cơ chế bên trong của quá trình phát triển chương trình Solana qua các macro và cú pháp. 

Mặt khác, Steel cho phép nhà phát triển truy cập mọi thành phần của chương trình Solana ở cấp độ nguyên thủy nhất. Mức độ chi tiết này đặc biệt hữu ích trong quá trình kiểm thử vì mặc định các bài kiểm thử được viết bằng Rust, mang lại trải nghiệm gỡ lỗi trực tiếp.

Steel là một framework hợp đồng thông minh tuyệt vời nhờ bản chất của nó (tức một lớp bọc tối giản quanh Rust native) và những gì nó không có (tức cú pháp bổ sung gây ra chi phí).

Nói đơn giản, Steel là phiên bản Rust native thân thiện hơn với nhà phát triển, giữ nguyên sức mạnh mà không làm giảm hiệu quả.

Steel so với Pinocchio

Pinocchio là thư viện không có dependency để tạo chương trình Solana bằng Rust. Febo viết thư viện này như một dự án phụ, sau đó nó trở thành một dự án chính thức của Anza. Pinocchio tận dụng cách các loader SVM tuần tự hóa tham số đầu vào của chương trình thành một mảng byte, rồi truyền mảng đó đến điểm vào của chương trình để định nghĩa các kiểu zero-copy dùng cho việc đọc đầu vào.

Nói đơn giản, Pinocchio là phiên bản tinh gọn hơn của solana_program, không phụ thuộc vào bất kỳ crate bên ngoài nào và tránh sử dụng các kiểu động.

Kể từ khi Pinocchio ra mắt, đã có nhiều hiểu lầm về bản chất của nó. Thư viện Pinocchio được tạo ra để thay thế thư viện solana_program — đây không phải đối thủ cạnh tranh của Anchor hay Steel. Nó bổ trợ cho các framework này bằng cách giúp chúng nhẹ hơn.

Thứ mà phần lớn mọi người gọi là chương trình Pinocchio thực chất chỉ là mã Rust native phụ thuộc vào pinocchio thay vì solana_program.

Cách tạo token bằng Steel

Để minh họa cách Steel hoạt động, chúng ta sẽ viết một chương trình Solana đơn giản để tạo token SPL. Nếu thích học qua hình ảnh, bạn có thể xem video sau.

Điều kiện tiên quyết

  • Rust/Cargo
  • Solana
  • Steel

Cài đặt Rust

Bạn có thể cài đặt Rust từ trang web chính thức của Rust hoặc qua CLI:

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

Cài đặt bộ công cụ Solana

Steel cũng yêu cầu bộ công cụ Solana. Có thể cài đặt bản phát hành mới nhất (tức 2.2.15 tại thời điểm viết bài) bằng lệnh sau trên macOS và Linux:

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

Người dùng Windows có thể cài đặt bộ công cụ Solana bằng lệnh sau:

Mã
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"

Tuy nhiên, chúng tôi đặc biệt khuyến nghị sử dụng Windows Subsystem for Linux (WSL) để thay thế. Cách này cho phép chạy môi trường Linux trên máy Windows mà không cần khởi động kép hoặc thiết lập máy ảo riêng. Nếu chọn cách này, hãy làm theo hướng dẫn cài đặt dành cho Linux ở trên (tức lệnh curl).

Nhà phát triển có thể thay v2.2.15 bằng tag phát hành của phiên bản muốn tải xuống, hoặc sử dụng tên kênh stable, beta hay edge. 

Sau khi cài đặt, hãy chạy solana –-version để xác nhận phiên bản solana mong muốn đã được cài đặt.

Cài đặt Steel

Chúng ta có thể cài đặt Steel bằng Cargo qua lệnh:

Mã
cargo install steel-cli

Tạo dự án Steel

Để tạo dự án Steel, chỉ cần chạy:

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

// enter directory
cd create-token

Thư mục token sẽ có cấu trúc như sau:

Mã
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

Bố cục mặc định của dự án Steel gồm hai thư mục có tên api và program.

api chứa các kiểu như state và errors mà chúng ta sẽ sử dụng khi triển khai chương trình Solana, còn thư mục program chứa logic chương trình. 

Khi phát triển chương trình bằng Steel, nên bắt đầu với thư mục api vì thư mục program phụ thuộc vào nó.

Xóa các mô-đun state, const và error

Trong thư mục api có một số mô-đun chúng ta sẽ không dùng cho dự án create-token, chẳng hạn như state, const và error, vì vậy hãy xóa chúng. 

Có thể xóa các mô-đun Steel bằng cách chạy những lệnh sau:

Mã
# 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/>)

Sau khi xóa các mô-đun, chúng ta phải cập nhật tệp api/src/lib.rs vì tệp này gọi các mô-đun đó.

Cập nhật api/src/lib.rs như sau:

Mã
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");

Định nghĩa instruction trong Steel

Trong Steel, các instruction được định nghĩa trong api/src/instructions.rs. Mọi instruction của một chương trình Steel đều được định nghĩa trong một enum và mỗi instruction là một struct.

Enum chứa tất cả instruction có dạng như sau:

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

Nếu một instruction không yêu cầu đối số, chẳng hạn Initialize, nó sẽ không có trường nào. 

Các instruction cần dữ liệu sẽ sử dụng dạng biểu diễn byte. Ví dụ: Add::amount is [u8; 8] ánh xạ đến một u64.

Sau khi định nghĩa enum instruction và struct instruction, chúng ta phải truyền chúng vào macro instruction!, với đối số đầu tiên là enum instruction và đối số thứ hai là struct instruction:

Mã
instruction!(CreateTokenInstruction, Initialize);
instruction!(CreateTokenInstruction, Add);

Chương trình create-token có một instruction nhận bốn đối số, vì vậy api/src/instructions sẽ có dạng như sau:

Mã
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);

Trong Create, các trường name, symbol và uri là chuỗi được biểu diễn dưới dạng mảng byte có kích thước cố định:

  • name: [u8; 16] — dành cho tên dài tối đa 16 byte
  • symbol: [u8; 8] — ký hiệu thường ngắn
  • uri: [u8; 128] — URI thường dài hơn

Các kích thước này phụ thuộc vào độ dài tối đa dự kiến tính bằng byte, không phải ký tự (ví dụ: ký tự UTF-8 nhiều byte có thể cần nhiều dung lượng hơn).

decimals chỉ đơn giản là một u8 vì số chữ số thập phân của token sẽ vừa trong một byte.

Cập nhật SDK

Trong api/src có một tệp tên là sdk.rs. Chúng ta không dùng tệp này khi triển khai logic chương trình, nhưng sẽ dùng nó để chạy kiểm thử hoặc mã client Rust. Tệp này chứa các hàm tạo riêng từng instruction trong chương trình Steel. Vì chương trình chỉ có một instruction nên chúng ta chỉ cần một hàm SDK. Do đó, api/src/sdk.rs sẽ có dạng như sau:

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

Chúng ta có một hàm tên create nhận năm đối số: user là khóa công khai của account sẽ gọi instruction này, mint là khóa công khai của account sẽ đại diện cho token mint, còn name, symbol, uri và decimals đều là dữ liệu sẽ được sử dụng khi triển khai logic chương trình mà chúng ta đã định nghĩa trong api/src/instructions::Create.

Chúng ta cần lưu trữ metadata của token và sẽ dùng chương trình Metaplex Metadata để thực hiện việc này. Trước tiên, hãy thêm:

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

Trong khối mã này, chúng ta đang cố lấy Program Derived Address (PDA), nơi sẽ lưu trữ metadata của token. Để dẫn xuất địa chỉ cần thiết, chúng ta cần các seed sau:

  • Chuỗi “metadata” dưới dạng byte (tức "metadata".as_bytes())
  • ID chương trình của chương trình metadata dưới dạng slice (tức mpl_token_metadata::ID.as_ref())
  • Khóa công khai mint dưới dạng slice (tức mint.as_ref())

Tất cả đầu vào này kết hợp thành các seed. Với đối số thứ hai của Pubkey::find_program::address, chúng ta chỉ cần ID chương trình của chương trình Metadata.

Trong khối mã cuối cùng, chúng ta trả về kiểu Instruction đại diện cho instruction này.

Kiểu Instruction có dạng như sau:

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

 Kiểu Instruction là một struct có ba trường:

  • program_id 
  • accounts
  • data

Trong khối này, chúng ta khai báo một instance của Instruction phù hợp với instruction của chương trình.

Để lấy program_id từ api/src/lib.rs, hãy dùng:

Mã
program_id: crate::ID 

Trường accounts là một vector metadata của Account (tức Vec<AccountMeta>), vì vậy chúng ta phải khai báo tất cả account sẽ được sử dụng trong instruction này:

Mã
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),
        ],

Cuối cùng, trường data biểu diễn các đối số sẽ dùng cho những instruction này dưới dạng byte:

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

Bây giờ chúng ta đã hoàn tất thư mục api.

Tiếp theo, hãy thêm các dependency cần thiết rồi chuyển sang thư mục program.

Thêm dependency cho Steel

Hiện tại, nếu chạy steel build để biên dịch chương trình, quá trình sẽ thất bại với các lỗi sau:

Mã
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`

Điều này cho thấy chúng ta đang thiếu các crate spl_token và mpl_token_metadata cần thiết cho chương trình.

Để thêm các crate còn thiếu, hãy thêm nội dung sau vào tệp /Cargo.toml:

Mã
// /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

Trong /api/Cargo.toml, hãy thêm: 

Mã
// /api/Cargo.toml

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

Bây giờ, nếu chạy steel build, các lỗi dependency sẽ biến mất.

Tuy nhiên, vì chúng ta đã xóa mã trong thư mục api mà thư mục program phụ thuộc vào, một số lỗi như sau vẫn sẽ xuất hiện:

Mã
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`

Đừng lo, chúng ta sẽ khắc phục những lỗi này trong phần tiếp theo.

Triển khai logic chương trình bằng Steel

Theo mặc định, dự án Steel có hai thư mục: api và program. Chúng ta vừa định nghĩa các kiểu cần thiết cho chương trình trong thư mục api. Bây giờ, chúng ta phải triển khai logic chương trình trong thư mục program.

Để bắt đầu, hãy cập nhật /program/lib.rs bằng:

Mã
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);

Trong tệp này, chúng ta định nghĩa hàm process_instruction chính rồi truyền hàm đó vào macro entrypoint!. Macro sẽ tạo mã soạn sẵn cần thiết để runtime Solana gọi logic chương trình.

Bên trong hàm process_instruction có hai khối mã quan trọng cần được giải thích.

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

parse_instruction phân tích một instruction từ dữ liệu instruction. Điều này có nghĩa là với dữ liệu được truyền vào chương trình, chúng ta có thể xác định instruction cần gọi. 

Nó trả về một tuple gồm instruction(ix) và instruction data(data) trong trường hợp Ok().

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

Sau khi lấy instruction(ix) từ parse_instruction, chúng ta dùng match để chọn instruction cần gọi. Ở đây chỉ có một nhánh match vì chương trình chỉ có một instruction. 

Giờ đây, logic chương trình đã được thiết lập để gọi đúng instruction khi được kích hoạt. Tuy nhiên, process_create và mod create chưa tồn tại, vì vậy hãy tạo chúng.

Trong terminal, hãy chạy:

Mã
// 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 

Bây giờ hãy cập nhật program/src/create.rs bằng:

Mã
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(())
}

Hãy cùng xem điều gì đang diễn ra ở đây.

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

Trong khối mã này, chúng ta tải các account cần thiết cho instruction. Nếu những account được truyền vào không khớp với các account đã định nghĩa, khối này sẽ báo lỗi ProgramError::NotEnoughAccountKeys.

Nếu quan sát kỹ, bạn sẽ nhận thấy một quy tắc trong cách đặt tên account:

  • Account “thông thường” kết thúc bằng info
  • Account chương trình kết thúc bằng program
  • Sysvar kết thúc bằng sysvar

Đây là cách đặt tên account mang tính định hướng trong Steel. Bạn có thể chọn cách khác vì nó không thực sự ảnh hưởng đến chương trình.

Tiếp theo, khối mã này xác thực các account:

Mã
// 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

Tiếp theo, chúng ta tạo account mint bằng trình trợ giúp create_account:

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

Sau khi tạo các account mint, chúng ta giải tuần tự hóa dữ liệu instruction từ byte thành các kiểu Rust:

Mã
    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;

Dòng đầu tiên chuyển đổi dữ liệu instruction từ kiểu &[u8] thành api/instructions.rs/Create, còn ba dòng tiếp theo chuyển các trường dạng byte trong Create thành chuỗi bằng trình trợ giúp bytes_to_string.

Ngoài ra, hãy lưu ý rằng bytes_to_string nhận một tham số generic const (tức ::<32>), giúp tạo chuỗi có độ dài chính xác để tiết kiệm đơn vị tính toán.

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

Tiếp theo, chúng ta khởi tạo account mint bằng hàm trợ giúp initialize_mint.

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

Ở đây, chúng ta đã tạo account metadata cho token mint. Account này chứa thông tin như tên, ký hiệu và người tạo bộ sưu tập.

Giờ đây khi đã hoàn tất tệp create.rs, hãy chạy steel build. 

Chúng ta sẽ thấy các lỗi sau:

Mã
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`

Các lỗi này cho thấy chúng ta đang gặp vấn đề về dependency. Có thể khắc phục bằng cách cập nhật tệp /program/Cargo.toml với nội dung sau:

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

Bây giờ, nếu chạy lại steel build, chúng ta sẽ gặp một lỗi cuối cùng:

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

Lỗi này xảy ra vì cần tính năng spl trong Steel để truy cập hàm trợ giúp initialize_mint. Vì vậy, chúng ta phải cập nhật phần import của hàm trong tệp /Cargo.toml:

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

Bây giờ, nếu chạy steel build, chương trình sẽ biên dịch mà không gặp lỗi. 

Chúc mừng bạn đã tiến đến đây!

Còn một bước cuối cùng: chúng ta phải kiểm thử chương trình.

Kiểm thử chương trình Steel

Theo mặc định, các bài kiểm thử trong Steel được viết bằng Rust. Steel sử dụng solana-program-test để kiểm thử, nhưng bạn có thể dùng liteSVM hoặc mollusk nếu muốn.

Các bài kiểm thử được viết trong /program/tests/test.rs.

Hãy bắt đầu bằng cách cập nhật tệp với nội dung sau:

Mã
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);
}

Tệp kiểm thử gồm hai hàm: setup và run_test.

Chúng ta thực hiện ba việc quan trọng trong hàm setup:

  1. Tạo một instance của ProgramTest, trong đó chương trình create_token_program được thêm sẵn theo mặc định
  2. Thêm chương trình token_metadata vào instance ProgramTest vì chương trình token Metaplex mà chúng ta sử dụng không nằm trong ProgramTest theo mặc định
  3. Khởi chạy một instance của ProgramTest bằng phương thức start, phương thức này trả về tuple (BanksClient, Keypair, Hash)
Mã
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
}

Trong phần đầu của run_test, chúng ta gọi hàm setup và tạo một Keypair cho token mint.

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

Tiếp theo, chúng ta chuẩn bị dữ liệu instruction.

Vì instruction create cần dạng biểu diễn byte, chúng ta sử dụng trình trợ giúp string_to_bytes để chuyển đổi chuỗi thành byte.

Mã
   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;

Bạn còn nhớ trong thư mục api, chúng ta đã triển khai một hàm không dùng trong logic chương trình, đó là hàm create trong api/src/sdk.rs không?

Đó chính là hàm được gọi đầu tiên trong khối mã bên dưới để tạo một instance của Instruction. Chúng ta truyền instance này vào instance Transaction bằng Transaction::new_signed_with_payer, rồi truyền giao dịch tới banks.process_transaction(tx).await; để xử lý.

assert!(res.is_ok()); xác nhận rằng giao dịch đã được xử lý.

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

Cho đến lúc này, chúng ta đã thực thi instruction trong môi trường kiểm thử (ProgramTest).

Bây giờ, hãy kiểm tra xem instruction có được thực thi đúng cách hay không:

Mã
// 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);

Chúng ta có thể viết thêm các assertion để kiểm tra những yếu tố khác, chẳng hạn dữ liệu được lưu trong account metadata, nhưng để đơn giản, chúng ta sẽ dừng tại đây.

Nếu muốn, bạn có thể tự thêm các assertion đó. Nếu cần trợ giúp, hãy xem hướng dẫn kiểm thử Steel dành cho nhà phát triển Solana.

Giờ đây khi đã hoàn tất tệp kiểm thử, hãy chạy lệnh kiểm thử — steel test. 

Rất tiếc, quá trình sẽ thất bại vì chúng ta không có mã nguồn/tệp ELF cho chương trình mpl_token_metadata.

Đừng lo, chúng ta có thể khắc phục bằng cách chạy:

Mã
// 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

Bây giờ, nếu chạy steel test, chúng ta sẽ nhận được kết quả sau:

Mã
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

Một lần nữa, xin chúc mừng!

Nếu nhận được cùng kết quả, chương trình của bạn đã vượt qua các bài kiểm thử.

Kết luận

Steel là framework phát triển mô-đun và gọn nhẹ để xây dựng các chương trình Solana thông minh, được tối ưu hóa về hiệu năng. Bài viết này đã giải thích cách Steel hoạt động, so sánh Steel với Anchor và Pinocchio, đồng thời hướng dẫn qua một ví dụ về cách tạo token mới bằng Steel.

Tài nguyên bổ sung

Để tiếp tục tìm hiểu về Steel và hoạt động phát triển chương trình Solana, hãy khám phá các tài nguyên sau:

Đăng ký nhận tin từ Helius

Luôn cập nhật những thông tin mới nhất về phát triển Solana và nhận thông báo khi chúng tôi đăng bài