
So schreibst du Solana-Programme mit Steel
Inhaltsverzeichnis
Steel ist ein leichtgewichtiges, modulares Framework zum Schreiben nativer Solana-Programme mit minimalem Boilerplate-Code und maximaler Kontrolle. Steel wurde von Hardhat Chad (von Ore) entwickelt und richtet sich an Entwickler, die die Leistung von nativem Rust nutzen möchten, ohne Abstriche bei der Developer Experience zu machen.
In diesem Artikel erfährst du:
- Was Steel ist und wie es mit Anchor und Pinocchio zusammenhängt
- Wie du Instructions definierst und ein Steel-Projekt strukturierst
- Wie du mit Steel einen eigenen SPL-Token erstellst
- Wie du dein Programm mit
solana-program-testtestest
Voraussetzungen
Dieser Leitfaden setzt voraus, dass du mit Folgendem vertraut bist:
- Grundlegende Rust-Syntax und Toolchain
- Grundlagen der Solana-Entwicklung (Accounts, Instructions, Programme)
- Verwendung der CLI (z. B. cargo, solana, curl)
Wenn du grundlegende Solana- oder Rust-Programme schreiben kannst, bist du bereit für Steel.
Was ist Steel?
Steel ist ein neues modulares Framework zum Erstellen von Programmen auf Solana. Entwickler können damit Programme mit weniger Boilerplate-Code schreiben, und es macht weniger Vorgaben als Anchor.
Steel bietet Makros und Hilfsfunktionen für Cross-Program Invocations (CPI), mit denen du die Entwicklung von Solana-Programmen auf native Weise beschleunigen kannst (ohne Framework). So erhältst du native Leistung und zugleich eine bessere Developer Experience.
Sehen wir uns einige der Makros und Hilfsfunktionen von Steel an.
Steel-Makros
Zu den von Steel angebotenen Makros gehören:
account!
Das Makro account! definiert Account-Typen in Steel und gewährt ihnen außerdem Zugriff auf den Trait AccountValidation. Dieser stellt Hilfsfunktionen bereit, um den Zustand von Accounts während der Entwicklung zu validieren.
instruction!
Das Makro instruction! definiert Instruction-Typen in Steel und gewährt ihnen außerdem Zugriff auf eine to_bytes-Funktion, die in api/src/sdk verwendet wird.
Weitere Makros in Steel sind error und event. Wie ihre Namen vermuten lassen, werden sie für Fehler beziehungsweise Events verwendet.
Steel-CPI-Hilfsfunktionen
Steel stellt Hilfsfunktionen bereit, die Entwickler für die meisten Cross-Program Invocations (CPIs) benötigen. Dazu gehören Instructions aus system_program wie create_account, transfer und weitere.
Außerdem enthält es Instructions aus spl_token_program / spl_associated_token_program, darunter mint_to, burn, create_associated_token_account und weitere.
Damit du in Steel auf die CPI-Hilfsfunktionen von spl_token_program und spl_associated_token_program zugreifen kannst, musst du das spl-Feature-Flag aktivieren.
CU-Optimierungen
Du könntest erwarten, dass Steel aufgrund dessen, was es tut, CU-effizient ist. Tatsächlich ist es aber effizient, weil es bestimmte Dinge nicht tut. Das Steel-Framework ist leichtgewichtig und fügt Solana-Programmen kaum oder gar keinen Overhead hinzu. Daher ist es genauso optimal wie in nativem Rust geschriebene Solana-Programme – und dank bytemuck als standardmäßigem Datenserialisierer sogar noch besser.
Steel vs. Anchor
Anchor ist ein leistungsstarkes Framework mit klaren Vorgaben, mit dem du schnell sichere Solana-Programme erstellen kannst. Es vereinfacht die Entwicklung, indem es Boilerplate-Code etwa für die (De-)Serialisierung von Accounts und Instruction-Daten reduziert, wichtige Sicherheitsprüfungen durchführt, automatisch Client-Bibliotheken generiert und eine umfassende Testumgebung bereitstellt.
Was ist der wichtigste Unterschied zwischen Steel und Anchor?
Anchor ist ein einsteigerfreundliches Smart-Contract-Framework, mit dem Solana-Entwickler unabhängig von ihrer Erfahrung schnell Solana-Programme schreiben können. Anchor legt den Fokus auf eine intuitive, zugängliche Developer Experience. Deshalb setzen so viele Solana-Entwickler darauf.
Diese Einfachheit hat jedoch ihren Preis.
Anchor hat im Laufe der Zeit Overhead angesammelt, der die Binärdateien von Solana-Programmen aufbläht und ihre On-Chain-Leistung beeinträchtigt. Dadurch steigen beispielsweise die Kosten für das Deployment von Solana-Programmen und den Aufruf von Instructions.
Solana ist so schnell und effizient, dass die meisten Menschen den von Anchor verursachten Overhead nicht bemerken. Eine Ausnahme sind Entwickler komplexerer Programme wie Ore und Code-vm, bei denen der Overhead die On-Chain-Nutzung unmöglich machen würde.
Solche Solana-Programme würden normalerweise mit nativem Rust erstellt. Ihre Maintainer wissen jedoch, wie schwierig das wäre, und benötigen ein zugänglicheres Framework, das mit Anchor vergleichbar und zugleich so leistungsstark wie natives Rust ist.
Vorteile und Kompromisse von Steel und Anchor
Trotz des Overheads, den Anchor Solana-Programmen hinzufügt, bietet es die beste Developer Experience im Solana-Ökosystem und bleibt das empfohlene Framework für neue Solana-Entwickler.
Die Syntax von Anchor ist leicht verständlich. Zudem stellt es Interface Definition Languages (IDLs) bereit. Damit kannst du Solana-Programme einfach in anderen Sprachen wie JavaScript testen und clientseitige Anwendungen entwickeln, die mit Solana-Programmen kommunizieren.
Anchor-IDLs sind so leistungsfähig, dass Tools wie Codama daraus automatisch Clients, Command Line Interfaces (CLIs) und Dokumentation für Solana-Programme generieren können.
Das Steel-Framework bietet derzeit keine IDLs. Zwar ist seine Syntax entwicklerfreundlich, doch Steel setzt gute Rust-Kenntnisse voraus.
Anchor wird neuen Entwicklern empfohlen, kann technisch versiertere Entwickler jedoch einschränken, weil es die internen Abläufe der Solana-Programmentwicklung hinter Makros und seiner Syntax verbirgt.
Steel hingegen gibt Entwicklern auf der grundlegendsten Ebene Zugriff auf alle Bestandteile eines Solana-Programms. Diese Granularität ist besonders beim Testen hilfreich. Tests werden standardmäßig in Rust geschrieben und ermöglichen direktes Debugging.
Steel ist ein hervorragendes Smart-Contract-Framework – sowohl wegen dessen, was es ist (ein minimaler Wrapper um natives Rust), als auch wegen dessen, was es nicht ist (zusätzliche Syntax, die Overhead verursacht).
Einfach gesagt: Steel ist eine entwicklerfreundlichere Version von nativem Rust, die dessen Leistungsfähigkeit ohne Effizienzeinbußen bewahrt.
Steel vs. Pinocchio
Pinocchio ist eine Bibliothek ohne Abhängigkeiten, mit der du Solana-Programme in Rust erstellen kannst. Febo entwickelte sie als Nebenprojekt, später wurde sie zu einem vollwertigen Anza-Projekt. Sie nutzt die Art, wie SVM-Loader Programmeingabeparameter in ein Byte-Array serialisieren und anschließend an den Entry Point des Programms übergeben, um Zero-Copy-Typen zum Lesen der Eingabe zu definieren.
Einfach gesagt ist Pinocchio eine schlankere Version von solana_program, die keine externen Crates benötigt und dynamische Typen vermeidet.
Seit der Veröffentlichung von Pinocchio gibt es viele Missverständnisse darüber, was es ist. Die Pinocchio-Bibliothek soll die Bibliothek solana_program ersetzen. Sie konkurriert nicht mit Anchor oder Steel, sondern ergänzt diese Frameworks, weil sie sie schlanker macht.
Was die meisten als Pinocchio-Programm bezeichnen, ist lediglich nativer Rust-Code, der von pinocchio statt von solana_program abhängt.
So erstellst du mit Steel einen Token
Um zu zeigen, wie Steel funktioniert, schreiben wir ein einfaches Solana-Programm, das einen SPL-Token erstellt. Wenn du lieber visuell lernst, kannst du dir das folgende Video ansehen.
Voraussetzungen
- Rust/Cargo
- Solana
- Steel
Rust installieren
Du kannst Rust über die offizielle Rust-Website oder die CLI installieren:
curl --proto '=https' --tlsv1.2 -sSf <https://sh.rustup.rs> | shSolana Tool Suite installieren
Steel benötigt außerdem die Solana Tool Suite. Die zum Zeitpunkt der Artikelerstellung aktuelle Version 2.2.15 kannst du unter macOS und Linux mit folgendem Befehl installieren:
sh -c "$(curl -sSfL <https://release.anza.xyz/v2.2.14/install>)"Unter Windows kannst du die Solana Tool Suite mit folgendem Befehl installieren:
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"Wir empfehlen jedoch dringend, stattdessen das Windows Subsystem for Linux (WSL) zu verwenden. Damit kannst du eine Linux-Umgebung auf deinem Windows-Rechner ausführen, ohne ein Dual-Boot-System oder eine separate virtuelle Maschine einzurichten. Wenn du diesen Weg wählst, folge den Installationsanweisungen für Linux, also dem curl-Befehl.
Entwickler können v2.2.15 durch das Release-Tag der gewünschten Version ersetzen oder die Kanalnamen stable, beta oder edge verwenden.
Führe nach der Installation solana –-version aus, um zu prüfen, ob die gewünschte Version von solana installiert ist.
Steel installieren
Wir können Steel mit Cargo installieren:
cargo install steel-cliSteel-Projekt erstellen
Ein Steel-Projekt erstellst du ganz einfach mit:
// creates a new Steel project named `create-token`
steel new token
// enter directory
cd create-tokenUnser Verzeichnis token sollte so aussehen:
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
Das Standardlayout eines Steel-Projekts enthält zwei Ordner namens api und program.
api enthält Typen wie state und errors, die wir beim Implementieren unseres Solana-Programms verwenden. Der Ordner program enthält die Programmlogik.
Bei der Entwicklung von Programmen mit Steel solltest du mit dem Ordner api beginnen, da der Ordner program davon abhängt.
Module state, const und error entfernen
Im Ordner api gibt es einige Module, die wir für unser Projekt create-token nicht verwenden werden, etwa state, const und error. Entfernen wir sie.
Mit den folgenden Befehlen kannst du Steel-Module entfernen:
# 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/>)Nach dem Löschen der Module müssen wir unsere Datei api/src/lib.rs aktualisieren, da sie diese Module aufruft.
Aktualisiere api/src/lib.rs wie folgt:
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");Instructions in Steel definieren
In Steel werden Instructions in api/src/instructions.rs definiert. Alle Instructions eines Steel-Programms werden in einem Enum definiert, und jede Instruction ist ein Struct.
Das Enum mit allen Instructions sieht so aus:
#[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]
}Wenn eine Instruction wie Initialize keine Argumente benötigt, hat sie keine Felder.
Instructions, die Daten benötigen, verwenden eine Byte-Darstellung. Ein Beispiel ist Add::amount is [u8; 8], das einem u64 zugeordnet wird.
Nachdem wir unser Instruction-Enum und Instruction-Struct definiert haben, müssen wir beide an das Makro instruction! übergeben. Das erste Argument ist das Instruction-Enum, das zweite das Instruction-Struct:
instruction!(CreateTokenInstruction, Initialize);
instruction!(CreateTokenInstruction, Add);Unser Programm create-token hat eine Instruction, die vier Argumente akzeptiert. api/src/instructions sollte daher so aussehen:
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);In Create sind die Felder name, symbol und uri Strings, die als Byte-Arrays fester Größe dargestellt werden:
name: [u8; 16] — für Namen mit bis zu 16 Bytessymbol: [u8; 8] — Symbole sind normalerweise kurzuri: [u8; 128] — URIs sind normalerweise länger
Diese Größen richten sich nach der erwarteten maximalen Länge in Bytes, nicht in Zeichen. UTF-8-Zeichen mit mehreren Bytes können beispielsweise mehr Platz benötigen.
decimals ist einfach ein u8, da die Anzahl der Dezimalstellen des Tokens in ein Byte passt.
SDK aktualisieren
In api/src gibt es eine Datei namens sdk.rs. Wir verwenden sie nicht beim Implementieren unserer Programmlogik, benötigen sie aber zum Ausführen von Tests oder Rust-Clientcode. Sie enthält Funktionen, die jede Instruction eines Steel-Programms einzeln erstellen. Da unser Programm nur eine Instruction hat, benötigen wir auch nur eine SDK-Funktion. Unser api/src/sdk.rs sollte daher so aussehen:
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(),
}
}Wir haben eine Funktion namens create, die fünf Argumente akzeptiert: user ist der öffentliche Schlüssel des Accounts, der diese Instruction aufruft. mint ist der öffentliche Schlüssel des Accounts, der token mint repräsentiert. name, symbol, uri und decimals sind die Daten, die wir beim Implementieren der in api/src/instructions::Create definierten Programmlogik verwenden.
Wir müssen die Metadaten unseres Tokens speichern. Dafür verwenden wir das Metaplex-Metadata-Programm. Zuerst fügen wir Folgendes hinzu:
let metadata = Pubkey::find_program_address(
&[
"metadata".as_bytes(),
mpl_token_metadata::ID.as_ref(),
mint.as_ref(),
],
&mpl_token_metadata::ID,
)
.0;In diesem Codeblock versuchen wir, die Program Derived Address (PDA) zu ermitteln, unter der wir die Metadaten unseres Tokens speichern. Zum Ableiten der benötigten Adresse brauchen wir folgende Seeds:
- Den String „metadata“ als Bytes (also
"metadata".as_bytes()) - Die Programm-ID des Metadata-Programms als Slice (also
mpl_token_metadata::ID.as_ref()) - Den öffentlichen Schlüssel des Mint-Accounts als Slice (also
mint.as_ref())
Alle diese Eingaben bilden zusammen die Seeds. Als zweites Argument von Pubkey::find_program::address benötigen wir nur die Programm-ID des Metadata-Programms.
Im letzten Codeblock geben wir den Typ Instruction zurück, der diese Instruction repräsentiert.
Der Typ Instruction sieht so aus:
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(),
} Der Typ Instruction ist ein Struct mit drei Feldern:
program_idaccountsdata
In diesem Block deklarieren wir eine Instanz von Instruction, die zur Instruction unseres Programms passt.
Verwende Folgendes, um program_id aus api/src/lib.rs abzurufen:
program_id: crate::ID Das Feld accounts ist ein Vektor aus Account-Metadaten (also Vec<AccountMeta>). Daher müssen wir alle Accounts deklarieren, die in dieser Instruction verwendet werden:
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),
],Das Feld data repräsentiert schließlich die Argumente, die wir für diese Instructions als Bytes verwenden:
data: Create {
name,
symbol,
uri,
decimals,
}
.to_bytes(),Damit sind wir mit dem Ordner api fertig.
Als Nächstes fügen wir die erforderlichen Abhängigkeiten hinzu und fahren dann mit dem Ordner program fort.
Steel-Abhängigkeiten hinzufügen
Wenn du jetzt steel build ausführst, um dein Programm zu kompilieren, sollte der Vorgang mit diesen Fehlern fehlschlagen:
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`
Das zeigt, dass die für unser Programm erforderlichen Crates spl_token und mpl_token_metadata fehlen.
Füge der Datei /Cargo.toml Folgendes hinzu, um die fehlenden Crates zu ergänzen:
// /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 = trueFüge in /api/Cargo.toml Folgendes hinzu:
// /api/Cargo.toml
[dependencies]
...
...
mpl-token-metadata.workspace = true
spl-token.workspace = trueWenn wir jetzt steel build ausführen, sollten die Abhängigkeitsfehler verschwunden sein.
Da wir im Ordner api jedoch Code gelöscht haben, von dem der Ordner program abhängt, sehen wir weiterhin Fehler wie diesen:
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`
Keine Sorge, diese Fehler beheben wir im nächsten Abschnitt.
Programmlogik mit Steel implementieren
Steel-Projekte enthalten standardmäßig zwei Ordner: api und program. Wir haben gerade im Ordner api die Typen definiert, die wir in unserem Programm benötigen. Jetzt müssen wir unsere Programmlogik im Ordner program implementieren.
Aktualisiere zunächst /program/lib.rs mit:
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);In dieser Datei definieren wir unsere Hauptfunktion process_instruction, die wir an das Makro entrypoint! übergeben. Das Makro generiert den Boilerplate-Code, den die Solana-Runtime zum Aufrufen unserer Programmlogik benötigt.
Innerhalb der Funktion process_instruction gibt es zwei wichtige Codeblöcke, die wir besprechen müssen.
let (ix, data) = parse_instruction(&create_token_api::ID, program_id, data)?;parse_instruction parst eine Instruction aus den Instruction-Daten. Anhand der an unser Programm übergebenen Daten können wir also bestimmen, welche Instruction aufgerufen werden soll.
Im Fall Ok() gibt die Funktion ein Tupel aus instruction(ix) und instruction data(data) zurück.
match ix {
CreateTokenInstruction::Create => process_create(accounts, data)?,
}Nachdem wir instruction(ix) aus parse_instruction erhalten haben, wählen wir mit match die aufzurufende Instruction aus. Hier gibt es nur einen Match-Arm, da unser Programm nur eine Instruction besitzt.
Unsere Programmlogik ist nun so eingerichtet, dass sie beim Aufruf die richtige Instruction ausführt. process_create und das Mod create existieren jedoch noch nicht. Erstellen wir sie.
Führe in einem Terminal Folgendes aus:
// 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 Aktualisiere nun program/src/create.rs mit:
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(())
}Sehen wir uns Schritt für Schritt an, was hier passiert.
// Load accounts.
let [user_info, mint_info, metadata_info, token_program, token_metadata_program, system_program, rent_sysvar] =
accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
};In diesem Codeblock laden wir die für diese Instruction benötigten Accounts. Wenn die übergebenen Accounts nicht mit den definierten Accounts übereinstimmen, löst dieser Block den Fehler ProgramError::NotEnoughAccountKeys aus.
Wenn du genau hinsiehst, erkennst du ein Muster bei der Benennung der Accounts:
- „Normale“ Accounts enden mit
info - Programm-Accounts enden mit
program - Sysvars enden mit
sysvar
Steel gibt diese Benennung von Accounts vor. Du kannst dich jedoch anders entscheiden, da sie keine tatsächlichen Auswirkungen auf das Programm hat.
Als Nächstes validiert dieser Codeblock unsere Accounts:
// validate
user_info.is_signer()?; // user is a signer
mint_info.is_empty()?.is_signer()?; // mint is empty and is a signer
metadata_info.is_empty()?.is_writable()?; // metadata is empty and is writable
token_program.is_program(&spl_token::ID)?; // token program == spl_token::ID
token_metadata_program.is_program(&mpl_token_metadata::ID)?; // token meatadata == mpl_token_metadata::ID
system_program.is_program(&system_program::ID)?; // system program == system_program::ID
rent_sysvar.is_sysvar(&sysvar::rent::ID)?; // rent sysvar == sysvar::rent::IDSteel bietet einfache Hilfsfunktionen zur verkettbaren Validierung von Accounts.
Als Nächstes erstellen wir den Account mint mit der Hilfsfunktion create_account:
// create mint account
create_account(
user_info,
mint_info,
system_program,
spl_token::state::Mint::LEN,
&token_program.key,
)?;Nach dem Erstellen der mint-Accounts deserialisieren wir die Instruction-Daten aus Bytes in Rust-Typen:
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;Die erste Zeile konvertiert die Instruction-Daten vom Typ &[u8] in api/instructions.rs/Create. Die nächsten drei Zeilen konvertieren die als Bytes vorliegenden Felder in Create mithilfe der Hilfsfunktion bytes_to_string in Strings.
Beachte außerdem, dass bytes_to_string einen konstanten generischen Parameter (also ::<32>) akzeptiert. Dieser hilft, den String mit der exakten Länge zu generieren und so Compute Units zu sparen.
// initialize mint
initialize_mint(
mint_info,
user_info,
Some(user_info),
token_program,
rent_sysvar,
decimals,
)?;Als Nächstes initialisieren wir den Account mint mit der Hilfsfunktion 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()?;Hier haben wir den Account metadata für unseren Token-Mint erstellt. Er enthält Informationen wie den Namen, das Symbol und die Ersteller der Kollektion.
Nachdem wir mit der Datei create.rs fertig sind, führen wir steel build aus.
Wir sollten folgende Fehler sehen:
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`Diese Fehler weisen auf Probleme mit den Abhängigkeiten hin. Wir können sie beheben, indem wir unsere Datei /program/Cargo.toml wie folgt bearbeiten:
[dependencies]
...
...
mpl-token-metadata.workspace = true
spl-token.workspace = trueWenn wir steel build nun erneut ausführen, tritt ein letzter Fehler auf:
error[E0425]: cannot find function `initialize_mint` in this scope
--> program/src/create.rs:57:5
|
57 | initialize_mint(
| ^^^^^^^^^^^^^^^ not found in this scopeDieser Fehler tritt auf, weil für den Zugriff auf die Hilfsfunktion initialize_mint das Feature spl in Steel benötigt wird. Daher müssen wir den Import in der Datei /Cargo.toml aktualisieren:
[workspace.dependencies]
...
...
steel = { version = "3.0", features = ["spl"] }Wenn wir steel build jetzt ausführen, sollte unser Programm fehlerfrei kompilieren.
Glückwunsch, du bist bis hierher gekommen!
Ein letzter Schritt fehlt noch: Wir müssen unser Programm testen.
Dein Steel-Programm testen
Tests in Steel werden standardmäßig in Rust geschrieben. Steel verwendet solana-program-test für Tests. Du kannst aber auch liteSVM oder mollusk verwenden.
Tests werden in /program/tests/test.rs geschrieben.
Aktualisieren wir die Datei zunächst mit:
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);
}
Unsere Testdatei besteht aus zwei Funktionen: setup und run_test.
In der Funktion setup führen wir drei wichtige Schritte aus:
- Wir erstellen eine Instanz von
ProgramTest, der unser Programmcreate_token_programstandardmäßig hinzugefügt ist - Wir fügen unserer Instanz von
ProgramTestdas Programmtoken_metadatahinzu, da das von uns verwendete Metaplex-Tokenprogramm standardmäßig nicht Teil vonProgramTestist - Wir starten mit der Methode
starteine Instanz vonProgramTest. Sie gibt ein Tupel aus (BanksClient,Keypair,Hash) zurück
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
}Im ersten Teil von run_test rufen wir die Funktion setup auf und erstellen eine Keypair für unseren Token-Mint.
// Setup test
let (mut banks, payer, blockhash) = setup().await;
let mint_keypair = Keypair::new();Als Nächstes bereiten wir unsere Instruction-Daten vor.
Da unsere Instruction create Byte-Darstellungen erwartet, konvertieren wir unsere Strings mit der Hilfsfunktion string_to_bytes in 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;Erinnerst du dich an die Funktion im Ordner api, die wir implementiert, aber nicht in unserer Programmlogik verwendet haben – die Funktion create in api/src/sdk.rs?
Genau diese Funktion rufen wir im folgenden Codeblock zuerst auf, um eine Instanz von Instruction zu erstellen. Diese übergeben wir mit Transaction::new_signed_with_payer an unsere Instanz von Transaction. Anschließend übergeben wir unsere Transaktion zur Verarbeitung an banks.process_transaction(tx).await;.
assert!(res.is_ok()); bestätigt, dass die Transaktion verarbeitet wurde.
// 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());Bisher haben wir unsere Instruction in unserer Testumgebung (ProgramTest) ausgeführt.
Testen wir nun, ob sie korrekt ausgeführt wurde:
// 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);Wir könnten weitere Assertions schreiben, die andere Dinge prüfen, etwa die im Account metadata gespeicherten Daten. Der Einfachheit halber belassen wir es dabei.
Wenn du möchtest, kannst du sie selbst hinzufügen. Falls du Hilfe brauchst, sieh dir diesen Solana-Entwicklerleitfaden für Steel-Tests an.
Nachdem unsere Testdatei fertig ist, führen wir den Testbefehl steel test aus.
Leider schlägt er fehl, weil uns der Quellcode beziehungsweise die ELF-Datei für das Programm mpl_token_metadata fehlt.
Keine Sorge, das können wir mit folgendem Befehl beheben:
// 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.soWenn wir nun steel test ausführen, sollten wir folgende Ausgabe erhalten:
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.16sNoch einmal Glückwunsch!
Wenn du dieselbe Ausgabe erhältst, hat dein Programm die Tests bestanden.
Fazit
Steel ist ein modulares und leichtgewichtiges Entwicklungsframework zum Erstellen intelligenter, leistungsoptimierter Solana-Programme. Dieser Artikel hat erklärt, wie Steel funktioniert, Steel mit Anchor und Pinocchio verglichen und anhand eines Beispiels gezeigt, wie du mit Steel einen neuen Token erstellst.
Weitere Ressourcen
Mit diesen Ressourcen kannst du mehr über Steel und die Entwicklung von Solana-Programmen lernen:
- Steel-GitHub-Repository
- Solana Development Bootcamp (GitHub)
- Solana Development Bootcamp (Video)
- Blueshift — Lerne, wie du deine eigenen On-Chain-Programme schreibst
Ähnliche Artikel
Helius abonnieren
Bleib bei der Solana-Entwicklung auf dem Laufenden und erhalte Updates, wenn wir neue Beiträge veröffentlichen


