Documentation SolanaDéveloppement de programmesProgrammes Rust

Structure de 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ù 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 :

  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.

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.

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

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 imports dont nous aurons besoin pour le programme et configurons 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

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 :

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 :

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 :

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

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 :

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

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

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

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 imports dont nous aurons besoin pour le programme et configurons 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

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 :

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 :

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 :

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

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 :

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

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

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

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.

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

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

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.

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

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

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

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 :

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 flag --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

Tout 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 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 :

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

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.

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

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

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

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 :

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

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

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

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

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

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

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 afficher 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 validateur local

Pour le développement, nous utiliserons un validateur de test local.

Tout 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 validateur de test dans un terminal séparé :

Terminal
$
solana-test-validator

Déployer le programme

Avec le validateur 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 votre ID de 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 validateur 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 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?

Table des matières

Modifier la page

Géré par

© 2026 Fondation Solana.
Tous droits réservés.
Restez connecté