Structure du programme Rust
Les programmes Solana écrits en Rust ont des exigences structurelles minimales,
permettant une flexibilité dans l'organisation du code. La seule exigence est
qu'un programme doit avoir un entrypoint
, qui définit où l'exécution d'un
programme commence.
Structure du programme
Bien qu'il n'y ait pas de règles strictes pour la structure des fichiers, les programmes Solana suivent généralement un modèle commun :
entrypoint.rs
: Définit le point d'entrée qui achemine les instructions entrantes.state.rs
: Définissent l'état spécifique au programme (données du compte).instructions.rs
: Définit les instructions que le programme peut exécuter.processor.rs
: Définit les gestionnaires d'instructions (fonctions) qui implémentent la logique métier pour chaque instruction.error.rs
: Définit les erreurs personnalisées que le programme peut renvoyer.
Vous pouvez trouver des exemples dans la Solana Program Library.
Programme d'exemple
Pour démontrer comment construire un programme Rust natif avec plusieurs instructions, nous allons parcourir un programme de compteur simple qui implémente deux instructions :
InitializeCounter
: Crée et initialise un nouveau compte avec une valeur initiale.IncrementCounter
: Incrémente la valeur stockée dans un compte existant.
Pour simplifier, le programme sera implémenté dans un seul fichier lib.rs
,
bien qu'en pratique, vous pourriez vouloir diviser les programmes plus
volumineux en plusieurs fichiers.
Créer un nouveau programme
D'abord, créez un nouveau projet Rust en utilisant la commande standard
cargo init
avec l'option --lib
.
cargo init counter_program --lib
Naviguez vers le répertoire du projet. Vous devriez voir les fichiers par défaut
src/lib.rs
et Cargo.toml
cd counter_program
Ensuite, ajoutez la dépendance solana-program
. C'est la dépendance minimale
requise pour construire un programme Solana.
cargo add solana-program@1.18.26
Ensuite, ajoutez l'extrait suivant à Cargo.toml
. Si vous n'incluez pas cette
configuration, le répertoire target/deploy
ne sera pas généré lors de la
compilation du programme.
[lib]crate-type = ["cdylib", "lib"]
Votre fichier Cargo.toml
devrait ressembler à ce qui suit :
[package]name = "counter_program"version = "0.1.0"edition = "2021"[lib]crate-type = ["cdylib", "lib"][dependencies]solana-program = "1.18.26"
Point d'entrée du programme
Un point d'entrée de programme Solana est la fonction qui est appelée lorsqu'un programme est invoqué. Le point d'entrée a la définition brute suivante et les développeurs sont libres de créer leur propre implémentation de la fonction de point d'entrée.
Pour simplifier, utilisez la macro
entrypoint!
du module solana_program
pour définir le point d'entrée dans votre programme.
#[no_mangle]pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;
Remplacez le code par défaut dans lib.rs
par le code suivant. Cet extrait :
- Importe les dépendances requises depuis
solana_program
- Définit le point d'entrée du programme en utilisant la macro
entrypoint!
- Implémente la fonction
process_instruction
qui acheminera les instructions vers les fonctions de traitement appropriées
use solana_program::{account_info::{next_account_info, AccountInfo},entrypoint,entrypoint::ProgramResult,msg,program::invoke,program_error::ProgramError,pubkey::Pubkey,system_instruction,sysvar::{rent::Rent, Sysvar},};entrypoint!(process_instruction);pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {// Your program logicOk(())}
La macro entrypoint!
nécessite une fonction avec la
signature de type
suivante comme argument :
pub type ProcessInstruction =fn(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult;
Lorsqu'un programme Solana est invoqué, le point d'entrée
désérialise
les
données d'entrée
(fournies sous forme d'octets) en trois valeurs et les transmet à la fonction
process_instruction
:
program_id
: La clé publique du programme invoqué (programme actuel)accounts
: LeAccountInfo
pour les comptes requis par l'instruction invoquéeinstruction_data
: Données supplémentaires transmises au programme qui spécifient l'instruction à exécuter et ses arguments requis
Ces trois paramètres correspondent directement aux données que les clients doivent fournir lors de la construction d'une instruction pour invoquer un programme.
Définir l'état du programme
Lors de la création d'un programme Solana, vous commencerez généralement par définir l'état de votre programme - les données qui seront stockées dans les comptes créés et détenus par votre programme.
L'état du programme est défini à l'aide de structures Rust qui représentent la disposition des données des comptes de votre programme. Vous pouvez définir plusieurs structures pour représenter différents types de comptes pour votre programme.
Lorsque vous travaillez avec des comptes, vous avez besoin d'un moyen de convertir les types de données de votre programme vers et depuis les octets bruts stockés dans le champ de données d'un compte :
- Sérialisation : Conversion de vos types de données en octets pour les stocker dans le champ de données d'un compte
- Désérialisation : Conversion des octets stockés dans un compte en vos types de données
Bien que vous puissiez utiliser n'importe quel format de sérialisation pour le développement de programmes Solana, Borsh est couramment utilisé. Pour utiliser Borsh dans votre programme Solana :
- Ajoutez la crate
borsh
comme dépendance à votreCargo.toml
:
cargo add borsh
- Importez les traits Borsh et utilisez la macro derive pour implémenter les traits pour vos structures :
use borsh::{BorshSerialize, BorshDeserialize};// Define struct representing our counter account's data#[derive(BorshSerialize, BorshDeserialize, Debug)]pub struct CounterAccount {count: u64,}
Ajoutez la structure CounterAccount
à lib.rs
pour définir l'état du
programme. Cette structure sera utilisée à la fois dans les instructions
d'initialisation et d'incrémentation.
use solana_program::{account_info::{next_account_info, AccountInfo},entrypoint,entrypoint::ProgramResult,msg,program::invoke,program_error::ProgramError,pubkey::Pubkey,system_instruction,sysvar::{rent::Rent, Sysvar},};use borsh::{BorshSerialize, BorshDeserialize};entrypoint!(process_instruction);pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {// Your program logicOk(())}#[derive(BorshSerialize, BorshDeserialize, Debug)]pub struct CounterAccount {count: u64,}
Définir les instructions
Les instructions font référence aux différentes opérations que votre programme Solana peut exécuter. Considérez-les comme des API publiques pour votre programme - elles définissent quelles actions les utilisateurs peuvent effectuer lors de l'interaction avec votre programme.
Les instructions sont généralement définies à l'aide d'une énumération Rust où :
- Chaque variante de l'énumération représente une instruction différente
- La charge utile de la variante représente les paramètres de l'instruction
Notez que les variantes d'énumération Rust sont implicitement numérotées à partir de 0.
Voici un exemple d'une énumération définissant deux instructions :
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 }, // variant 0IncrementCounter, // variant 1}
Lorsqu'un client invoque votre programme, il doit fournir des instruction data (sous forme de tampon d'octets) où :
- Le premier octet identifie quelle variante d'instruction exécuter (0, 1, etc.)
- Les octets restants contiennent les paramètres d'instruction sérialisés (si nécessaire)
Pour convertir les instruction data (octets) en une variante de l'énumération, il est courant d'implémenter une méthode auxiliaire. Cette méthode :
- Sépare le premier octet pour obtenir la variante d'instruction
- Effectue une correspondance sur la variante et analyse les paramètres supplémentaires à partir des octets restants
- Renvoie la variante d'énumération correspondante
Par exemple, la méthode unpack
pour l'énumération CounterInstruction
:
impl CounterInstruction {pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {// Get the instruction variant from the first bytelet (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;// Match instruction type and parse the remaining bytes based on the variantmatch variant {0 => {// For InitializeCounter, parse a u64 from the remaining byteslet initial_value = u64::from_le_bytes(rest.try_into().map_err(|_| ProgramError::InvalidInstructionData)?);Ok(Self::InitializeCounter { initial_value })}1 => Ok(Self::IncrementCounter), // No additional data needed_ => Err(ProgramError::InvalidInstructionData),}}}
Ajoutez le code suivant à lib.rs
pour définir les instructions du programme de
compteur.
use borsh::{BorshDeserialize, BorshSerialize};use solana_program::{account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, msg,program_error::ProgramError, pubkey::Pubkey,};entrypoint!(process_instruction);pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {// Your program logicOk(())}#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 }, // variant 0IncrementCounter, // variant 1}impl CounterInstruction {pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {// Get the instruction variant from the first bytelet (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;// Match instruction type and parse the remaining bytes based on the variantmatch variant {0 => {// For InitializeCounter, parse a u64 from the remaining byteslet initial_value = u64::from_le_bytes(rest.try_into().map_err(|_| ProgramError::InvalidInstructionData)?,);Ok(Self::InitializeCounter { initial_value })}1 => Ok(Self::IncrementCounter), // No additional data needed_ => Err(ProgramError::InvalidInstructionData),}}}
Gestionnaires d'instructions
Les gestionnaires d'instructions font référence aux fonctions qui contiennent la
logique métier pour chaque instruction. Il est courant de nommer les fonctions
de gestionnaire process_<instruction_name>
, mais vous êtes libre de choisir
n'importe quelle convention de nommage.
Ajoutez le code suivant à lib.rs
. Ce code utilise l'énumération
CounterInstruction
et la méthode unpack
définie à l'étape précédente pour
acheminer les instructions entrantes vers les fonctions de gestionnaire
appropriées :
entrypoint!(process_instruction);pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {// Unpack instruction datalet instruction = CounterInstruction::unpack(instruction_data)?;// Match instruction typematch instruction {CounterInstruction::InitializeCounter { initial_value } => {process_initialize_counter(program_id, accounts, initial_value)?}CounterInstruction::IncrementCounter => process_increment_counter(program_id, accounts)?,};}fn process_initialize_counter(program_id: &Pubkey,accounts: &[AccountInfo],initial_value: u64,) -> ProgramResult {// Implementation details...Ok(())}fn process_increment_counter(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {// Implementation details...Ok(())}
Ensuite, ajoutez l'implémentation de la fonction process_initialize_counter
.
Ce gestionnaire d'instruction :
- Crée et alloue de l'espace pour un nouveau compte pour stocker les données du compteur
- Initialise les données du compte avec
initial_value
passé à l'instruction
// Initialize a new counter accountfn process_initialize_counter(program_id: &Pubkey,accounts: &[AccountInfo],initial_value: u64,) -> ProgramResult {let accounts_iter = &mut accounts.iter();let counter_account = next_account_info(accounts_iter)?;let payer_account = next_account_info(accounts_iter)?;let system_program = next_account_info(accounts_iter)?;// Size of our counter accountlet account_space = 8; // Size in bytes to store a u64// Calculate minimum balance for rent exemptionlet rent = Rent::get()?;let required_lamports = rent.minimum_balance(account_space);// Create the counter accountinvoke(&system_instruction::create_account(payer_account.key, // Account paying for the new accountcounter_account.key, // Account to be createdrequired_lamports, // Amount of lamports to transfer to the new accountaccount_space as u64, // Size in bytes to allocate for the data fieldprogram_id, // Set program owner to our program),&[payer_account.clone(),counter_account.clone(),system_program.clone(),],)?;// Create a new CounterAccount struct with the initial valuelet counter_data = CounterAccount {count: initial_value,};// Get a mutable reference to the counter account's datalet mut account_data = &mut counter_account.data.borrow_mut()[..];// Serialize the CounterAccount struct into the account's datacounter_data.serialize(&mut account_data)?;msg!("Counter initialized with value: {}", initial_value);Ok(())}
Ensuite, ajoutez l'implémentation de la fonction process_increment_counter
.
Cette instruction incrémente la valeur d'un compte compteur existant.
// Update an existing counter's valuefn process_increment_counter(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {let accounts_iter = &mut accounts.iter();let counter_account = next_account_info(accounts_iter)?;// Verify account ownershipif counter_account.owner != program_id {return Err(ProgramError::IncorrectProgramId);}// Mutable borrow the account datalet mut data = counter_account.data.borrow_mut();// Deserialize the account data into our CounterAccount structlet mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?;// Increment the counter valuecounter_data.count = counter_data.count.checked_add(1).ok_or(ProgramError::InvalidAccountData)?;// Serialize the updated counter data back into the accountcounter_data.serialize(&mut &mut data[..])?;msg!("Counter incremented to: {}", counter_data.count);Ok(())}
Test des instructions
Pour tester les instructions du programme, ajoutez les dépendances suivantes à
Cargo.toml
.
cargo add solana-program-test@1.18.26 --devcargo add solana-sdk@1.18.26 --devcargo add tokio --dev
Ensuite, ajoutez le module de test suivant à lib.rs
et exécutez
cargo test-sbf
pour exécuter les tests. Facultativement, utilisez l'option
--nocapture
pour voir les instructions print dans la sortie.
cargo test-sbf -- --nocapture
#[cfg(test)]mod test {use super::*;use solana_program_test::*;use solana_sdk::{instruction::{AccountMeta, Instruction},signature::{Keypair, Signer},system_program,transaction::Transaction,};#[tokio::test]async fn test_counter_program() {let program_id = Pubkey::new_unique();let (mut banks_client, payer, recent_blockhash) = ProgramTest::new("counter_program",program_id,processor!(process_instruction),).start().await;// Create a new keypair to use as the address for our counter accountlet counter_keypair = Keypair::new();let initial_value: u64 = 42;// Step 1: Initialize the counterprintln!("Testing counter initialization...");// Create initialization instructionlet mut init_instruction_data = vec![0]; // 0 = initialize instructioninit_instruction_data.extend_from_slice(&initial_value.to_le_bytes());let initialize_instruction = Instruction::new_with_bytes(program_id,&init_instruction_data,vec![AccountMeta::new(counter_keypair.pubkey(), true),AccountMeta::new(payer.pubkey(), true),AccountMeta::new_readonly(system_program::id(), false),],);// Send transaction with initialize instructionlet mut transaction =Transaction::new_with_payer(&[initialize_instruction], Some(&payer.pubkey()));transaction.sign(&[&payer, &counter_keypair], recent_blockhash);banks_client.process_transaction(transaction).await.unwrap();// Check account datalet account = banks_client.get_account(counter_keypair.pubkey()).await.expect("Failed to get counter account");if let Some(account_data) = account {let counter: CounterAccount = CounterAccount::try_from_slice(&account_data.data).expect("Failed to deserialize counter data");assert_eq!(counter.count, 42);println!("✅ Counter initialized successfully with value: {}",counter.count);}// Step 2: Increment the counterprintln!("Testing counter increment...");// Create increment instructionlet increment_instruction = Instruction::new_with_bytes(program_id,&[1], // 1 = increment instructionvec![AccountMeta::new(counter_keypair.pubkey(), true)],);// Send transaction with increment instructionlet mut transaction =Transaction::new_with_payer(&[increment_instruction], Some(&payer.pubkey()));transaction.sign(&[&payer, &counter_keypair], recent_blockhash);banks_client.process_transaction(transaction).await.unwrap();// Check account datalet account = banks_client.get_account(counter_keypair.pubkey()).await.expect("Failed to get counter account");if let Some(account_data) = account {let counter: CounterAccount = CounterAccount::try_from_slice(&account_data.data).expect("Failed to deserialize counter data");assert_eq!(counter.count, 43);println!("✅ Counter incremented successfully to: {}", counter.count);}}}
Exemple de sortie :
running 1 test[2024-10-29T20:51:13.783708000Z INFO solana_program_test] "counter_program" SBF program from /counter_program/target/deploy/counter_program.so, modified 2 seconds, 169 ms, 153 µs and 461 ns ago[2024-10-29T20:51:13.855204000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM invoke [1][2024-10-29T20:51:13.856052000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 invoke [2][2024-10-29T20:51:13.856135000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 success[2024-10-29T20:51:13.856242000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Counter initialized with value: 42[2024-10-29T20:51:13.856285000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM consumed 3791 of 200000 compute units[2024-10-29T20:51:13.856307000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM success[2024-10-29T20:51:13.860038000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM invoke [1][2024-10-29T20:51:13.860333000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Counter incremented to: 43[2024-10-29T20:51:13.860355000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM consumed 756 of 200000 compute units[2024-10-29T20:51:13.860375000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM successtest test::test_counter_program ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s
Is this page helpful?