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éfinissent l'état spécifique au programme (données du compte).
  • instructions.rs : Définit les instructions que le programme peut exécuter.
  • processor.rs : Définit les gestionnaires d'instructions (fonctions) qui implémentent la logique métier pour chaque instruction.
  • error.rs : Définit les erreurs personnalisées que le programme peut renvoyer.

Vous pouvez trouver des exemples dans la Solana Program Library.

Programme d'exemple

Pour démontrer comment construire un programme Rust natif avec plusieurs instructions, nous allons parcourir un programme de compteur simple qui implémente deux instructions :

  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.

Créer un nouveau programme

D'abord, créez un nouveau projet Rust en utilisant la commande standard cargo init avec l'option --lib.

Terminal
cargo init counter_program --lib

Naviguez vers le répertoire du projet. Vous devriez voir les fichiers par défaut src/lib.rs et Cargo.toml

Terminal
cd counter_program

Ensuite, ajoutez la dépendance solana-program. C'est la dépendance minimale requise pour construire un programme Solana.

Terminal
cargo add solana-program@1.18.26

Ensuite, ajoutez l'extrait suivant à Cargo.toml. Si vous n'incluez pas cette configuration, le répertoire target/deploy ne sera pas généré lors de la compilation du programme.

Cargo.toml
[lib]
crate-type = ["cdylib", "lib"]

Votre fichier Cargo.toml devrait ressembler à ce qui suit :

Cargo.toml
[package]
name = "counter_program"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
[dependencies]
solana-program = "1.18.26"

Point d'entrée du programme

Un point d'entrée de programme Solana est la fonction qui est appelée lorsqu'un programme est invoqué. Le point d'entrée a la définition brute suivante et les développeurs sont libres de créer leur propre implémentation de la fonction de point d'entrée.

Pour simplifier, utilisez la macro entrypoint! du module solana_program pour définir le point d'entrée dans votre programme.

#[no_mangle]
pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;

Remplacez le code par défaut dans lib.rs par le code suivant. Cet extrait :

  1. Importe les dépendances requises depuis solana_program
  2. Définit le point d'entrée du programme en utilisant la macro entrypoint!
  3. Implémente la fonction process_instruction qui acheminera les instructions vers les fonctions de traitement appropriées
lib.rs
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
msg,
program::invoke,
program_error::ProgramError,
pubkey::Pubkey,
system_instruction,
sysvar::{rent::Rent, Sysvar},
};
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
// Your program logic
Ok(())
}

La macro entrypoint! nécessite une fonction avec la signature de type suivante comme argument :

pub type ProcessInstruction =
fn(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult;

Lorsqu'un programme Solana est invoqué, le point d'entrée désérialise les données d'entrée (fournies sous forme d'octets) en trois valeurs et les transmet à la fonction process_instruction :

  • program_id : La clé publique du programme invoqué (programme actuel)
  • accounts : Le AccountInfo pour les comptes requis par l'instruction invoquée
  • instruction_data : Données supplémentaires transmises au programme qui spécifient l'instruction à exécuter et ses arguments requis

Ces trois paramètres correspondent directement aux données que les clients doivent fournir lors de la construction d'une instruction pour invoquer un programme.

Définir l'état du programme

Lors de la création d'un programme Solana, vous commencerez généralement par définir l'état de votre programme - les données qui seront stockées dans les comptes créés et détenus par votre programme.

L'état du programme est défini à l'aide de structures Rust qui représentent la disposition des données des comptes de votre programme. Vous pouvez définir plusieurs structures pour représenter différents types de comptes pour votre programme.

Lorsque vous travaillez avec des comptes, vous avez besoin d'un moyen de convertir les types de données de votre programme vers et depuis les octets bruts stockés dans le champ de données d'un compte :

  • Sérialisation : Conversion de vos types de données en octets pour les stocker dans le champ de données d'un compte
  • Désérialisation : Conversion des octets stockés dans un compte en vos types de données

Bien que vous puissiez utiliser n'importe quel format de sérialisation pour le développement de programmes Solana, Borsh est couramment utilisé. Pour utiliser Borsh dans votre programme Solana :

  1. Ajoutez la crate borsh comme dépendance à votre Cargo.toml :
Terminal
cargo add borsh
  1. Importez les traits Borsh et utilisez la macro derive pour implémenter les traits pour vos structures :
use borsh::{BorshSerialize, BorshDeserialize};
// Define struct representing our counter account's data
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CounterAccount {
count: u64,
}

Ajoutez la structure CounterAccount à lib.rs pour définir l'état du programme. Cette structure sera utilisée à la fois dans les instructions d'initialisation et d'incrémentation.

lib.rs
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
msg,
program::invoke,
program_error::ProgramError,
pubkey::Pubkey,
system_instruction,
sysvar::{rent::Rent, Sysvar},
};
use borsh::{BorshSerialize, BorshDeserialize};
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
// Your program logic
Ok(())
}
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CounterAccount {
count: u64,
}

Définir les instructions

Les instructions font référence aux différentes opérations que votre programme Solana peut exécuter. Considérez-les comme des API publiques pour votre programme - elles définissent quelles actions les utilisateurs peuvent effectuer lors de l'interaction avec votre programme.

Les instructions sont généralement définies à l'aide d'une énumération Rust où :

  • Chaque variante de l'énumération représente une instruction différente
  • La charge utile de la variante représente les paramètres de l'instruction

Notez que les variantes d'énumération Rust sont implicitement numérotées à partir de 0.

Voici un exemple d'une énumération définissant deux instructions :

#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub enum CounterInstruction {
InitializeCounter { initial_value: u64 }, // variant 0
IncrementCounter, // variant 1
}

Lorsqu'un client invoque votre programme, il doit fournir des instruction data (sous forme de tampon d'octets) où :

  • Le premier octet identifie quelle variante d'instruction exécuter (0, 1, etc.)
  • Les octets restants contiennent les paramètres d'instruction sérialisés (si nécessaire)

Pour convertir les instruction data (octets) en une variante de l'énumération, il est courant d'implémenter une méthode auxiliaire. Cette méthode :

  1. Sépare le premier octet pour obtenir la variante d'instruction
  2. Effectue une correspondance sur la variante et analyse les paramètres supplémentaires à partir des octets restants
  3. Renvoie la variante d'énumération correspondante

Par exemple, la méthode unpack pour l'énumération CounterInstruction :

impl CounterInstruction {
pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
// Get the instruction variant from the first byte
let (&variant, rest) = input
.split_first()
.ok_or(ProgramError::InvalidInstructionData)?;
// Match instruction type and parse the remaining bytes based on the variant
match variant {
0 => {
// For InitializeCounter, parse a u64 from the remaining bytes
let initial_value = u64::from_le_bytes(
rest.try_into()
.map_err(|_| ProgramError::InvalidInstructionData)?
);
Ok(Self::InitializeCounter { initial_value })
}
1 => Ok(Self::IncrementCounter), // No additional data needed
_ => Err(ProgramError::InvalidInstructionData),
}
}
}

Ajoutez le code suivant à lib.rs pour définir les instructions du programme de compteur.

lib.rs
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, msg,
program_error::ProgramError, pubkey::Pubkey,
};
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
// Your program logic
Ok(())
}
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub enum CounterInstruction {
InitializeCounter { initial_value: u64 }, // variant 0
IncrementCounter, // variant 1
}
impl CounterInstruction {
pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
// Get the instruction variant from the first byte
let (&variant, rest) = input
.split_first()
.ok_or(ProgramError::InvalidInstructionData)?;
// Match instruction type and parse the remaining bytes based on the variant
match variant {
0 => {
// For InitializeCounter, parse a u64 from the remaining bytes
let initial_value = u64::from_le_bytes(
rest.try_into()
.map_err(|_| ProgramError::InvalidInstructionData)?,
);
Ok(Self::InitializeCounter { initial_value })
}
1 => Ok(Self::IncrementCounter), // No additional data needed
_ => Err(ProgramError::InvalidInstructionData),
}
}
}

Gestionnaires d'instructions

Les gestionnaires d'instructions font référence aux fonctions qui contiennent la logique métier pour chaque instruction. Il est courant de nommer les fonctions de gestionnaire process_<instruction_name>, mais vous êtes libre de choisir n'importe quelle convention de nommage.

Ajoutez le code suivant à lib.rs. Ce code utilise l'énumération CounterInstruction et la méthode unpack définie à l'étape précédente pour acheminer les instructions entrantes vers les fonctions de gestionnaire appropriées :

lib.rs
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
// Unpack instruction data
let instruction = CounterInstruction::unpack(instruction_data)?;
// Match instruction type
match instruction {
CounterInstruction::InitializeCounter { initial_value } => {
process_initialize_counter(program_id, accounts, initial_value)?
}
CounterInstruction::IncrementCounter => process_increment_counter(program_id, accounts)?,
};
}
fn process_initialize_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
initial_value: u64,
) -> ProgramResult {
// Implementation details...
Ok(())
}
fn process_increment_counter(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
// Implementation details...
Ok(())
}

Ensuite, ajoutez l'implémentation de la fonction process_initialize_counter. Ce gestionnaire d'instruction :

  1. Crée et alloue de l'espace pour un nouveau compte pour stocker les données du compteur
  2. Initialise les données du compte avec initial_value passé à l'instruction

lib.rs
// Initialize a new counter account
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)?;
// Size of our counter account
let account_space = 8; // Size in bytes to store a u64
// Calculate minimum balance for rent exemption
let rent = Rent::get()?;
let required_lamports = rent.minimum_balance(account_space);
// Create the counter account
invoke(
&system_instruction::create_account(
payer_account.key, // Account paying for the new account
counter_account.key, // Account to be created
required_lamports, // Amount of lamports to transfer to the new account
account_space as u64, // Size in bytes to allocate for the data field
program_id, // Set program owner to our program
),
&[
payer_account.clone(),
counter_account.clone(),
system_program.clone(),
],
)?;
// Create a new CounterAccount struct with the initial value
let counter_data = CounterAccount {
count: initial_value,
};
// Get a mutable reference to the counter account's data
let mut account_data = &mut counter_account.data.borrow_mut()[..];
// Serialize the CounterAccount struct into the account's data
counter_data.serialize(&mut account_data)?;
msg!("Counter initialized with value: {}", initial_value);
Ok(())
}

Ensuite, ajoutez l'implémentation de la fonction process_increment_counter. Cette instruction incrémente la valeur d'un compte compteur existant.

lib.rs
// Update an existing counter's value
fn process_increment_counter(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let counter_account = next_account_info(accounts_iter)?;
// Verify account ownership
if counter_account.owner != program_id {
return Err(ProgramError::IncorrectProgramId);
}
// Mutable borrow the account data
let mut data = counter_account.data.borrow_mut();
// Deserialize the account data into our CounterAccount struct
let mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?;
// Increment the counter value
counter_data.count = counter_data
.count
.checked_add(1)
.ok_or(ProgramError::InvalidAccountData)?;
// Serialize the updated counter data back into the account
counter_data.serialize(&mut &mut data[..])?;
msg!("Counter incremented to: {}", counter_data.count);
Ok(())
}

Test des instructions

Pour tester les instructions du programme, ajoutez les dépendances suivantes à Cargo.toml.

Terminal
cargo add solana-program-test@1.18.26 --dev
cargo add solana-sdk@1.18.26 --dev
cargo add tokio --dev

Ensuite, ajoutez le module de test suivant à lib.rs et exécutez cargo test-sbf pour exécuter les tests. Facultativement, utilisez l'option --nocapture pour voir les instructions print dans la sortie.

Terminal
cargo test-sbf -- --nocapture

lib.rs
#[cfg(test)]
mod test {
use super::*;
use solana_program_test::*;
use solana_sdk::{
instruction::{AccountMeta, Instruction},
signature::{Keypair, Signer},
system_program,
transaction::Transaction,
};
#[tokio::test]
async fn test_counter_program() {
let program_id = Pubkey::new_unique();
let (mut banks_client, payer, recent_blockhash) = ProgramTest::new(
"counter_program",
program_id,
processor!(process_instruction),
)
.start()
.await;
// Create a new keypair to use as the address for our counter account
let counter_keypair = Keypair::new();
let initial_value: u64 = 42;
// Step 1: Initialize the counter
println!("Testing counter initialization...");
// Create initialization instruction
let mut init_instruction_data = vec![0]; // 0 = initialize instruction
init_instruction_data.extend_from_slice(&initial_value.to_le_bytes());
let initialize_instruction = Instruction::new_with_bytes(
program_id,
&init_instruction_data,
vec![
AccountMeta::new(counter_keypair.pubkey(), true),
AccountMeta::new(payer.pubkey(), true),
AccountMeta::new_readonly(system_program::id(), false),
],
);
// Send transaction with initialize instruction
let mut transaction =
Transaction::new_with_payer(&[initialize_instruction], Some(&payer.pubkey()));
transaction.sign(&[&payer, &counter_keypair], recent_blockhash);
banks_client.process_transaction(transaction).await.unwrap();
// Check account data
let account = banks_client
.get_account(counter_keypair.pubkey())
.await
.expect("Failed to get counter account");
if let Some(account_data) = account {
let counter: CounterAccount = CounterAccount::try_from_slice(&account_data.data)
.expect("Failed to deserialize counter data");
assert_eq!(counter.count, 42);
println!(
"✅ Counter initialized successfully with value: {}",
counter.count
);
}
// Step 2: Increment the counter
println!("Testing counter increment...");
// Create increment instruction
let increment_instruction = Instruction::new_with_bytes(
program_id,
&[1], // 1 = increment instruction
vec![AccountMeta::new(counter_keypair.pubkey(), true)],
);
// Send transaction with increment instruction
let mut transaction =
Transaction::new_with_payer(&[increment_instruction], Some(&payer.pubkey()));
transaction.sign(&[&payer, &counter_keypair], recent_blockhash);
banks_client.process_transaction(transaction).await.unwrap();
// Check account data
let account = banks_client
.get_account(counter_keypair.pubkey())
.await
.expect("Failed to get counter account");
if let Some(account_data) = account {
let counter: CounterAccount = CounterAccount::try_from_slice(&account_data.data)
.expect("Failed to deserialize counter data");
assert_eq!(counter.count, 43);
println!("✅ Counter incremented successfully to: {}", counter.count);
}
}
}

Exemple de sortie :

Terminal
running 1 test
[2024-10-29T20:51:13.783708000Z INFO solana_program_test] "counter_program" SBF program from /counter_program/target/deploy/counter_program.so, modified 2 seconds, 169 ms, 153 µs and 461 ns ago
[2024-10-29T20:51:13.855204000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM invoke [1]
[2024-10-29T20:51:13.856052000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 invoke [2]
[2024-10-29T20:51:13.856135000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 success
[2024-10-29T20:51:13.856242000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Counter initialized with value: 42
[2024-10-29T20:51:13.856285000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM consumed 3791 of 200000 compute units
[2024-10-29T20:51:13.856307000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM success
[2024-10-29T20:51:13.860038000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM invoke [1]
[2024-10-29T20:51:13.860333000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Counter incremented to: 43
[2024-10-29T20:51:13.860355000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM consumed 756 of 200000 compute units
[2024-10-29T20:51:13.860375000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM success
test test::test_counter_program ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s

Is this page helpful?

Table des matières

Modifier la page