Instructions
Comme nous l'avons vu précédemment, l'utilisation du trait TryFrom
nous permet de séparer proprement la validation de la logique métier, améliorant ainsi à la fois la maintenabilité et la sécurité.
Structure d'Instruction
Lorsque vient le moment de traiter la logique, nous pouvons créer une structure comme celle-ci :
pub struct Deposit<'a> {
pub accounts: DepositAccounts<'a>,
pub instruction_datas: DepositInstructionData,
}
Cette structure définit les données qui seront accessibles pendant le traitement de la logique. Nous désérialisons ensuite cela à l'aide de la fonction try_from
que vous trouverez dans le fichier lib.rs
:
impl<'a> TryFrom<(&'a [u8], &'a [AccountInfo])> for Deposit<'a> {
type Error = ProgramError;
fn try_from((data, accounts): (&'a [u8], &'a [AccountInfo])) -> Result<Self, Self::Error> {
let accounts = DepositAccounts::try_from(accounts)?;
let instruction_datas = DepositInstructionData::try_from(data)?;
Ok(Self {
accounts,
instruction_datas,
})
}
}
Ce wrapper offre trois avantages clés :
- Il accepte à la fois les entrées brutes (octets et comptes)
- Il délègue la validation aux implémentations individuelles de
TryFrom
- Il renvoie une structure Deposit entièrement typée et validée
Nous pouvons ensuite implémenter la logique de traitement comme ceci :
impl<'a> Deposit<'a> {
pub const DISCRIMINATOR: &'a u8 = &0;
pub fn process(&self) -> ProgramResult {
// deposit logic
Ok(())
}
}
- Le
DISCRIMINATOR
est l'octet que nous utilisons pour la correspondance de modèle dans le point d'entrée - La méthode
process()
ne contient que la logique métier car toutes les vérifications de validation sont déjà terminées
Le résultat ? Nous bénéficions d'une ergonomie de type Anchor avec tous les avantages d'une solution entièrement native : explicite, prédictible et rapide.
Invocation de Programme Croisé
Comme mentionné précédemment, Pinocchio fournit des crates d'aide telles que pinocchio-system
et pinocchio-token
qui simplifient les Invocations de Programme Croisé (CPIs) aux programmes natifs.
Ces structures et méthodes d'aide remplacent l'approche CpiContext
d'Anchor que nous utilisions auparavant :
Transfer {
from: self.accounts.owner,
to: self.accounts.vault,
lamports: self.instruction_datas.amount,
}
.invoke()?;
La structure Transfer
(de pinocchio-system
) encapsule tous les champs nécessaires par le programme système et .invoke()
exécute le CPI. Aucun créateur de contexte ni code passe-partout supplémentaire n'est nécessaire.
Lorsque l'appelant doit être une Adresse Dérivée de Programme (PDA), Pinocchio conserve la même API concise :
let seeds = [
Seed::from(b"vault"),
Seed::from(self.accounts.owner.key().as_ref()),
Seed::from(&[bump]),
];
let signers = [Signer::from(&seeds)];
Transfer {
from: self.accounts.vault,
to: self.accounts.owner,
lamports: self.accounts.vault.lamports(),
}
.invoke_signed(&signers)?;
Voici comment cela fonctionne :
Seeds
crée un tableau d'objets Seed qui correspondent à la dérivation PDASigner
enveloppe ces seeds dans une fonction d'aide Signerinvoke_signed
exécute le CPI en transmettant le tableau des signataires pour autoriser le transfert
Le résultat ? Une interface claire et de première classe pour les CPIs réguliers et signés - aucune macro requise et aucune magie cachée.
Structure d'Instructions Multiples
Souvent, vous souhaiterez réutiliser la même structure de compte et la même logique de validation dans plusieurs instructions, par exemple lors de la mise à jour de différents champs de configuration.
Au lieu de créer un discriminateur distinct pour chaque instruction, vous pouvez utiliser un modèle qui distingue les instructions en fonction de la taille de leur charge utile de données.
Voici comment cela fonctionne :
Nous utilisons un seul discriminateur d'instructions pour toutes les mises à jour de configuration associées. L'instruction précise est déterminée par la longueur des données entrantes.
Ensuite, dans votre processeur, faites correspondre self.data.len()
. Chaque type d'instruction a une taille de données unique, ce qui vous permet de l'envoyer vers le'handler approprié.
Cela ressemblera à ceci :
pub struct UpdateConfig<'a> {
pub accounts: UpdateConfigAccounts<'a>,
pub data: &'a [u8],
}
impl<'a> TryFrom<(&'a [u8], &'a [AccountInfo])> for UpdateConfig<'a> {
type Error = ProgramError;
fn try_from((data, accounts): (&'a [u8], &'a [AccountInfo])) -> Result<Self, Self::Error> {
let accounts = UpdateConfigAccounts::try_from(accounts)?;
// Return the initialized struct
Ok(Self { accounts, data })
}
}
impl<'a> UpdateConfig<'a> {
pub const DISCRIMINATOR: &'a u8 = &4;
pub fn process(&mut self) -> ProgramResult {
match self.data.len() {
len if len == size_of::<UpdateConfigStatusInstructionData>() => {
self.process_update_status()
}
len if len == size_of::<UpdateConfigFeeInstructionData>() => self.process_update_fee(),
len if len == size_of::<UpdateConfigAuthorityInstructionData>() => {
self.process_update_authority()
}
_ => Err(ProgramError::InvalidInstructionData),
}
}
//..
}
Notez que nous reportons la désérialisation des données d'instruction jusqu'à ce que nous sachions quel handler appeler. Cela évite un parsing inutile et permet de conserver la logique du point d'entrée claire.
Chaque handler peut alors désérialiser son type de données spécifique et effectuer la mise à jour :
pub fn process_update_authority(&mut self) -> ProgramResult {
let instruction_data = UpdateConfigAuthorityInstructionData::try_from(self.data)?;
let mut data = self.accounts.config.try_borrow_mut_data()?;
let config = Config::load_mut_unchecked(&mut data)?;
unsafe { config.set_authority_unchecked(instruction_data.authority) }?;
Ok(())
}
pub fn process_update_fee(&mut self) -> ProgramResult {
let instruction_data = UpdateConfigFeeInstructionData::try_from(self.data)?;
let mut data = self.accounts.config.try_borrow_mut_data()?;
let config = Config::load_mut_unchecked(&mut data)?;
unsafe { config.set_fee_unchecked(instruction_data.fee) }?;
Ok(())
}
pub fn process_update_status(&mut self) -> ProgramResult {
let instruction_data = UpdateConfigStatusInstructionData::try_from(self.data)?;
let mut data = self.accounts.config.try_borrow_mut_data()?;
let config = Config::load_mut_unchecked(&mut data)?;
unsafe { config.set_state_unchecked(instruction_data.status) }?;
Ok(())
}
Cette approche vous permet de partager la validation des comptes et d'utiliser un point d'entrée unique pour plusieurs instructions ce qui réduit les répétitions et facilite la maintenance de votre code.
En effectuant une correspondance de modèles sur la taille des données, vous pouvez router efficacement vers la logique appropriée sans avoir recours à des discriminateurs supplémentaires ou à un parsing complexe.