Documentation SolanaDéveloppement de programmesProgrammes Rust

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 :

  1. InitializeCounter : Crée et initialise un nouveau compte avec une valeur initiale.
  2. 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.

Terminal
$
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.

Terminal
$
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.

Cargo.toml
[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 :

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 :

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 :

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 :

lib.rs
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 :

lib.rs
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 :

lib.rs
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 le counter_account
  • Le désérialise en une structure CounterAccount
  • Incrémente le champ count de 1
  • Sérialise la structure CounterAccount dans le champ data du compte

Ajoutez le code suivant à lib.rs en mettant à jour la fonction process_increment_counter :

lib.rs
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.

Créer un nouveau programme

Tout d'abord, créons un nouveau projet Rust pour notre programme Solana.

Terminal
$
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.

Terminal
$
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.

Cargo.toml
[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 :

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 :

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 :

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 :

lib.rs
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 :

lib.rs
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 :

lib.rs
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 le counter_account
  • Le désérialise en une structure CounterAccount
  • Incrémente le champ count de 1
  • Sérialise la structure CounterAccount dans le champ data du compte

Ajoutez le code suivant à lib.rs en mettant à jour la fonction process_increment_counter :

lib.rs
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.

Cargo.toml
lib.rs
[package]
name = "counter_program"
version = "0.1.0"
edition = "2021"
[dependencies]

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.

Terminal
$
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 :

lib.rs
#[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 :

lib.rs
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.

Terminal
$
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.

lib.rs
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 :

lib.rs
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 :

lib.rs
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 :

lib.rs
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 :

lib.rs
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.

Terminal
$
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: 42
Testing 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

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.

Terminal
$
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 :

lib.rs
#[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 :

lib.rs
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.

Terminal
$
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.

lib.rs
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 :

lib.rs
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 :

lib.rs
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 :

lib.rs
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 :

lib.rs
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.

Terminal
$
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: 42
Testing 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
Cargo.toml
[package]
name = "counter_program"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
[dependencies]
borsh = "1.5.7"
solana-program = "2.2.0"
[dev-dependencies]
litesvm = "0.6.1"
solana-sdk = "2.2.0"

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é.

Terminal
$
mkdir examples
$
touch examples/client.rs

Ajoutez la configuration suivante à Cargo.toml :

Cargo.toml
[[example]]
name = "client"
path = "examples/client.rs"

Installez les dépendances du client :

Terminal
$
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 :

Terminal
$
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 :

examples/client.rs
let program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH")
.expect("Invalid program ID");
examples/client.rs
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 deployment
let program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH")
.expect("Invalid program ID");
// Connect to local cluster
let rpc_url = String::from("http://localhost:8899");
let client = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed());
// Generate a new keypair for paying fees
let payer = Keypair::new();
// Request airdrop of 1 SOL for transaction fees
println!("Requesting airdrop...");
let airdrop_signature = client
.request_airdrop(&payer.pubkey(), 1_000_000_000)
.expect("Failed to request airdrop");
// Wait for airdrop confirmation
loop {
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 data
let 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 data
let 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);
}
}
}

Créer un exemple de client

Créons un client Rust pour interagir avec notre programme déployé.

Terminal
$
mkdir examples
$
touch examples/client.rs

Ajoutez la configuration suivante à Cargo.toml :

Cargo.toml
[[example]]
name = "client"
path = "examples/client.rs"

Installez les dépendances du client :

Terminal
$
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 :

Terminal
$
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 :

examples/client.rs
let program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH")
.expect("Invalid program ID");
Cargo.toml
[package]
name = "counter_program"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
[dependencies]
borsh = "1.5.7"
solana-program = "2.2.0"
[dev-dependencies]
litesvm = "0.6.1"
solana-sdk = "2.2.0"
solana-client = "2.2.0"
tokio = "1.47.1"
[[example]]
name = "client"
path = "examples/client.rs"

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.

Terminal
$
cargo build-sbf

Cette commande compile votre programme et génère deux fichiers importants dans target/deploy/ :

counter_program.so # The compiled program
counter_program-keypair.json # Keypair for the program ID

Vous pouvez consulter l'ID de votre programme en exécutant la commande suivante :

Terminal
$
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 :

Terminal
$
solana config set -ul

Exemple de sortie :

Config File: ~/.config/solana/cli/config.yml
RPC URL: http://localhost:8899
WebSocket URL: ws://localhost:8900/ (computed)
Keypair Path: ~/.config/solana/id.json
Commitment: confirmed

Maintenant, démarrez le validator de test dans un terminal séparé :

Terminal
$
solana-test-validator

Déployer le programme

Avec le validator en cours d'exécution, déployez votre programme sur le cluster local :

Terminal
$
solana program deploy ./target/deploy/counter_program.so

Exemple de sortie :

Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Signature: 5xKdnh3dDFnZXB5UevYYkFBpCVcuqo5SaUPLnryFWY7eQD2CJxaeVDKjQ4ezQVJfkGNqZGYqMZBNqymPKwCQQx5h

Vous pouvez vérifier le déploiement en utilisant la commande solana program show avec l'ID de votre programme :

Terminal
$
solana program show HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF

Exemple de sortie :

Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Owner: BPFLoaderUpgradeab1e11111111111111111111111
ProgramData Address: 47MVf5tRZ4zWXQMX7ydrkgcFQr8XTk1QBjohwsUzaiuM
Authority: 4kh6HxYZiAebF8HWLsUWod2EaQQ6iWHpHYCz8UcmFbM1
Last Deployed In Slot: 16
Data Length: 82696 (0x14308) bytes
Balance: 0.57676824 SOL

Exécuter le client

Avec le validator local toujours en cours d'exécution, exécutez le client :

Terminal
$
cargo run --example client

Sortie attendue :

Requesting airdrop...
Airdrop confirmed
Initializing counter...
Counter initialized!
Transaction: 2uenChtqNeLC1fitqoVE2LBeygSBTDchMZ4gGqs7AiDvZZVJguLDE5PfxsfkgY7xs6zFWnYsbEtb82dWv9tDT14k
Counter address: EppPAmwqD42u4SCPWpPT7wmWKdFad5VnM9J4R9Zfofcy
Incrementing 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?

Table des matières

Modifier la page