
Comment écrire des programmes Solana avec Steel
Sommaire
Steel est un framework léger et modulaire qui permet d’écrire des programmes Solana natifs avec un minimum de code répétitif et un contrôle maximal. Créé par Hardhat Chad (d’Ore), Steel s’adresse aux développeurs qui recherchent les performances du Rust natif sans sacrifier l’expérience développeur.
Dans cet article, vous apprendrez :
- Ce qu’est Steel et comment il se positionne par rapport à Anchor et Pinocchio
- Comment définir des instructions et structurer un projet Steel
- Comment créer un token SPL personnalisé avec Steel
- Comment tester votre programme avec
solana-program-test
Prérequis
Ce guide suppose que vous maîtrisez :
- La syntaxe et la chaîne d’outils de base de Rust
- Les fondamentaux du développement Solana (comptes, instructions, programmes)
- L’utilisation de la CLI (par exemple, cargo, solana, curl)
Si vous savez écrire des programmes Solana ou Rust simples, vous êtes prêt à développer avec Steel.
Qu’est-ce que Steel ?
Steel est un nouveau framework modulaire pour créer des programmes sur Solana. Il permet aux développeurs d’écrire moins de code répétitif et impose moins de conventions qu’Anchor.
Steel propose des macros et des utilitaires d’invocation inter-programmes (Cross-Program Invocation, CPI) qui accélèrent le développement de programmes Solana d’une manière proche du natif (sans framework). Vous bénéficiez ainsi de performances similaires au natif et d’une meilleure expérience développeur.
Examinons quelques-unes des macros et des fonctions utilitaires proposées par Steel.
Macros Steel
Voici quelques-unes des macros proposées par Steel :
account!
La macro account! définit les types Account dans Steel et leur donne également accès au trait AccountValidation, qui fournit des utilitaires pour valider l’état des comptes pendant le développement.
instruction!
La macro instruction! définit les types Instruction dans Steel et leur donne également accès à une fonction to_bytes, qui sera utilisée dans api/src/sdk.
Steel propose aussi les macros error et event ; comme leur nom l’indique, elles servent respectivement aux erreurs et aux événements.
Utilitaires CPI de Steel
Steel fournit les fonctions utilitaires dont la plupart des développeurs ont besoin pour les invocations inter-programmes (CPI), notamment les instructions de system_program, parmi lesquelles create_account, transfer et bien d’autres.
Il inclut également les instructions de spl_token_program / spl_associated_token_program, parmi lesquelles mint_to, burn, create_associated_token_account et bien d’autres.
Pour accéder aux utilitaires CPI de spl_token_program et spl_associated_token_program dans Steel, vous devez activer l’indicateur de fonctionnalité spl.
Optimisations des CU
Vous pourriez penser que Steel utilise efficacement les CU en raison de ce qu’il fait, mais son efficacité tient en réalité à ce qu’il ne fait pas. Comme le framework Steel est léger et n’ajoute que peu ou pas de surcharge aux programmes Solana, il est aussi optimal que les programmes Solana écrits en Rust natif, voire davantage grâce à l’utilisation de bytemuck comme sérialiseur de données par défaut.
Steel ou Anchor
Anchor est un framework puissant et structurant conçu pour créer rapidement des programmes Solana sécurisés. Il simplifie le développement en réduisant le code répétitif dans des domaines tels que la (dé)sérialisation des comptes et les données d’instruction, en effectuant des contrôles de sécurité essentiels, en générant automatiquement les bibliothèques clientes et en fournissant un environnement de test complet.
Quelle est la principale différence entre Steel et Anchor ?
Anchor est un framework de smart contracts accessible aux débutants qui permet aux développeurs Solana de tous niveaux d’écrire rapidement des programmes Solana. Anchor privilégie une expérience développeur intuitive et conviviale, ce qui explique pourquoi tant de développeurs Solana l’utilisent.
Cette simplicité a cependant un coût.
Anchor a accumulé une surcharge qui alourdit les fichiers binaires des programmes Solana et nuit à leurs performances on-chain. Elle augmente, par exemple, le coût de déploiement des programmes Solana et d’appel des instructions.
Grâce à la rapidité et à l’efficacité de Solana, la plupart des utilisateurs ne remarquent pas la surcharge ajoutée par Anchor aux programmes Solana. Seuls ceux qui développent des programmes plus complexes comme Ore et Code-vm la ressentent, car elle rendrait ces programmes inutilisables on-chain.
En général, de tels programmes Solana seraient développés en Rust natif. Mais leurs mainteneurs savent à quel point cette tâche peut être complexe et ont besoin d’un framework plus accessible, comparable à Anchor, tout en restant aussi performant que le Rust natif.
Avantages et compromis de Steel et Anchor
Même avec la surcharge qu’il ajoute aux programmes Solana, Anchor offre la meilleure expérience développeur de l’écosystème Solana et reste le framework recommandé aux nouveaux développeurs Solana.
La syntaxe d’Anchor est facile à comprendre. Anchor fournit également des langages de définition d’interface (IDL), qui facilitent le test des programmes Solana dans d’autres langages, comme JavaScript, ainsi que le développement d’applications clientes communiquant avec ces programmes.
Les IDL d’Anchor sont si puissants que des outils comme Codama peuvent les utiliser pour générer automatiquement des clients, des interfaces de ligne de commande (CLI) et la documentation des programmes Solana.
Le framework Steel ne propose pas encore d’IDL. Bien que sa syntaxe soit accessible aux développeurs, Steel exige une bonne maîtrise de Rust.
Même si Anchor est recommandé aux nouveaux développeurs, il peut limiter les profils plus techniques, car ses macros et sa syntaxe masquent le fonctionnement interne du développement de programmes Solana.
Steel donne au contraire accès à tous les aspects d’un programme Solana, jusque dans leurs éléments les plus fondamentaux. Cette granularité est particulièrement utile pendant les tests : ceux-ci sont écrits en Rust par défaut et permettent un débogage direct.
Steel est un excellent framework de smart contracts à la fois pour ce qu’il est, à savoir une enveloppe minimale autour du Rust natif, et pour ce qu’il n’est pas, à savoir une syntaxe supplémentaire génératrice de surcharge.
En résumé, Steel est une version du Rust natif plus accessible aux développeurs, qui conserve sa puissance sans compromettre son efficacité.
Steel ou Pinocchio
Pinocchio est une bibliothèque sans dépendance permettant de créer des programmes Solana en Rust. Febo l’a écrite comme projet personnel avant qu’elle ne devienne un projet Anza à part entière. Elle exploite la façon dont les chargeurs SVM sérialisent les paramètres d’entrée d’un programme dans un tableau d’octets, ensuite transmis au point d’entrée du programme afin de définir des types zero-copy pour lire les entrées.
En résumé, Pinocchio est une version plus légère de solana_program qui ne dépend d’aucun crate externe et évite les types dynamiques.
Depuis la sortie de Pinocchio, de nombreuses idées fausses circulent à son sujet. La bibliothèque Pinocchio vise à remplacer la bibliothèque solana_program ; elle ne concurrence ni Anchor ni Steel. Elle complète ces frameworks en les allégeant.
Ce que la plupart des utilisateurs considèrent comme un programme Pinocchio est simplement du code Rust natif qui dépend de pinocchio plutôt que de solana_program.
Comment créer un token avec Steel
Pour illustrer le fonctionnement de Steel, nous allons écrire un programme Solana simple qui crée un token SPL. Si vous préférez les supports visuels, vous pouvez regarder la vidéo suivante.
Prérequis
- Rust/Cargo
- Solana
- Steel
Installer Rust
Vous pouvez installer Rust depuis le site officiel de Rust ou via la CLI :
curl --proto '=https' --tlsv1.2 -sSf <https://sh.rustup.rs> | shInstaller la suite d’outils Solana
Steel nécessite également la suite d’outils Solana. Vous pouvez installer la dernière version disponible au moment de la rédaction de cet article, à savoir 2.2.15, avec la commande suivante sous macOS et Linux :
sh -c "$(curl -sSfL <https://release.anza.xyz/v2.2.14/install>)"Sous Windows, vous pouvez installer la suite d’outils Solana avec la commande suivante :
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"Il est toutefois vivement recommandé d’utiliser plutôt le Sous-système Windows pour Linux (WSL). Vous pourrez ainsi exécuter un environnement Linux sur votre machine Windows sans double démarrage ni machine virtuelle distincte. Si vous choisissez cette approche, reportez-vous aux instructions d’installation pour Linux, à savoir la commande curl.
Les développeurs peuvent remplacer v2.2.15 par la balise de la version à télécharger ou utiliser les noms de canaux stable, beta ou edge.
Après l’installation, exécutez solana –-version pour vérifier que la version souhaitée de solana est bien installée.
Installer Steel
Vous pouvez installer Steel avec Cargo en exécutant :
cargo install steel-cliCréer un projet Steel
Pour créer un projet Steel, il suffit d’exécuter :
// creates a new Steel project named `create-token`
steel new token
// enter directory
cd create-tokenNotre répertoire token doit ressembler à ceci :
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
La structure par défaut d’un projet Steel contient deux dossiers nommés api et program.
api contient des types comme state et errors, que nous utiliserons pour implémenter notre programme Solana. Le dossier program contient la logique du programme.
Lorsque vous développez des programmes avec Steel, il est préférable de commencer par le dossier api, car le dossier program en dépend.
Supprimer les modules state, const et error
Le dossier api contient certains modules que nous n’utiliserons pas dans notre projet create-token, notamment state, const et error. Supprimons-les.
Pour supprimer les modules Steel, exécutez les commandes suivantes :
# 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/>)Après avoir supprimé les modules, nous devons mettre à jour notre fichier api/src/lib.rs, car il les appelle.
Modifiez api/src/lib.rs comme suit :
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");Définir des instructions dans Steel
Dans Steel, les instructions sont définies dans api/src/instructions.rs. Toutes les instructions d’un programme Steel sont définies dans une enum, et chaque instruction est une struct.
L’enum qui contient toutes les instructions se présente ainsi :
#[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]
}Lorsqu’une instruction ne nécessite aucun argument, comme Initialize, elle ne comporte aucun champ.
Les instructions qui nécessitent des données utilisent une représentation en octets. C’est par exemple le cas de Add::amount is [u8; 8], qui correspond à un u64.
Après avoir défini l’enum et la struct de nos instructions, nous devons les transmettre à la macro instruction!. Le premier argument est l’enum des instructions et le second, la struct de l’instruction :
instruction!(CreateTokenInstruction, Initialize);
instruction!(CreateTokenInstruction, Add);Notre programme create-token possède une instruction qui accepte quatre arguments. api/src/instructions doit donc se présenter ainsi :
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);Dans Create, les champs name, symbol et uri sont des chaînes représentées par des tableaux d’octets de taille fixe :
name: [u8; 16] — pour les noms de 16 octets maximumsymbol: [u8; 8] — les symboles sont généralement courtsuri: [u8; 128] — les URI sont généralement plus longs
Ces tailles dépendent de la longueur maximale attendue en octets, et non en caractères. Par exemple, les caractères UTF-8 multioctets peuvent nécessiter davantage d’espace.
decimals est simplement un u8, car le nombre de décimales du token tient dans un octet.
Mettre à jour le SDK
Dans api/src, un fichier nommé sdk.rs n’est pas utilisé pour implémenter la logique de notre programme, mais servira à exécuter des tests ou du code client Rust. Il contient des fonctions qui construisent séparément toutes les instructions d’un programme Steel. Comme ce programme ne comporte qu’une seule instruction, une seule fonction SDK nous suffit. Notre api/src/sdk.rs doit donc se présenter ainsi :
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(),
}
}Nous avons une fonction nommée create qui accepte cinq arguments : user est la clé publique du compte qui appellera cette instruction, mint est la clé publique du compte qui représentera token mint, tandis que name, symbol, uri et decimals sont les données utilisées pour implémenter la logique du programme définie dans api/src/instructions::Create.
Nous devons stocker les métadonnées de notre token. Pour cela, nous utiliserons le programme Metaplex Metadata. Commençons par ajouter :
let metadata = Pubkey::find_program_address(
&[
"metadata".as_bytes(),
mpl_token_metadata::ID.as_ref(),
mint.as_ref(),
],
&mpl_token_metadata::ID,
)
.0;Dans ce bloc de code, nous cherchons à obtenir la Program Derived Address (PDA) où seront stockées les métadonnées de notre token. Pour dériver l’adresse nécessaire, nous avons besoin des seeds suivantes :
- La chaîne « metadata » sous forme d’octets (c’est-à-dire
"metadata".as_bytes()) - L’ID du programme de métadonnées sous forme de slice (c’est-à-dire
mpl_token_metadata::ID.as_ref()) - La clé publique du mint sous forme de slice (c’est-à-dire
mint.as_ref())
Toutes ces entrées forment ensemble les seeds. Pour le deuxième argument de Pubkey::find_program::address, nous avons simplement besoin de l’ID du programme Metadata.
Dans le dernier bloc de code, nous renvoyons le type Instruction qui représente cette instruction.
Le type Instruction se présente ainsi :
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(),
} Le type Instruction est une struct comportant trois champs :
program_idaccountsdata
Dans ce bloc, nous déclarons une instance de Instruction adaptée à l’instruction de notre programme.
Pour obtenir program_id depuis api/src/lib.rs, utilisez :
program_id: crate::ID Le champ accounts est un vecteur de métadonnées Account, à savoir Vec<AccountMeta>. Nous devons donc déclarer tous les comptes utilisés dans cette instruction :
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),
],Enfin, le champ data représente sous forme d’octets les arguments utilisés pour ces instructions :
data: Create {
name,
symbol,
uri,
decimals,
}
.to_bytes(),Nous en avons maintenant terminé avec le dossier api.
Ajoutons ensuite les dépendances nécessaires, puis passons au dossier program.
Ajouter les dépendances Steel
À ce stade, si vous exécutez steel build pour compiler votre programme, l’opération devrait échouer avec les erreurs suivantes :
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`
Cela indique que les crates spl_token et mpl_token_metadata nécessaires à notre programme sont absents.
Pour ajouter les crates manquants, insérez ceci dans le fichier /Cargo.toml :
// /Cargo.toml
[workspace.dependencies]
...
...
mpl-token-metadata = "5.1.0"
spl-token = { version = "8.0.0", features = ["no-entrypoint"] }
In /api/Cargo.toml add:
// /api/Cargo.toml
[dependencies]
...
...
mpl-token-metadata.workspace = true
spl-token.workspace = trueDans /api/Cargo.toml, ajoutez :
// /api/Cargo.toml
[dependencies]
...
...
mpl-token-metadata.workspace = true
spl-token.workspace = trueSi nous exécutons maintenant steel build, les erreurs de dépendance devraient avoir disparu.
Cependant, comme nous avons supprimé du code du dossier api dont dépend le dossier program, certaines erreurs de ce type continueront d’apparaître :
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`
Pas d’inquiétude : nous corrigerons ces erreurs dans la section suivante.
Implémenter la logique du programme avec Steel
Les projets Steel comportent par défaut deux dossiers : api et program. Nous venons de définir les types nécessaires à notre programme dans le dossier api. Nous devons maintenant implémenter la logique du programme dans le dossier program.
Pour commencer, mettez à jour /program/lib.rs avec :
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);Dans ce fichier, nous définissons notre fonction principale process_instruction, que nous transmettons à la macro entrypoint!. Cette macro génère le code répétitif nécessaire pour que le runtime Solana appelle la logique de notre programme.
La fonction process_instruction contient deux blocs de code importants que nous devons examiner.
let (ix, data) = parse_instruction(&create_token_api::ID, program_id, data)?;parse_instruction analyse une instruction à partir de ses données. Les données transmises à notre programme nous permettent ainsi de déterminer quelle instruction appeler.
Dans un cas Ok(), il renvoie un tuple contenant instruction(ix) et instruction data(data).
match ix {
CreateTokenInstruction::Create => process_create(accounts, data)?,
}Après avoir obtenu notre instruction(ix) depuis parse_instruction, nous utilisons match pour sélectionner l’instruction à appeler. Ce bloc ne comporte qu’une seule branche de correspondance, car notre programme ne possède qu’une instruction.
La logique de notre programme est désormais configurée pour appeler l’instruction appropriée lorsqu’il est invoqué. Toutefois, process_create et le mod create n’existent pas encore. Créons-les.
Dans un terminal, exécutez :
// 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 Mettez maintenant à jour program/src/create.rs avec :
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(())
}Examinons ce qui se passe ici.
// Load accounts.
let [user_info, mint_info, metadata_info, token_program, token_metadata_program, system_program, rent_sysvar] =
accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
};Dans ce bloc de code, nous chargeons les comptes nécessaires à cette instruction. Si les comptes transmis ne correspondent pas aux comptes définis, ce bloc déclenche l’erreur ProgramError::NotEnoughAccountKeys.
En regardant attentivement, vous remarquerez une convention dans la façon dont les comptes sont nommés :
- Les comptes « ordinaires » se terminent par
info - Les comptes de programme se terminent par
program - Les sysvars se terminent par
sysvar
Il s’agit d’une convention de nommage propre à Steel. Vous pouvez en choisir une autre, car elle n’a aucun impact réel sur le programme.
Le bloc de code suivant valide nos comptes :
// 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 fournit des utilitaires simples pour valider les comptes, qui peuvent être chaînés.
Nous créons ensuite le compte mint à l’aide de l’utilitaire create_account :
// create mint account
create_account(
user_info,
mint_info,
system_program,
spl_token::state::Mint::LEN,
&token_program.key,
)?;Après avoir créé les comptes mint, nous désérialisons les données de l’instruction afin de convertir les octets en types Rust :
let args = Create::try_from_bytes(data)?;
let name = bytes_to_string::<32>(&args.name)?;
let symbol = bytes_to_string::<8>(&args.symbol)?;
let uri = bytes_to_string::<128>(&args.uri)?;
let decimals = args.decimals;La première ligne convertit les données de l’instruction du type &[u8] vers api/instructions.rs/Create. Les trois lignes suivantes convertissent en chaînes les champs de Create, qui sont des octets, à l’aide de l’utilitaire bytes_to_string.
Notez également que bytes_to_string accepte un paramètre générique const, à savoir ::<32>, qui permet de générer la chaîne avec la longueur exacte afin d’économiser des unités de calcul.
// initialize mint
initialize_mint(
mint_info,
user_info,
Some(user_info),
token_program,
rent_sysvar,
decimals,
)?;Nous initialisons ensuite le compte mint avec la fonction utilitaire 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()?;Ici, nous avons créé le compte metadata pour le mint de notre token. Il contient des informations telles que le nom, le symbole et les créateurs de la collection.
Maintenant que nous avons terminé le fichier create.rs, exécutons steel build.
Les erreurs suivantes devraient apparaître :
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`Ces erreurs signalent des problèmes de dépendances. Nous pouvons les corriger en modifiant notre fichier /program/Cargo.toml avec :
[dependencies]
...
...
mpl-token-metadata.workspace = true
spl-token.workspace = trueSi nous exécutons de nouveau steel build, il ne restera plus qu’une erreur :
error[E0425]: cannot find function `initialize_mint` in this scope
--> program/src/create.rs:57:5
|
57 | initialize_mint(
| ^^^^^^^^^^^^^^^ not found in this scopeCette erreur apparaît parce que la fonction utilitaire initialize_mint nécessite la fonctionnalité spl de Steel. Nous devons donc mettre à jour son import dans le fichier /Cargo.toml :
[workspace.dependencies]
...
...
steel = { version = "3.0", features = ["spl"] }Si nous exécutons maintenant steel build, notre programme devrait se compiler sans erreur.
Félicitations d’être arrivé jusqu’ici !
Une dernière étape : nous devons tester notre programme.
Tester votre programme Steel
Par défaut, les tests Steel sont écrits en Rust. Steel utilise solana-program-test pour les tests, mais vous pouvez utiliser liteSVM ou mollusk si vous préférez.
Les tests sont écrits dans /program/tests/test.rs.
Commençons par le mettre à jour avec :
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);
}
Notre fichier de test comprend deux fonctions : setup et run_test.
Nous effectuons trois opérations importantes dans la fonction setup :
- Nous créons une instance de
ProgramTest, à laquelle notre programmecreate_token_programest ajouté par défaut - Nous ajoutons le programme
token_metadataà notre instance deProgramTest, car le programme de token Metaplex que nous utilisons n’est pas inclus par défaut dansProgramTest - Nous démarrons une instance de
ProgramTestavec la méthodestart, qui renvoie un tuple de (BanksClient,Keypair,Hash)
async fn setup() -> (BanksClient, Keypair, Hash) {
let mut program_test = ProgramTest::new(
"create_token_program",
create_token_api::ID,
processor!(create_token_program::process_instruction),
);
program_test.add_program("token_metadata", mpl_token_metadata::ID, None);
program_test.prefer_bpf(true);
program_test.start().await
}Dans la première partie de run_test, nous appelons la fonction setup et créons un Keypair pour le mint de notre token.
// Setup test
let (mut banks, payer, blockhash) = setup().await;
let mint_keypair = Keypair::new();Nous préparons ensuite les données de notre instruction.
Comme notre instruction create attend des représentations en octets, nous utilisons l’utilitaire string_to_bytes pour convertir nos chaînes en octets.
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;Vous souvenez-vous de la fonction implémentée dans le dossier api, mais inutilisée dans la logique de notre programme ? Il s’agissait de la fonction create dans api/src/sdk.rs.
C’est cette même fonction que nous appelons en premier dans le bloc de code ci-dessous pour créer une instance de Instruction. Nous la transmettons ensuite à notre instance Transaction avec Transaction::new_signed_with_payer, puis nous envoyons notre transaction à banks.process_transaction(tx).await; pour traitement.
assert!(res.is_ok()); confirme que la transaction a été traitée.
// 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());Jusqu’ici, nous avons exécuté notre instruction dans notre environnement de test, ProgramTest.
Vérifions maintenant qu’elle s’est correctement exécutée :
// 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);Nous pourrions écrire d’autres assertions pour vérifier différents éléments, comme les données stockées dans le compte metadata, mais nous nous arrêterons ici par souci de simplicité.
Si vous le souhaitez, vous pouvez les ajouter vous-même. Et si vous avez besoin d’aide, consultez ce guide de développement Solana consacré aux tests Steel.
Maintenant que notre fichier de test est terminé, exécutons la commande de test : steel test.
Malheureusement, elle échouera, car nous ne disposons pas du code source/fichier ELF du programme mpl_token_metadata.
Pas d’inquiétude, nous pouvons corriger ce problème en exécutant :
// 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.soSi nous exécutons maintenant steel test, nous devrions obtenir le résultat suivant :
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.16sEncore félicitations !
Si vous obtenez le même résultat, votre programme a réussi les tests.
Conclusion
Steel est un framework de développement modulaire et léger permettant de créer des programmes Solana intelligents et optimisés pour les performances. Cet article a expliqué le fonctionnement de Steel, l’a comparé à Anchor et Pinocchio, puis vous a montré comment créer un nouveau token avec Steel.
Ressources supplémentaires
Pour approfondir vos connaissances sur Steel et le développement de programmes Solana, consultez ces ressources :
- Dépôt GitHub de Steel
- Bootcamp de développement Solana (GitHub)
- Bootcamp de développement Solana (vidéo)
- Blueshift — Apprenez à écrire vos propres programmes on-chain
Articles associés
Abonnez-vous à Helius
Suivez les dernières actualités du développement sur Solana et recevez une notification à chaque publication


