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éfinit l'état du 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.
Par exemple, consultez le Programme Token.
Exemple de programme
Pour démontrer comment construire un programme Rust natif avec plusieurs instructions, nous allons examiner 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.
Partie 1 : Écriture du programme
Commençons par construire le programme de compteur. Nous allons créer un programme qui peut initialiser un compteur avec une valeur de départ et l'incrémenter.
Créer un nouveau programme
Tout d'abord, créons un nouveau projet Rust pour notre programme Solana.
$cargo new counter_program --lib$cd counter_program
Vous devriez voir les fichiers par défaut src/lib.rs
et Cargo.toml
.
Mettez à jour le champ edition
dans Cargo.toml
à 2021. Sinon, vous
pourriez rencontrer une erreur lors de la compilation du programme.
Ajouter les dépendances
Maintenant, ajoutons les dépendances nécessaires pour construire un programme
Solana. Nous avons besoin de solana-program
pour le SDK principal et borsh
pour la sérialisation.
$cargo add solana-program@2.2.0$cargo add borsh
Il n'y a pas d'obligation d'utiliser Borsh. Cependant, c'est une bibliothèque de sérialisation couramment utilisée pour les programmes Solana.
Configurer crate-type
Les programmes Solana doivent être compilés comme des bibliothèques dynamiques.
Ajoutez la section [lib]
pour configurer la façon dont Cargo compile le
programme.
[lib]crate-type = ["cdylib", "lib"]
Si vous n'incluez pas cette configuration, le répertoire target/deploy ne sera pas généré lorsque vous compilerez le programme.
Configurer le point d'entrée du programme
Chaque programme Solana possède un point d'entrée, qui est la fonction appelée lorsque le programme est invoqué. Commençons par ajouter les importations dont nous aurons besoin pour le programme et configurer le point d'entrée.
Ajoutez le code suivant à lib.rs
:
use borsh::{BorshDeserialize, BorshSerialize};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 {Ok(())}
La macro
entrypoint
gère la désérialisation des données input
dans les paramètres de la fonction
process_instruction
.
Un entrypoint
de programme Solana a la signature de fonction suivante. Les
développeurs sont libres de créer leur propre implémentation de la fonction
entrypoint
.
#[no_mangle]pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;
Définir l'état du programme
Maintenant, définissons la structure de données qui sera stockée dans nos
comptes compteur. Ce sont les données qui seront stockées dans le champ data
du compte.
Ajoutez le code suivant à lib.rs
:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub struct CounterAccount {pub count: u64,}
Définir l'énumération d'instructions
Définissons les instructions que notre programme peut exécuter. Nous utiliserons une énumération où chaque variante représente une instruction différente.
Ajoutez le code suivant à lib.rs
:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 },IncrementCounter,}
Implémenter la désérialisation des instructions
Maintenant, nous devons désérialiser le instruction_data
(octets bruts) en
l'une de nos variantes d'énumération CounterInstruction
. La méthode Borsh
try_from_slice
gère automatiquement cette conversion.
Mettez à jour la fonction process_instruction
pour utiliser la désérialisation
Borsh :
pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {let instruction = CounterInstruction::try_from_slice(instruction_data).map_err(|_| ProgramError::InvalidInstructionData)?;Ok(())}
Acheminer les instructions vers les gestionnaires
Maintenant, mettons à jour la fonction principale process_instruction
pour
acheminer les instructions vers leurs fonctions gestionnaires appropriées.
Ce modèle d'acheminement est courant dans les programmes Solana. Le
instruction_data
est désérialisé en une variante d'une énumération
représentant l'instruction, puis la fonction gestionnaire appropriée est
appelée. Chaque fonction gestionnaire comprend l'implémentation pour cette
instruction.
Ajoutez le code suivant à lib.rs
en mettant à jour la fonction
process_instruction
et en ajoutant les gestionnaires pour les instructions
InitializeCounter
et IncrementCounter
:
pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {let instruction = CounterInstruction::try_from_slice(instruction_data).map_err(|_| ProgramError::InvalidInstructionData)?;match instruction {CounterInstruction::InitializeCounter { initial_value } => {process_initialize_counter(program_id, accounts, initial_value)?}CounterInstruction::IncrementCounter => {process_increment_counter(program_id, accounts)?}};Ok(())}fn process_initialize_counter(program_id: &Pubkey,accounts: &[AccountInfo],initial_value: u64,) -> ProgramResult {Ok(())}fn process_increment_counter(program_id: &Pubkey,accounts: &[AccountInfo],) -> ProgramResult {Ok(())}
Implémenter le gestionnaire d'initialisation
Implémentons le gestionnaire pour créer et initialiser un nouveau compte compteur. Comme seul le System Program peut créer des comptes sur Solana, nous utiliserons une Cross Program Invocation (CPI), essentiellement en appelant un autre programme depuis notre programme.
Notre programme effectue un CPI pour appeler l'instruction create_account
du
System Program. Le nouveau compte est créé avec notre programme comme
propriétaire, donnant à notre programme la capacité d'écrire dans le compte et
d'initialiser les données.
Ajoutez le code suivant à lib.rs
en mettant à jour la fonction
process_initialize_counter
:
fn 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)?;let account_space = 8;let rent = Rent::get()?;let required_lamports = rent.minimum_balance(account_space);invoke(&system_instruction::create_account(payer_account.key,counter_account.key,required_lamports,account_space as u64,program_id,),&[payer_account.clone(),counter_account.clone(),system_program.clone(),],)?;let counter_data = CounterAccount {count: initial_value,};let mut account_data = &mut counter_account.data.borrow_mut()[..];counter_data.serialize(&mut account_data)?;msg!("Counter initialized with value: {}", initial_value);Ok(())}
Cette instruction est uniquement à des fins de démonstration. Elle n'inclut pas les vérifications de sécurité et de validation nécessaires pour les programmes en production.
Implémenter le gestionnaire d'incrémentation
Maintenant, implémentons le gestionnaire qui incrémente un compteur existant. Cette instruction :
- Lit le champ
data
du compte pour lecounter_account
- Le désérialise en une structure
CounterAccount
- Incrémente le champ
count
de 1 - Sérialise la structure
CounterAccount
dans le champdata
du compte
Ajoutez le code suivant à lib.rs
en mettant à jour la fonction
process_increment_counter
:
fn process_increment_counter(program_id: &Pubkey,accounts: &[AccountInfo],) -> ProgramResult {let accounts_iter = &mut accounts.iter();let counter_account = next_account_info(accounts_iter)?;if counter_account.owner != program_id {return Err(ProgramError::IncorrectProgramId);}let mut data = counter_account.data.borrow_mut();let mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?;counter_data.count = counter_data.count.checked_add(1).ok_or(ProgramError::InvalidAccountData)?;counter_data.serialize(&mut &mut data[..])?;msg!("Counter incremented to: {}", counter_data.count);Ok(())}
Cette instruction est uniquement à des fins de démonstration. Elle n'inclut pas les vérifications de sécurité et de validation nécessaires pour les programmes en production.
Programme complet
Félicitations ! Vous avez construit un programme Solana complet qui démontre la structure de base partagée par tous les programmes Solana :
- Point d'entrée : Définit où l'exécution du programme commence et achemine toutes les requêtes entrantes vers les gestionnaires d'instructions appropriés
- Gestion des instructions : Définit les instructions et leurs fonctions de gestion associées
- Gestion d'état : Définit les structures de données des comptes et gère leur état dans les comptes appartenant au programme
- Cross Program Invocation (CPI) : Appelle le System Program pour créer de nouveaux comptes appartenant au programme
La prochaine étape consiste à tester le programme pour s'assurer que tout fonctionne correctement.
Partie 2 : Tester le programme
Maintenant, testons notre programme de compteur. Nous utiliserons LiteSVM, un framework de test qui nous permet de tester des programmes sans les déployer sur un cluster.
Ajouter les dépendances de test
D'abord, ajoutons les dépendances nécessaires pour les tests. Nous utiliserons
litesvm
pour les tests et solana-sdk
.
$cargo add litesvm@0.6.1 --dev$cargo add solana-sdk@2.2.0 --dev
Créer un module de test
Maintenant, ajoutons un module de test à notre programme. Nous commencerons par la structure de base et les importations.
Ajoutez le code suivant à lib.rs
, directement sous le code du programme :
#[cfg(test)]mod test {use super::*;use litesvm::LiteSVM;use solana_sdk::{account::ReadableAccount,instruction::{AccountMeta, Instruction},message::Message,signature::{Keypair, Signer},system_program,transaction::Transaction,};#[test]fn test_counter_program() {// Test implementation will go here}}
L'attribut #[cfg(test)]
garantit que ce code n'est compilé que lors de
l'exécution des tests.
Initialiser l'environnement de test
Configurons l'environnement de test avec LiteSVM et finançons un compte payeur.
LiteSVM simule l'environnement d'exécution Solana, nous permettant de tester notre programme sans le déployer sur un véritable cluster.
Ajoutez le code suivant à lib.rs
en mettant à jour la fonction
test_counter_program
:
let mut svm = LiteSVM::new();let payer = Keypair::new();svm.airdrop(&payer.pubkey(), 1_000_000_000).expect("Failed to airdrop");
Charger le programme
Maintenant, nous devons compiler et charger notre programme dans l'environnement
de test. Exécutez la commande cargo build-sbf
pour compiler le programme. Cela
générera le fichier counter_program.so
dans le répertoire target/deploy
.
$cargo build-sbf
Assurez-vous que le edition
dans Cargo.toml
est défini sur 2021
.
Après la compilation, nous pouvons charger le programme.
Mettez à jour la fonction test_counter_program
pour charger le programme dans
l'environnement de test.
let program_keypair = Keypair::new();let program_id = program_keypair.pubkey();svm.add_program_from_file(program_id,"target/deploy/counter_program.so").expect("Failed to load program");
Vous devez exécuter cargo build-sbf
avant d'exécuter les tests pour générer
le fichier .so
. Le test charge le programme compilé.
Tester l'instruction d'initialisation
Testons l'instruction d'initialisation en créant un nouveau compte compteur avec une valeur de départ.
Ajoutez le code suivant à lib.rs
en mettant à jour la fonction
test_counter_program
:
let counter_keypair = Keypair::new();let initial_value: u64 = 42;println!("Testing counter initialization...");let init_instruction_data =borsh::to_vec(&CounterInstruction::InitializeCounter { initial_value }).expect("Failed to serialize instruction");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),],);let message = Message::new(&[initialize_instruction], Some(&payer.pubkey()));let transaction = Transaction::new(&[&payer, &counter_keypair],message,svm.latest_blockhash());let result = svm.send_transaction(transaction);assert!(result.is_ok(), "Initialize transaction should succeed");let logs = result.unwrap().logs;println!("Transaction logs:\n{:#?}", logs);
Vérifier l'initialisation
Après l'initialisation, vérifions que le compte compteur a été créé correctement avec la valeur attendue.
Ajoutez le code suivant à lib.rs
en mettant à jour la fonction
test_counter_program
:
let account = svm.get_account(&counter_keypair.pubkey()).expect("Failed to get counter account");let counter: CounterAccount = CounterAccount::try_from_slice(account.data()).expect("Failed to deserialize counter data");assert_eq!(counter.count, 42);println!("Counter initialized successfully with value: {}", counter.count);
Tester l'instruction d'incrémentation
Maintenant, testons l'instruction d'incrémentation pour nous assurer qu'elle met à jour correctement la valeur du compteur.
Ajoutez le code suivant à lib.rs
en mettant à jour la fonction
test_counter_program
:
println!("Testing counter increment...");let increment_instruction_data =borsh::to_vec(&CounterInstruction::IncrementCounter).expect("Failed to serialize instruction");let increment_instruction = Instruction::new_with_bytes(program_id,&increment_instruction_data,vec![AccountMeta::new(counter_keypair.pubkey(), true)],);let message = Message::new(&[increment_instruction], Some(&payer.pubkey()));let transaction = Transaction::new(&[&payer, &counter_keypair],message,svm.latest_blockhash());let result = svm.send_transaction(transaction);assert!(result.is_ok(), "Increment transaction should succeed");let logs = result.unwrap().logs;println!("Transaction logs:\n{:#?}", logs);
Vérifier les résultats finaux
Enfin, vérifions que l'incrémentation a fonctionné correctement en contrôlant la valeur mise à jour du compteur.
Ajoutez le code suivant à lib.rs
en mettant à jour la fonction
test_counter_program
:
let account = svm.get_account(&counter_keypair.pubkey()).expect("Failed to get counter account");let counter: CounterAccount = CounterAccount::try_from_slice(account.data()).expect("Failed to deserialize counter data");assert_eq!(counter.count, 43);println!("Counter incremented successfully to: {}", counter.count);
Exécutez les tests avec la commande suivante. Le drapeau --nocapture
affiche
la sortie du test.
$cargo test -- --nocapture
Sortie attendue :
Testing counter initialization...Transaction logs:["Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq invoke [1]","Program 11111111111111111111111111111111 invoke [2]","Program 11111111111111111111111111111111 success","Program log: Counter initialized with value: 42","Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq consumed 3803 of 200000 compute units","Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq success",]Counter initialized successfully with value: 42Testing counter increment...Transaction logs:["Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq invoke [1]","Program log: Counter incremented to: 43","Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq consumed 762 of 200000 compute units","Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq success",]Counter incremented successfully to: 43
Partie 3 : Invoquer le programme
Maintenant, ajoutons un script client pour invoquer le programme.
Créer un exemple de client
Créons un client Rust pour interagir avec notre programme déployé.
$mkdir examples$touch examples/client.rs
Ajoutez la configuration suivante à Cargo.toml
:
[[example]]name = "client"path = "examples/client.rs"
Installez les dépendances du client :
$cargo add solana-client@2.2.0 --dev$cargo add tokio --dev
Implémenter le code du client
Maintenant, implémentons le client qui invoquera notre programme déployé.
Exécutez la commande suivante pour obtenir l'ID de votre programme à partir du fichier keypair :
$solana address -k ./target/deploy/counter_program-keypair.json
Ajoutez le code du client à examples/client.rs
et remplacez le program_id
par le résultat de la commande précédente :
let program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH").expect("Invalid program ID");
use solana_client::rpc_client::RpcClient;use solana_sdk::{commitment_config::CommitmentConfig,instruction::{AccountMeta, Instruction},pubkey::Pubkey,signature::{Keypair, Signer},system_program,transaction::Transaction,};use std::str::FromStr;use counter_program::CounterInstruction;#[tokio::main]async fn main() {// Replace with your actual program ID from deploymentlet program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH").expect("Invalid program ID");// Connect to local clusterlet rpc_url = String::from("http://localhost:8899");let client = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed());// Generate a new keypair for paying feeslet payer = Keypair::new();// Request airdrop of 1 SOL for transaction feesprintln!("Requesting airdrop...");let airdrop_signature = client.request_airdrop(&payer.pubkey(), 1_000_000_000).expect("Failed to request airdrop");// Wait for airdrop confirmationloop {if client.confirm_transaction(&airdrop_signature).unwrap_or(false){break;}std::thread::sleep(std::time::Duration::from_millis(500));}println!("Airdrop confirmed");println!("\nInitializing counter...");let counter_keypair = Keypair::new();let initial_value = 100u64;// Serialize the initialize instruction datalet instruction_data = borsh::to_vec(&CounterInstruction::InitializeCounter { initial_value }).expect("Failed to serialize instruction");let initialize_instruction = Instruction::new_with_bytes(program_id,&instruction_data,vec![AccountMeta::new(counter_keypair.pubkey(), true),AccountMeta::new(payer.pubkey(), true),AccountMeta::new_readonly(system_program::id(), false),],);let mut transaction =Transaction::new_with_payer(&[initialize_instruction], Some(&payer.pubkey()));let blockhash = client.get_latest_blockhash().expect("Failed to get blockhash");transaction.sign(&[&payer, &counter_keypair], blockhash);match client.send_and_confirm_transaction(&transaction) {Ok(signature) => {println!("Counter initialized!");println!("Transaction: {}", signature);println!("Counter address: {}", counter_keypair.pubkey());}Err(err) => {eprintln!("Failed to initialize counter: {}", err);return;}}println!("\nIncrementing counter...");// Serialize the increment instruction datalet increment_data = borsh::to_vec(&CounterInstruction::IncrementCounter).expect("Failed to serialize instruction");let increment_instruction = Instruction::new_with_bytes(program_id,&increment_data,vec![AccountMeta::new(counter_keypair.pubkey(), true)],);let mut transaction =Transaction::new_with_payer(&[increment_instruction], Some(&payer.pubkey()));transaction.sign(&[&payer, &counter_keypair], blockhash);match client.send_and_confirm_transaction(&transaction) {Ok(signature) => {println!("Counter incremented!");println!("Transaction: {}", signature);}Err(err) => {eprintln!("Failed to increment counter: {}", err);}}}
Partie 4 : Déploiement du programme
Maintenant que notre programme et notre client sont prêts, construisons, déployons et invoquons le programme.
Construire le programme
D'abord, construisons notre programme.
$cargo build-sbf
Cette commande compile votre programme et génère deux fichiers importants dans
target/deploy/
:
counter_program.so # The compiled programcounter_program-keypair.json # Keypair for the program ID
Vous pouvez consulter l'ID de votre programme en exécutant la commande suivante :
$solana address -k ./target/deploy/counter_program-keypair.json
Exemple de sortie :
HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Démarrer le validator local
Pour le développement, nous utiliserons un validator de test local.
D'abord, configurez la CLI Solana pour utiliser localhost :
$solana config set -ul
Exemple de sortie :
Config File: ~/.config/solana/cli/config.ymlRPC URL: http://localhost:8899WebSocket URL: ws://localhost:8900/ (computed)Keypair Path: ~/.config/solana/id.jsonCommitment: confirmed
Maintenant, démarrez le validator de test dans un terminal séparé :
$solana-test-validator
Déployer le programme
Avec le validator en cours d'exécution, déployez votre programme sur le cluster local :
$solana program deploy ./target/deploy/counter_program.so
Exemple de sortie :
Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJFSignature: 5xKdnh3dDFnZXB5UevYYkFBpCVcuqo5SaUPLnryFWY7eQD2CJxaeVDKjQ4ezQVJfkGNqZGYqMZBNqymPKwCQQx5h
Vous pouvez vérifier le déploiement en utilisant la commande
solana program show
avec l'ID de votre programme :
$solana program show HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Exemple de sortie :
Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJFOwner: BPFLoaderUpgradeab1e11111111111111111111111ProgramData Address: 47MVf5tRZ4zWXQMX7ydrkgcFQr8XTk1QBjohwsUzaiuMAuthority: 4kh6HxYZiAebF8HWLsUWod2EaQQ6iWHpHYCz8UcmFbM1Last Deployed In Slot: 16Data Length: 82696 (0x14308) bytesBalance: 0.57676824 SOL
Exécuter le client
Avec le validator local toujours en cours d'exécution, exécutez le client :
$cargo run --example client
Sortie attendue :
Requesting airdrop...Airdrop confirmedInitializing counter...Counter initialized!Transaction: 2uenChtqNeLC1fitqoVE2LBeygSBTDchMZ4gGqs7AiDvZZVJguLDE5PfxsfkgY7xs6zFWnYsbEtb82dWv9tDT14kCounter address: EppPAmwqD42u4SCPWpPT7wmWKdFad5VnM9J4R9ZfofcyIncrementing counter...Counter incremented!Transaction: 4qv1Rx6FHu1M3woVgDQ6KtYUaJgBzGcHnhej76ZpaKGCgsTorbcHnPKxoH916UENw7X5ppnQ8PkPnhXxEwrYuUxS
Avec le validator local en cours d'exécution, vous pouvez consulter les
transactions sur Solana Explorer
en utilisant les signatures de transaction de sortie. Notez que le cluster sur
Solana Explorer doit être défini sur "Custom RPC URL", qui est par défaut
http://localhost:8899
sur lequel le solana-test-validator
s'exécute.
Is this page helpful?