Rust
Pinocchio for Dummies

Pinocchio for Dummies

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 :

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

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

  1. Il accepte à la fois les entrées brutes (octets et comptes)
  2. Il délègue la validation aux implémentations individuelles de TryFrom
  3. Il renvoie une structure Deposit entièrement typée et validée

Nous pouvons ensuite implémenter la logique de traitement comme ceci :

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

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

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

  1. Seeds crée un tableau d'objets Seed qui correspondent à la dérivation PDA
  2. Signer enveloppe ces seeds dans une fonction d'aide Signer
  3. invoke_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 :

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

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

Blueshift © 2025Commit: 6d01265