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ù commence l'exécution
d'un programme.
Structure de 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 de 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 retourner.
Par exemple, consultez le Token Program.
Exemple de programme
Pour démontrer comment construire un programme Rust natif avec plusieurs instructions, nous allons parcourir un simple programme de compteur 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.
Par souci de simplicité, le programme sera implémenté dans un seul fichier
lib.rs, bien qu'en pratique vous puissiez 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 aucune obligation d'utiliser Borsh. Cependant, c'est une bibliothèque de sérialisation couramment utilisée pour les programmes Solana.
Configurer le crate-type
Les programmes Solana doivent être compilés en tant que 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 imports dont nous aurons besoin pour le programme et configurons 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
Définissons maintenant la structure de données qui sera stockée dans nos comptes
de 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 des 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
Nous devons maintenant désérialiser les instruction_data (octets bruts) en
l'une de nos variantes d'énumération CounterInstruction. La méthode
try_from_slice de Borsh gère cette conversion automatiquement.
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
Mettons maintenant à jour la fonction principale process_instruction pour
acheminer les instructions vers leurs fonctions de gestionnaire appropriées.
Ce modèle d'acheminement est courant dans les programmes Solana. Les
instruction_data sont désérialisées en une variante d'une énumération
représentant l'instruction, puis la fonction de gestionnaire appropriée est
appelée. Chaque fonction de gestionnaire inclut l'implémentation de 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 de compteur. Étant donné que seul le System Program peut créer des comptes sur Solana, nous utiliserons une invocation de programme croisée (CPI), c'est-à-dire appeler 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, ce qui donne à 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 fournie à titre de démonstration uniquement. Elle n'inclut pas les vérifications de sécurité et de validation qui sont requises pour les programmes en production.
Implémenter le gestionnaire d'incrémentation
Implémentons maintenant le gestionnaire qui incrémente un compteur existant. Cette instruction :
- Lit le champ
datadu compte pour lecounter_account - Le désérialise en une structure
CounterAccount - Incrémente le champ
countde 1 - Sérialise la structure
CounterAccountdans le champdatadu 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 fournie à titre de démonstration uniquement. Elle n'inclut pas les vérifications de sécurité et de validation qui sont requises pour les programmes en production.
Programme terminé
Félicitations ! Vous avez créé 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ù commence l'exécution du programme 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 gestionnaire associées
- Gestion de l'état : définit les structures de données des comptes et gère leur état dans les comptes appartenant au programme
- Invocation de programme croisé (CPI) : appelle le System Program pour créer de nouveaux comptes appartenant au programme
L'étape suivante consiste à tester le programme pour s'assurer que tout fonctionne correctement.
Partie 2 : tester le programme
Testons maintenant notre programme de compteur. Nous utiliserons LiteSVM, un framework de test qui permet de tester les programmes sans les déployer sur un cluster.
Ajouter les dépendances de test
Tout 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 le module de test
Ajoutons maintenant un module de test à notre programme. Nous commencerons par la structure de base et les imports.
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
Nous devons maintenant 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 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 de lancer 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
Testons maintenant 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 flag --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 : Invocation du programme
Ajoutons maintenant 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 client
Implémentons maintenant 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 client à examples/client.rs et remplacez le program_id par
la sortie 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éployer le programme
Maintenant que notre programme et notre client sont prêts, construisons, déployons et invoquons le programme.
Construire le programme
Tout 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 afficher 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 validateur local
Pour le développement, nous utiliserons un validateur de test local.
Tout 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 validateur de test dans un terminal séparé :
$solana-test-validator
Déployer le programme
Avec le validateur 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 votre ID de 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 validateur 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 validateur local en cours d'exécution, vous pouvez consulter les
transactions sur Solana Explorer
en utilisant les signatures de transaction affichées. Notez que le cluster sur
Solana Explorer doit être défini sur « Custom RPC URL », qui pointe par défaut
vers http://localhost:8899 sur lequel le solana-test-validator s'exécute.
Is this page helpful?