
Introduction à Anchor : guide du débutant pour créer des programmes Solana
Sommaire
- De quoi parle cet article ?
- Prérequis
- Installation d’Anchor
- Installation de Rust
- Installation de la suite d’outils Solana
- Installation de Yarn
- Installation d’Anchor avec AVM
- Installation d’Anchor avec les binaires et compilation depuis le code source
- Solana Playground
- Hello, World!
- Création d’un projet avec une installation locale d’Anchor
- Création d’un projet avec Solana Playground
- Écriture de Hello, World!
- Compilation et déploiement en local
- Déploiement sur Devnet
- Compilation et déploiement sur Solana Playground
- Abstraction efficace : IDL et macros
- Structure d’un programme Anchor
- Types de comptes
- Contraintes de compte
- Analyse des contraintes d’un programme
- Espace des comptes
- Erreurs
- Appels interprogrammes (CPI)
- Élévation des privilèges
- Exécution d’un CPI
- Adresses dérivées de programme (PDA)
- Conclusion
- Ressources supplémentaires
Un grand merci à Noah, Mike, Jonas, Ryan, Prames et bl0ckpain pour leur relecture de cet article.
De quoi parle cet article ?
Rust est souvent décrit comme la lingua franca du développement de programmes Solana. Il serait toutefois plus juste de qualifier Anchor ainsi, puisque la plupart des développements en Rust utilisent ce framework. Anchor est un framework puissant et structurant conçu pour créer rapidement des programmes Solana sécurisés. Il simplifie le processus de développement en réduisant le code répétitif dans des domaines tels que la (dé)sérialisation des comptes et les données d’instruction, en effectuant des vérifications de sécurité essentielles, en générant automatiquement des bibliothèques clientes et en fournissant un environnement de test complet.
Cet article explique comment développer des programmes Anchor. Il aborde l’installation d’Anchor, l’utilisation de Solana Playground, ainsi que la création, la compilation et le déploiement d’un programme Hello, World! simple. Nous verrons ensuite plus en détail comment Anchor simplifie le processus de développement en examinant les IDL, les macros, la structure des programmes Anchor, les types et contraintes de comptes, ainsi que la gestion des erreurs. Nous aborderons également brièvement les invocations interprogrammes et les adresses dérivées de programme. Cet article vous fournira tout ce dont vous avez besoin pour commencer à utiliser Anchor dès aujourd’hui.
Prérequis
Cet article suppose que vous connaissez le modèle de programmation de Solana. Si vous débutez dans le développement sur Solana, je vous recommande de lire mon précédent article, Le modèle de programmation de Solana : introduction au développement sur Solana.
Ne vous inquiétez pas si vous débutez avec Rust : aucune connaissance avancée n’est nécessaire pour commencer à développer avec Anchor. La documentation d’Anchor précise que les développeurs doivent simplement maîtriser les bases de Rust (c’est-à-dire les neuf premiers chapitres du livre Rust). Je vous recommande de regarder Le guide de survie Rust pour découvrir une présentation claire des concepts essentiels de la programmation en Rust. Il est également crucial de comprendre les règles de Rust concernant la mémoire, la propriété et l’emprunt.
Pour faciliter l’apprentissage, je recommande aux développeurs qui découvrent les langages de bas niveau d’étudier différents concepts propres à la programmation système, souvent omis par les ressources consacrées à Rust. Je vous conseille par exemple d’explorer des sujets tels que la taille des variables, les pointeurs et les fuites de mémoire. Je recommande également Rust par l’exemple ainsi que mon dépôt consacré à diverses structures de données et divers algorithmes écrits en Rust, qui présentent des exemples concrets d’utilisation de Rust.
Vous préférez utiliser TypeScript ? Découvrez comment écrire des programmes Solana en TypeScript en utilisant le framework de Poseidon pour transpiler TypeScript en Rust et générer des programmes Anchor valides.
Cet article porte sur le développement avec Anchor, et uniquement sur celui-ci. Nous n’expliquerons pas comment développer des programmes en Rust natif, et cet article ne suppose aucune connaissance en la matière. Il n’abordera pas non plus le développement côté client avec Anchor : un prochain article expliquera comment tester les programmes Anchor et interagir avec eux via TypeScript.
Cela étant dit, commençons avec Anchor !
Installation d’Anchor
La configuration d’Anchor nécessite quelques étapes simples pour installer les outils et paquets requis. Cette section traite de l’installation de ces outils et paquets, à savoir Rust, la suite d’outils Solana, Yarn et Anchor Version Manager.
Installation de Rust
Vous pouvez installer Rust depuis le site officiel de Rust ou via la ligne de commande :
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shInstallation de la suite d’outils Solana
Anchor nécessite également la suite d’outils Solana. Vous pouvez installer la dernière version (1.17.16 au moment de la rédaction de cet article) avec la commande suivante sous macOS et Linux :
sh -c "$(curl -sSfL https://release.solana.com/v1.17.16/install)"Sous Windows, vous pouvez installer la suite d’outils Solana à l’aide de la commande suivante :
cmd /c "curl https://release.solana.com/v1.17.16/solana-install-init-x86_64-pc-windows-msvc.exe --output C:\solana-install-tmp\solana-install-init.exe --create-dirs"Il est toutefois vivement recommandé d’utiliser plutôt Windows Subsystem for Linux (WSL). Vous pourrez ainsi exécuter un environnement Linux sur votre machine Windows sans double démarrage ni machine virtuelle distincte. Si vous choisissez cette solution, reportez-vous aux instructions d’installation pour Linux, c’est-à-dire à la commande curl.
Les développeurs peuvent également remplacer v1.17.16 par l’étiquette de la version qu’ils souhaitent télécharger. Il est aussi possible d’utiliser les noms de canaux stable, beta ou edge. Après l’installation, exécutez solana –-version pour confirmer que la version souhaitée de solana est installée.
Installation de Yarn
Anchor nécessite également Yarn. Vous pouvez l’utiliser avec Corepack, inclus dans toutes les versions officielles de Node.js à partir de Node.js 14.9 / 16.9. Cependant, son activation reste actuellement facultative pendant sa phase expérimentale. Vous devez donc exécuter corepack enable avant de pouvoir l’utiliser. Certains distributeurs tiers n’incluent pas Corepack par défaut. Vous devrez alors peut-être exécuter npm install -g corepack avant corepack enable.
Installation d’Anchor avec AVM
La documentation d’Anchor recommande d’installer Anchor via Anchor Version Manager (AVM). AVM simplifie la gestion et la sélection de plusieurs installations du binaire anchor-cli. Cela peut être nécessaire pour produire des builds vérifiables ou utiliser différentes versions selon les programmes. Vous pouvez l’installer à l’aide de Cargo avec la commande suivante : cargo install --git [https://github.com/coral-xyz/anchor](https://github.com/coral-xyz/anchor) avm --locked --force. Installez ensuite la dernière version et utilisez-la :
avm install latest
avm use latest
# Verify the installation
avm --versionPour obtenir la liste des versions disponibles d’anchor-cli, utilisez la commande avm list. Les développeurs peuvent utiliser avm use <version> pour sélectionner une version précise. Cette version restera utilisée jusqu’à ce qu’elle soit modifiée. Les développeurs peuvent désinstaller une version donnée avec la commande avm uninstall <version>.
Installation d’Anchor avec les binaires et compilation depuis le code source
Sous Linux, les binaires Anchor sont disponibles via le paquet npm @coral-xyz/anchor-cli. Seul Linux x86_64 est actuellement pris en charge. Les développeurs doivent donc compiler le code source pour les autres systèmes d’exploitation. Ils peuvent utiliser Cargo pour installer directement la CLI. Par exemple :
cargo install --git https://github.com/coral-xyz/anchor --tag v0.29.0 anchor-cli --lockedModifiez l’argument --tag pour installer une autre version d’Anchor. Si l’installation avec Cargo échoue, vous devrez peut-être installer des dépendances supplémentaires. Par exemple, sous Ubuntu :
sudo apt-get update && sudo apt-get upgrade && sudo apt-get install -y pkg-config build-essential libudev-devLes développeurs peuvent ensuite vérifier leur installation d’Anchor avec la commande anchor --version.
Solana Playground
Les développeurs peuvent également commencer à utiliser Anchor avec Solana Playground (Solpg). Solana Playground est un IDE accessible dans le navigateur qui permet de développer, tester et déployer rapidement des programmes Solana.
Lors de leur première utilisation de Solana Playground, les développeurs doivent créer un portefeuille Playground. Cliquez sur l’indicateur d’état rouge Not connected en bas à gauche de l’écran. La fenêtre modale suivante s’affichera :
Il est recommandé d’enregistrer le fichier de la paire de clés du portefeuille comme sauvegarde avant de cliquer sur Continue. En effet, le portefeuille Playground est enregistré dans le stockage local du navigateur. Vider le cache du navigateur supprimera le portefeuille.
Cliquez sur Continue pour créer un portefeuille devnet prêt à être utilisé dans l’IDE.
Pour approvisionner le portefeuille, les développeurs peuvent exécuter la commande solana airdrop <amount> dans le terminal de Playground, en remplaçant <amount> par la quantité souhaitée de SOL devnet. Vous pouvez également obtenir des SOL devnet sur ce faucet. Je vous recommande de consulter ce guide expliquant comment obtenir des SOL devnet.
Notez que vous pourriez rencontrer l’erreur suivante :
Error: unable to confirm transaction. This can happen in situations such as transaction expiration and insufficient fee-payer fundsCette erreur survient souvent lorsque le faucet devnet est épuisé et/ou que la quantité de SOL demandée est trop importante. La limite actuelle est de 5 SOL, ce qui est largement suffisant pour déployer ce programme. Il est donc recommandé de demander 5 SOL au faucet ou d’exécuter la commande solana airdrop 5. Demander progressivement de plus petites quantités peut entraîner une limitation du débit.
Hello, World!
Les programmes Hello, World! sont considérés comme une excellente introduction aux nouveaux frameworks et langages de programmation. Leur simplicité les rend accessibles aux développeurs de tous niveaux. Ils permettent également de comprendre la structure et la syntaxe de base du nouveau modèle de programmation sans introduire de logique ni de fonctions complexes. Hello, World! est rapidement devenu un programme d’initiation incontournable en programmation. Il est donc tout naturel d’en écrire un nous-mêmes pour Anchor. Cette section explique comment compiler et déployer un programme Hello, World! avec une installation locale d’Anchor, ainsi qu’avec Solana Playground.
Création d’un projet avec une installation locale d’Anchor
Avec Anchor installé, créer un projet est aussi simple que ceci :
anchor init hello-world
cd hello-worldCes commandes initialisent un projet Anchor nommé hello-world, puis ouvrent son répertoire. Dans ce répertoire, accédez à hello-world/programs/hello-world/src/lib.rs. Ce fichier contient le code de départ suivant :
use anchor_lang::prelude::*;
declare_id!("HZfVb1ohL1TejhZNkgFSKqGsyTznYtrwLV6GpA8BwV5Q");
#[program]
pub mod hello-world {
use super::*;
pub fn initialize(ctx: Context) -> Result<()> {
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize {}Anchor a préparé plusieurs fichiers et répertoires pour nous. Plus précisément :
- Un répertoire app vide pour le client du programme
- Un dossier programs qui contiendra tous nos programmes Solana
- Un dossier tests pour les tests JavaScript. Il contient un fichier de test généré automatiquement pour le code de départ
- Un fichier de configuration Anchor.toml. Si vous débutez avec Rust, un fichier TOML est un format de fichier de configuration minimaliste dont la sémantique facilite la lecture. Le fichier Anchor.toml sert à configurer la manière dont Anchor interagit avec le programme, par exemple le cluster sur lequel celui-ci doit être déployé.
Création d’un projet avec Solana Playground
Créer un projet sur Solana Playground est très simple. Accédez au coin supérieur gauche et cliquez sur Create a New Project :
La fenêtre modale suivante s’affichera :
Nommez votre programme, sélectionnez Anchor(Rust), puis cliquez sur Create. Un projet Anchor sera directement créé dans votre navigateur. Dans la section Program à gauche, vous verrez un répertoire src. Il contient lib.rs, qui comprend le code de départ suivant :
use anchor_lang::prelude::*;
// This is your program's public key and it will update
// automatically when you build the project.
declare_id!("11111111111111111111111111111111");
#[program]
mod hello_anchor {
use super::*;
pub fn initialize(ctx: Context, data: u64) -> Result<()> {
ctx.accounts.new_account.data = data;
msg!("Changed data to: {}!", data); // Message will show up in the tx logs
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
// We must specify the space in order to initialize an account.
// First 8 bytes are default account discriminator,
// next 8 bytes come from NewAccount.data being type u64.
// (u64 = 64 bits unsigned integer = 8 bytes)
#[account(init, payer = signer, space = 8 + 8)]
pub new_account: Account<'info, NewAccount>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct NewAccount {
data: u64
}Notez que Solana Playground génère uniquement les fichiers client.ts et anchor.test.ts. Je vous recommande de lire la section consacrée à la création locale d’un programme avec Anchor pour découvrir les éléments généralement générés pour un nouveau projet Anchor.
Écriture de Hello, World!
Que vous utilisiez Anchor en local ou via Solana Playground, remplacez le code de départ par le code suivant pour créer un programme Hello, World! très simple :
use anchor_lang::prelude::*;
declare_id!("HZfVb1ohL1TejhZNkgFSKqGsyTznYtrwLV6GpA8BwV5Q");
#[program]
mod hello_world {
use super::*;
pub fn hello(_ctx: Context<Hello>) -> Result<()> {
msg!("Hello, World!");
Ok(())
}
#[derive(Accounts)]
pub struct Hello {}
}Nous examinerons en détail chaque partie dans les sections suivantes. Pour l’instant, il est important de noter l’utilisation de macros et de traits pour simplifier le processus de développement. La macro declare_id! définit la clé publique du programme. En développement local, la commande anchor init utilisée pour configurer le programme génère une paire de clés dans le répertoire target/deploy et renseigne cette macro. Solana Playground s’en charge également automatiquement.
Dans notre module principal hello_world, nous créons une fonction qui journalise Hello, World!. Elle renvoie également Ok(()) pour signaler que l’exécution du programme a réussi. Notez que nous préfixons ctx d’un trait de soulignement afin d’éviter les avertissements de variable inutilisée dans la console. Hello est une structure de compte qui ne nécessite aucun compte en argument, puisque le programme se contente de journaliser un nouveau message.
C’est tout ! Aucun compte n’est nécessaire et aucune logique complexe n’est requise. Le code présenté ci-dessus crée un programme qui journalise Hello, World!
Compilation et déploiement en local
Cette section porte sur le déploiement sur Localhost. Bien que Solana Playground utilise devnet par défaut, un environnement de développement local offre une bien meilleure expérience aux développeurs. En plus d’être plus rapide, il permet d’éviter plusieurs problèmes couramment rencontrés lors des tests sur devnet, comme un solde de SOL insuffisant pour les transactions, des déploiements lents ou l’impossibilité d’effectuer des tests lorsque devnet est indisponible. À l’inverse, le développement local garantit un état vierge pour chaque test. Il offre donc un environnement de développement mieux contrôlé et plus efficace.
Configuration de nos outils
Nous devons tout d’abord nous assurer que la suite d’outils Solana est correctement configurée pour le développement sur Localhost. Exécutez la commande solana config set --url localhost pour vérifier que toutes les configurations pointent vers des URL Localhost.
Assurez-vous également de disposer d’une paire de clés locale pour interagir avec Solana en local. Vous devez posséder un portefeuille Solana doté d’un solde en SOL pour déployer un programme avec la CLI Solana. Exécutez la commande solana address pour vérifier si vous disposez déjà d’une paire de clés locale. Si une erreur survient, exécutez la commande solana-keygen new. Par défaut, un nouveau portefeuille sera créé dans le système de fichiers à l’emplacement ~/.config/solana/id.json. Une phrase de récupération permettant de restaurer les clés publique et privée vous sera également fournie. Il est recommandé de sauvegarder cette paire de clés, même si elle n’est utilisée qu’en local. Notez également que si un portefeuille est déjà enregistré dans le système de fichiers à l’emplacement par défaut, la commande solana-keygen new ne le remplacera pas, sauf si vous le précisez avec la commande --force.
Configuration du fichier Anchor.toml
Nous devons ensuite nous assurer que notre fichier Anchor.toml pointe correctement vers Localhost. Vérifiez qu’il contient le code suivant :
...
[programs.localnet]
hello-world = "EJTW6qsbfya86xeLRQpKLM8qhn11cJXmU35QbJwE11R8"
...
[provider]
cluster = "Localnet"
wallet = '~config/solana/id.json'Ici, [programs.localnet] désigne l’ID du programme sur localnet, c’est-à-dire Localhost. L’ID du programme est toujours défini en fonction du cluster. En effet, un même programme peut être déployé à une autre adresse sur un autre cluster. Du point de vue de l’expérience développeur, devoir déclarer de nouveaux ID de programme pour les programmes déployés sur différents clusters peut être fastidieux.
L’ID du programme est public. Sa paire de clés est toutefois stockée dans le dossier target/deploy. Son nom respecte une convention précise basée sur le nom du programme. Par exemple, si le programme s’appelle hello_world, Anchor recherchera une paire de clés à l’emplacement target/deploy/hello-world-keypair.json. Si Anchor ne trouve pas ce fichier lors du déploiement, il génère une nouvelle paire de clés. Le programme reçoit alors un nouvel ID. Il est donc essentiel de mettre à jour l’ID du programme après le premier déploiement. Le fichier hello-world-keypair.json sert de preuve de propriété du programme. Si la paire de clés est divulguée, des acteurs malveillants peuvent apporter des modifications non autorisées au programme.
Avec [provider], nous indiquons à Anchor d’utiliser Localhost et le portefeuille spécifié pour payer le stockage et les transactions.
Compilation, déploiement et exécution d’un registre local
Utilisez la commande anchor build pour compiler le programme. Pour compiler un programme précis à partir de son nom, utilisez la commande anchor build -p <program name> en remplaçant <program name> par le nom du programme. Comme nous développons sur localnet, nous pouvons utiliser les commandes localnet de la CLI Anchor pour simplifier le processus de développement. Par exemple, anchor localnet --skip-build est particulièrement utile pour éviter de compiler un programme dans l’espace de travail. Cela permet de gagner du temps lors de l’exécution des tests si le code du programme n’a pas été modifié.
Si nous essayons maintenant d’exécuter la commande anchor deploy, nous obtiendrons une erreur. En effet, aucun cluster Solana sur lequel effectuer nos tests n’est en cours d’exécution sur notre machine. Nous pouvons lancer un registre local pour simuler un cluster sur notre machine. La CLI Solana intègre déjà un validateur de test. L’exécution de la commande solana-test-validator démarre un cluster mononœud complet sur votre poste de travail. Cette approche offre de nombreux avantages : aucune limite de débit RPC, aucune limite d’airdrop, déploiement direct de programmes on-chain, chargement de comptes à partir de fichiers et clonage de comptes depuis un cluster public. Le validateur de test doit s’exécuter dans une autre fenêtre de terminal ouverte et rester actif pour que le cluster localhost reste en ligne et disponible.
Nous pouvons maintenant exécuter anchor deploy pour déployer le programme sur notre registre local. Toutes les données transmises au registre local seront enregistrées dans un dossier test-ledger généré dans le répertoire de travail actuel. Il est recommandé d’ajouter ce dossier à votre fichier .gitignore pour éviter de le valider dans votre dépôt. Par ailleurs, l’arrêt du registre local, c’est-à-dire l’utilisation de Ctrl + C dans le terminal, ne supprimera aucune donnée envoyée au cluster. Pour les supprimer, effacez le dossier test-ledger ou exécutez solana-test-validator --reset.
Félicitations ! Vous venez de déployer votre premier programme Solana sur Localhost !
Solana Explorer
Les développeurs peuvent également configurer Solana Explorer pour leur registre local. Accédez à Solana Explorer. Dans la barre de navigation, cliquez sur le bouton vert indiquant le cluster actuel :
Une barre latérale vous permettant de choisir un cluster s’ouvrira. Cliquez sur Custom RPC URL. Le champ devrait automatiquement contenir http://localhost:8899. Si ce n’est pas le cas, saisissez cette adresse afin que l’explorateur pointe vers le port 8899 de votre machine :
Cette fonctionnalité est précieuse pour plusieurs raisons :
- Elle permet aux développeurs d’inspecter en temps réel les transactions de leur registre local, en reproduisant les fonctionnalités normalement offertes par un explorateur de blocs analysant devnet ou mainnet
- Elle facilite la visualisation de l’état des comptes, des tokens et des programmes comme s’ils fonctionnaient sur un cluster actif
- Elle fournit des informations détaillées sur les erreurs et les échecs de transaction
- Elle offre une expérience de développement homogène entre les clusters grâce à une interface familière
Déploiement sur Devnet
Même si nous recommandons le développement sur Localhost, les développeurs peuvent aussi déployer leur programme sur devnet s’ils souhaitent effectuer des tests spécifiquement sur ce cluster. Le processus est globalement identique, à ceci près qu’il n’est pas nécessaire d’exécuter un registre local, puisque nous disposons d’un cluster Solana complet avec lequel interagir.
Exécutez la commande solana config set --url devnet pour sélectionner devnet comme cluster. Toute commande solana exécutée dans le terminal s’exécutera désormais sur devnet. Dans le fichier Anchor.toml, dupliquez ensuite la section [programs.localnet] et renommez-la [programs.devnet]. Modifiez également [provider] afin qu’il pointe vers devnet :
...
[programs.localnet]
hello-world = "EJTW6qsbfya86xeLRQpKLM8qhn11cJXmU35QbJwE11R8"
[programs.devnet]
hello-world = "EJTW6qsbfya86xeLRQpKLM8qhn11cJXmU35QbJwE11R8"
...
[provider]
cluster = "Devnet"
wallet = '~config/solana/id.json'Les développeurs doivent s’assurer de disposer de SOL devnet pour déployer le programme. Utilisez la commande solana airdrop <amount> pour effectuer un airdrop vers la paire de clés située par défaut à l’emplacement ~/.config/solana/id.json. Vous pouvez également préciser l’adresse d’un portefeuille avec solana aidrop <amount> <wallet address>. Vous pouvez aussi obtenir des SOL devnet sur ce faucet. Je vous recommande de consulter ce guide expliquant comment obtenir des SOL devnet.
Vous pourriez rencontrer l’erreur suivante : impossible de confirmer la transaction. Cela peut se produire dans certaines situations, comme l’expiration de la transaction ou l’insuffisance des fonds du payeur des frais
Cette erreur survient souvent lorsque le faucet devnet est épuisé et/ou qu’une quantité de SOL trop importante est demandée en une seule fois. La limite actuelle est de 5 SOL, ce qui est largement suffisant pour déployer ce programme. Il est donc recommandé de demander 5 SOL au faucet ou d’exécuter la commande solana airdrop 5. Demander progressivement de plus petites quantités peut entraîner une limitation du débit.
Compilez et déployez maintenant le programme à l’aide des commandes suivantes :
anchor build
anchor deployFélicitations ! Vous venez de déployer localement votre premier programme Solana sur devnet !
Compilation et déploiement sur Solana Playground
Dans Solana Playground, accédez à l’icône Tools dans la barre latérale gauche. Cliquez sur Build. Vous devriez voir le texte suivant dans la console :
Building...
Build successful. Completed in 2.20s..Notez que l’ID de la macro declare_id! a été remplacé. Cette nouvelle adresse correspond à l’emplacement où nous déploierons le programme. Cliquez maintenant sur Deploy. Votre console devrait afficher quelque chose de similaire à ceci :
Deploying... This could take a while depending on the program size and network conditions.
Warning: 41 transactions not confirmed, retrying...
Deployment successful. Completed in 17sFélicitations ! Vous venez de déployer votre premier programme Solana sur devnet via Solana Playground !
Abstraction efficace : IDL et macros
Anchor simplifie le développement de programmes grâce à une abstraction efficace. Autrement dit, Anchor simplifie les concepts complexes de programmation blockchain, ce qui les rend plus accessibles et plus faciles à utiliser. Par exemple, Anchor emploie un langage de description d’interface (IDL) pour définir l’interface du programme. Lors de la création d’un programme, Anchor génère un fichier JSON représentant l’IDL du programme. Cette structure peut essentiellement être utilisée côté client pour définir comment interagir avec les fonctions et les structures de données du programme. Anchor fournit également des abstractions de plus haut niveau pour gérer l’état. Anchor permet aux développeurs de définir l’état de leur programme avec des structs Rust, ce qui peut être plus intuitif que de manipuler des tableaux d’octets bruts ou d’effectuer des sérialisations manuelles. Les développeurs peuvent donc définir l’état comme ils le feraient normalement avec n’importe quelle structure de données Rust classique, puis Anchor se charge de la sérialisation sous-jacente et du stockage dans les comptes.
Il est également très simple de publier un IDL on-chain. Les développeurs peuvent publier un IDL avec la commande suivante :
anchor idl init --filepath --provider.cluster --provider.walletAssurez-vous que le wallet fourni est l’autorité du programme et qu’il contient suffisamment de SOL pour la transaction. Les développeurs peuvent désormais consulter leur IDL dans un explorateur de blocs tel qu’Orb.
Voici, par exemple, l’IDL de l’agrégateur v4 de DFlow sur Orb.
Les macros d’Anchor constituent l’une des abstractions les plus importantes, si ce n’est la plus importante. En Rust, une macro est un fragment de code qui génère un autre fragment de code. Il s’agit d’une forme de métaprogrammation. Les macros déclaratives sont la forme de macros la plus utilisée en Rust. Elles permettent aux développeurs d’écrire quelque chose de similaire à une expression match grâce à la construction macro_rules!. Les macros procédurales fonctionnent davantage comme une fonction : elles acceptent du code en entrée, opèrent sur ce code et produisent un résultat. Dans Anchor, par exemple, la macro #[account] définit et applique des contraintes aux comptes Solana. Cela permet de réduire la complexité et les erreurs potentielles liées à la gestion des comptes. Présenter les macros d’Anchor implique inévitablement d’aborder la structure des programmes Anchor.
Structure d’un programme Anchor
La structure des programmes Anchor est conçue pour exploiter une combinaison de macros et de traits afin de générer le code standard et d’appliquer la logique du programme. Cette philosophie de conception contribue largement à simplifier le processus de développement ainsi qu’à garantir la cohérence et la fiabilité du comportement du programme.
Les déclarations use se trouvent en haut du fichier. Notez qu’elles relèvent de la sémantique générale du langage Rust et ne sont pas propres à Anchor. Ces déclarations créent une ou plusieurs liaisons de noms locales synonymes d’un autre chemin : les déclarations use raccourcissent le chemin nécessaire pour faire référence à un élément de module. Elles peuvent apparaître dans des modules ou des blocs. De plus, le mot-clé self peut lier une liste de chemins partageant un préfixe commun ainsi que leur module parent commun. Par exemple, toutes ces déclarations use sont valides :
use anchor_lang::prelude::*;
use std::collections::hash_map::{self, HashMap};
use a::b::{c, d, e::f, g::h::i};
use a::b::{self, c, d::e};La première macro Anchor qu’un développeur rencontrera est declare_id!. Elle sert à déclarer l’adresse du programme (l’ID du programme), afin que toutes les interactions soient correctement acheminées vers celui-ci. Anchor génère une nouvelle paire de clés lorsqu’un développeur compile un programme Anchor pour la première fois. Sauf indication contraire, cette paire de clés sert à déployer le programme. La clé publique de la paire de clés doit être fournie comme ID du programme à la macro declare_id! :
declare_id!("HZfVb1ohL1TejhZNkgFSKqGsyTznYtrwLV6GpA8BwV5Q");La macro d’attribut #[program] désigne le module contenant la logique des instructions du programme. Elle sert de point d’entrée et définit comment le programme interprète et exécute les instructions reçues. Cette macro simplifie l’acheminement de ces instructions vers la fonction appropriée du programme, ce qui rend son code plus organisé et plus facile à gérer. Chaque fonction de ce module est traitée comme une instruction distincte. Chacune prend comme premier argument un paramètre de contexte (ctx) de type Context. Les développeurs peuvent accéder aux comptes, à l’ID du programme en cours d’exécution et aux comptes restants.
Le type Context est défini comme suit :
pub struct Context<'a, 'b, 'c, 'info, T: Bumps> {
pub program_id: &'a Pubkey,
pub accounts: &'b mut T,
pub remaining_accounts: &'c [AccountInfo<'info>],
pub bumps: T::Bumps,
}Cela permet de fournir à un programme donné des entrées qui ne sont pas des arguments. Le champ program_id est de type Pubkey et représente l’ID du programme en cours d’exécution. accounts désigne les comptes sérialisés, tandis que remaining_accounts désigne les comptes restants fournis, mais non désérialisés ni validés — soyez très prudent si vous les utilisez directement. Le champ bumps est de type Bumps , généré par #[derive(Accounts)]. Il représente les bump seeds trouvés lors de la validation des contraintes. Nous aborderons les contraintes de compte dans une section ultérieure. Pour l’instant, retenez qu’il est fourni par commodité afin que les handlers n’aient pas à recalculer les bump seeds ni à les transmettre comme arguments.
Notez que Context est un type générique. En Rust, les génériques permettent aux développeurs d’écrire du code flexible et réutilisable qui fonctionne avec n’importe quel type de données. Ils permettent de définir des types pour les structs, les enums, les fonctions et les méthodes sans préciser le type exact avec lequel ils fonctionneront. Un paramètre substituable, généralement noté T, est utilisé à la place de ces types. Les génériques réduisent le code répétitif et améliorent la clarté. Par exemple, une enum peut être définie pour contenir des types de données génériques :
enum Option<T> {
Some(T),
None,
}L’extrait de code ci-dessus présente l’enum Option<T>. Il s’agit d’une enum Rust standard qui peut encapsuler une valeur de n’importe quel type (c’est-à-dire Some(T)) ou aucune valeur (None).
Dans notre cas, Context est un type générique dans lequel T spécifie les comptes requis pour une instruction (c’est-à-dire tout type qu’un développeur souhaite créer pour stocker des données). Lorsqu’ils utilisent Context, les développeurs peuvent définir T comme une struct implémentant le trait Accounts. Par exemple, Context<SetData>. Les développeurs peuvent accéder aux champs du type Context avec la notation par point. Par exemple, ctx.accounts accède au champ accounts de la struct Context.
Comme indiqué précédemment, la macro #[account] définit des types de comptes personnalisés. Dans les sections suivantes, nous étudierons les types et les contraintes de comptes à l’aide de #[account(...)]. Pour l’instant, il est important de noter que la struct Accounts est l’endroit où un développeur définit les comptes attendus par une instruction et les contraintes que ces comptes doivent respecter.
Types de comptes
Le type Account est utilisé lorsqu’une instruction souhaite accéder aux données désérialisées d’un compte. La struct Account est générique sur T et se définit comme suit :
pub struct Account<'info, T: AccountSerialize + AccountDeserialize + Clone> { /* private fields */ }Il s’agit d’un wrapper pour AccountInfo qui vérifie que le programme est propriétaire du compte et désérialise les données sous-jacentes en un type Rust. Il vérifie la propriété du programme de sorte que Account.info.owner == T::owner(). Autrement dit, il vérifie que le propriétaire des données correspond au ID — créé précédemment avec declare_id! — de la crate dans laquelle #[account] est utilisé. Cela signifie que le type de données encapsulé par Account (=T) doit implémenter le trait Owner. L’attribut #[account] implémente le trait pour une struct à l’aide de crate::ID déclaré par declare_id! dans le même programme. La plupart du temps, les développeurs peuvent simplement utiliser l’attribut #[account] pour ajouter à leurs données les traits et les implémentations nécessaires. L’attribut #[account] génère les implémentations des traits suivants :
Lors de l’implémentation des traits de sérialisation des comptes, les 8 premiers octets sont réservés à un discriminateur de compte unique. Ce discriminateur est déterminé par les 8 premiers octets du hash SHA-256 de l’identifiant Rust du compte. Tout appel à try_deserialize de AccountDeserialize vérifiera ce discriminateur et interrompra la désérialisation du compte avec une erreur si un compte non valide a été fourni.
Dans certains cas, les développeurs devront interagir avec des programmes autres qu’Anchor. Ils peuvent alors bénéficier de tous les avantages de Account en créant leur propre type wrapper personnalisé plutôt qu’en utilisant #[account]. Prenons l’extrait de code suivant comme exemple :
use anchor_lang::prelude::*;
use anchor_spl::token::TokenAccount;
// Rest of the program
#[derive(Accounts)]
pub struct SetData<'info> {
#[account(mut)]
pub my_account: Account<'info, MyAccount>,
#[account(
constraint = my_account.mint == token_account.mint,
has_one = owner
)]
pub token_account: Account<'info, TokenAccount>,
pub owner: Signer<'info>
}La plupart des validations de comptes sont effectuées au moyen de contraintes de compte, que nous aborderons dans la section suivante. Pour l’instant, observez comment le type TokenAccount sert à garantir que le compte reçu appartient au programme de tokens. TokenAccount encapsule la struct Account du programme de tokens et ajoute les fonctions nécessaires. Anchor peut ainsi désérialiser le compte, tandis que les développeurs peuvent utiliser ses champs dans les contraintes de compte et avec la fonction d’instruction.
Notez également dans l’extrait de code ci-dessus que la macro derive encapsule l’ensemble de la struct. Cela implémente un désérialiseur Accounts sur SetData, utilisé pour valider les comptes reçus.
Plusieurs types Account peuvent être utilisés dans la struct de validation des comptes, notamment :
- Account<’info, T> : un conteneur de compte qui vérifie la propriété lors de la désérialisation
- AccountInfo<’info> : un compte non vérifié pouvant être utilisé comme type. Il convient toutefois d’utiliser UncheckedAccount à la place, car AccountInfo disparaîtra probablement dans une future version
- AccountLoader<’info, T> : un type qui facilite la désérialisation zero-copy à la demande. Cette approche diffère de l’utilisation de
Account, car un développeur doit appelerload_initaprès l’initialisation d’un compte,loadlorsque le compte n’est pas mutable etload_mutlorsqu’il est mutable - Box<Account<’info, T>> ou Box<InterfaceAccount<’info, T>> : un type box qui économise de l’espace sur la pile, car les comptes sont parfois trop volumineux pour celle-ci et peuvent provoquer des dépassements de pile — placer le compte dans une box peut résoudre ce problème
- Interface<’info, T> : un type qui encapsule
Programet sert à vérifier que le compte appartient à l’un des programmes indiqués. Il vérifie si le programme attendu contient la clé du compte et si le compte est exécutable - InterfaceAccount<’info, T> : un conteneur de compte qui vérifie la propriété du programme et désérialise les données sous-jacentes en un type Rust
- Option<Account<’info, T>> : un type option pour les comptes facultatifs
- Program<’info, T> : un type qui vérifie si le compte correspond au programme indiqué
- Signer<’info> : un type qui vérifie si le compte a signé la transaction
- SystemAccount<’info> : un type qui vérifie si le compte appartient au System Program
- Sysvar<’info, T> : un type qui vérifie si le compte est une sysvar. Autrement dit, il vérifie si le compte est d’un type spécial contenant des données mises à jour dynamiquement concernant le cluster du réseau, l’historique de la blockchain et la transaction en cours d’exécution. Les sysvars
clock,epoch_schedule,instructionsetrentsont utiles pour le développement de programmes - UncheckedAccount<’info> : un conteneur de compte qui souligne explicitement qu’aucune vérification n’est effectuée sur le compte spécifié
Contraintes de compte
Les contraintes de compte sont essentielles pour développer des programmes Anchor sécurisés. Dans de futurs articles, nous approfondirons la sécurité des programmes Solana et le piratage des programmes Anchor. Il est néanmoins important d’aborder ici les contraintes. Celles-ci permettent aux développeurs de vérifier que certains comptes ou les données qu’ils contiennent correspondent à des exigences prédéfinies. Plusieurs types de contraintes peuvent être appliqués à l’aide de l’attribut #[account(...)], qui peut également faire référence à d’autres structures de données. Le format est le suivant :
#[account(constraint goes here)]
pub account: AccountTypeIl est également important de noter qu’au sein de la macro Accounts, les développeurs peuvent accéder aux arguments des instructions à l’aide de l’attribut #[instruction(...)]. Ils doivent répertorier les arguments de l’instruction dans le même ordre que dans l’instruction, mais peuvent omettre tous ceux qui suivent le dernier argument requis. Voici un exemple tiré de la documentation d’Anchor :
...
pub fn initialize(ctx: Context, bump: u8, authority: Pubkey, data: u64) -> anchor_lang::Result<()> {
...
Ok(())
}
...
#[derive(Accounts)]
#[instruction(bump: u8)]
pub struct Initialize<'info> {
...
}Les contraintes de compte peuvent être divisées en contraintes normales et en contraintes SPL. Nous examinerons des contraintes spécifiques tout au long du reste de cet article. Dans ces exemples, <expr> représente une expression arbitraire qui peut être transmise dès lors qu’elle produit une valeur du type attendu. Par exemple, owner = token_program.key().
Analyse des contraintes d’un programme
Je vous recommande de consulter la documentation d’Anchor sur les comptes pour obtenir une liste plus complète des contraintes possibles. Il serait trop fastidieux de passer en revue chaque contrainte et d’en fournir une définition formelle dans une sorte de tableau. Dans notre cas, il est plus utile d’analyser le programme suivant pour comprendre comment les contraintes de compte fonctionnent en pratique :
use anchor_lang::prelude::*;
#[cfg(not(feature = "no-entrypoint"))]
use {default_env::default_env, solana_security_txt::security_txt};
declare_id!("fanqeMu3fw8R4LwKNbahPtYXJsyLL6NXyfe2BqzhfB6");
pub mod errors;
pub mod instructions;
pub mod state;
pub use instructions::*;
pub use state::*;
#[cfg(not(feature = "no-entrypoint"))]
security_txt! {
name: "Fanout",
project_url: "http://helium.com",
contacts: "email:hello@helium.foundation",
policy: "https://github.com/helium/helium-program-library/tree/master/SECURITY.md",
// Optional Fields
preferred_languages: "en",
source_code: "https://github.com/helium/helium-program-library/tree/master/programs/fanout",
source_revision: default_env!("GITHUB_SHA", ""),
source_release: default_env!("GITHUB_REF_NAME", ""),
auditors: "Sec3"
}
#[program]
pub mod fanout {
use super::*;
pub fn initialize_fanout_v0(
ctx: Context<InitializeFanoutV0>,
args: InitializeFanoutArgsV0,
) -> Result<()> {
instructions::initialize_fanout_v0::handler(ctx, args)
}
pub fn stake_v0(ctx: Context<StakeV0>, args: StakeArgsV0) -> Result<()> {
instructions::stake_v0::handler(ctx, args)
}
pub fn unstake_v0(ctx: Context<UnstakeV0>) -> Result<()> {
instructions::unstake_v0::handler(ctx)
}
pub fn distribute_v0(ctx: Context<DistributeV0>) -> Result<()> {
instructions::distribute_v0::handler(ctx)
}
}Il s’agit du programme Fanout de Helium. Ce programme relativement complexe distribue des tokens à leurs détenteurs proportionnellement à leurs avoirs. Pour le moment, le projet ne nous semble pas très utile puisqu’il ne comporte aucune contrainte. Cependant, si nous analysons la struct StakeV0 de l’instruction stake_v0, nous découvrons une multitude de contraintes à étudier.
mut
La première contrainte de cette instruction est la contrainte de compte mut. mut est définie comme #[account(mut)] ou #[account(mut @ <custom_error>)] et prend en charge les erreurs personnalisées avec la notation @. Cette contrainte vérifie si un compte donné est mutable et demande à Anchor de conserver toute modification de son état. Dans le programme de Helium, la contrainte garantit que le compte payer est mutable :
...
pub struct StakeV0<'info> {
#[account(mut)]
pub payer: Signer<'info>,
pub staker: Signer<'info>,
/// CHECK: Just needed to receive nft
pub recipient: AccountInfo<'info>,
...has_one
La contrainte has_one est définie comme #[account(has_one = <target_account)] ou #[account(has_one = <target_account> @ <custom_error>)]. Elle vérifie le champ target_account afin de déterminer si le compte correspond à la clé du champ target_account dans la struct Accounts. Les erreurs personnalisées sont prises en charge par l’annotation @.
Dans le contexte de la struct StakeV0, la contrainte has_one sert à vérifier si le compte possède un membership_mint, un token_account et un membership_collection :
...
#[account(
mut,
has_one = membership_mint,
has_one = token_account,
has_one = membership_collection
)]
pub fanout: Box<Account<'info, FanoutV0>>,
pub membership_mint: Box<Account<'info, Mint>>,
pub token_account: Box<Account<'info, TokenAccount>>,
pub membership_collection: Box<Account<'info, Mint>>,
...Notez que plusieurs contraintes has_one sont présentes et que la contrainte mut est également utilisée. Il est possible d’appliquer simultanément plusieurs contraintes de compte à un même compte.
seeds, bump
Les contraintes seeds et bump servent à vérifier qu’un compte donné est un PDA dérivé du programme en cours d’exécution, des seeds et, s’il est fourni, du bump :
#[account(seeds = <seeds>, bump)]#[account(seeds = <seeds>, bump, seeds::program = <expr>)]#[account(seeds = <seeds>, bump = <expr>)]#[account(seeds = <seeds>, bump = <expr>, seeds::program = <expr>)]
Si le bump n’est pas fourni, Anchor utilise le bump canonique. Seeds::program = <expr> permet de dériver le PDA d’un autre programme que celui en cours d’exécution.
Dans le programme Fanout de Helium, la contrainte seeds vérifie si le texte « metadata », la clé token_metadata_program, la clé membership_collection et le texte « edition » sont les seeds utilisées pour dériver ce PDA. La contrainte seeds::program garantit que token_metadata_program est utilisé pour dériver le PDA à la place du programme actuel :
...
#[account(
mut,
seeds = ["metadata".as_bytes(), token_metadata_program.key().as_ref(), membership_collection.key().as_ref()],
seeds::program = token_metadata_program.key(),
bump,
)]
pub collection_metadata: UncheckedAccount<'info>,
...token::mint, token::authority
Les contraintes token::mint et token::authority sont définies comme suit :
#[account(token::mint = <target account>, token::authority = <target account>)]#[account(token::mint = <target account>, token::authority = <target account>, token::token_program = <target account>)]
Les contraintes de token mint et authority servent à vérifier l’adresse de mint et l’autorité d’un TokenAccount. Ces contraintes peuvent servir de vérification ou être utilisées avec la contrainte init pour créer un compte de tokens avec l’adresse de mint et l’autorité indiquées. Lorsqu’elles servent de vérification, il est possible de n’en spécifier qu’un sous-ensemble.
Dans le contexte du programme de Helium, ces contraintes servent à vérifier si le mint de associated_token est égal à membership_mint et si l’autorité du token est définie sur staker :
...
#[account(
mut,
associated_token::mint = membership_mint,
associated_token::authority = staker,
)]
pub from_account: Box<Account<'info, TokenAccount>>,
...init, payer, space
À ce stade, il est pertinent d’avancer un peu dans le code pour analyser les contraintes init, payer et space. La contrainte init est définie comme [#account(init, payer = <target_account>, space = <num_bytes>)]. Cette contrainte crée le compte au moyen d’un CPI vers le System Program, puis l’initialise en définissant son discriminateur de compte. Le compte est alors marqué comme mutable. Cette contrainte est mutuellement exclusive avec mut. Pour les comptes de plus de 10 kibioctets, utilisez #[account(zero)].
La contrainte init doit être utilisée avec plusieurs contraintes supplémentaires. Elle exige la contrainte payer, qui indique le compte chargé de payer la création du compte. Elle exige également que le System Program figure dans la struct sous le nom system_program. La contrainte space doit aussi être définie. Dans la section consacrée à l’espace des comptes, nous étudierons cette contrainte et les besoins en espace plus en détail.
Dans le programme Fanout de Helium, la commande init crée un nouveau compte. payer est défini comme payer, précédemment établi dans la struct comme pub payer: Signer<'info>. L’espace du compte est défini sur la taille de FanoutVoucherV0, avec 8 octets supplémentaires pour le discriminateur et 61 octets d’espace additionnels :
...
#[account(
init,
payer = payer,
space = 60 + 8 + std::mem::size_of::<FanoutVoucherV0>() + 1,
seeds = ["fanout_voucher".as_bytes(), mint.key().as_ref()],
bump,
)]
pub voucher: Box<Account<'info, FanoutVoucherV0>>,
...init_if_needed
La contrainte init_if_needed est définie comme #[account(init_if_nedded, payer = <target_Account>)] ou #[account(init)if_needed, payer = <target_account>, space = <num_bytes>)]. Elle possède exactement les mêmes fonctionnalités que init. Toutefois, elle ne s’exécute que si le compte n’existe pas encore. Si le compte existe, init_if_needed vérifie tout de même que toutes les contraintes d’initialisation sont respectées, par exemple que la quantité d’espace allouée au compte est correcte ou, dans le cas d’un PDA, que les seeds sont correctes.
init_if_needed doit être utilisé avec prudence, car cette fonctionnalité est placée derrière un feature flag en raison des risques potentiels. Pour l’activer, importez anchor-lang avec la fonctionnalité cargo init-if-needed. Lors de l’utilisation de init_if_needed, il est crucial de se protéger contre les attaques par réinitialisation. Les développeurs doivent s’assurer que leur code comprend des vérifications empêchant le compte de retrouver son état initial après son initialisation, sauf si ce comportement est intentionnel. Pour limiter ces attaques, il est recommandéé de conserver des chemins d’exécution d’instructions simples. Envisagez de séparer les instructions entre une instruction d’initialisation et toutes les autres consacrées aux opérations ultérieures.
Le programme Fanout de Helium utilise la contrainte init_if_needed pour initialiser recipient_account si le compte n’existe pas encore :
...
#[account(
init_if_needed,
payer = payer,
associated_token::mint = mint,
associated_token::authority = recipient,
)]
pub receipt_account: Box<Account<'info, TokenAccount>>,
...constraint
La contrainte constraint est définie comme #[account(constraint = <expr>)] ou #[account(constraint = <expr> @ <custom_error>)]. Elle vérifie si l’expression fournie produit la valeur true. Elle est utile lorsqu’aucune autre contrainte ne correspond au cas d’usage souhaité. Elle prend également en charge les erreurs personnalisées au moyen de l’annotation @.
Le programme Fanout utilise constraint pour vérifier si la supply du mint est définie sur zéro :
...
#[account(
mut,
constraint = mint.supply == 0,
mint::decimals = 0,
mint::authority = voucher,
mint::freeze_authority = voucher,
)]
pub mint: Box<Account<'info, Mint>>,
...mint::authority, mint::decimals, mint::freeze_authority
Dans l’extrait de code ci-dessus, les contraintes mint::decimals, mint::authority et mint::freeze_authority servent à vérifier si le nombre de décimales du mint est défini sur zéro et si voucher possède l’autorité et l’autorité de gel.
À titre de contexte, les contraintes mint::authority, mint::decimals et mint::freeze_authority sont définies comme suit :
#[account(mint::authority = <target account>, mint::decimals = <expr>)]#[account(mint::authority = <target account>, mint::decimals = <expr>, mint::freeze_authority = <target account>)]
Ces contraintes sont explicites : elles vérifient respectivement l’autorité du token, son nombre de décimales et son autorité de gel. Elles peuvent servir de vérification ou être utilisées avec init pour créer un compte de mint avec le nombre de décimales et l’autorité de mint indiqués. L’autorité de gel est entièrement facultative lorsqu’elle est utilisée avec init. Lorsqu’elles servent de vérification, il est possible de n’en spécifier qu’un sous-ensemble.
Espace des comptes
L’espace de stockage de chaque compte utilisé par un programme sur Solana doit être explicitement alloué. Cette allocation est essentielle à une gestion efficace des ressources, car elle garantit que seules les données nécessaires sont stockées on-chain. Elle rend également les coûts de transaction prévisibles et améliore l’efficacité de leur exécution : les transactions peuvent être traitées sans avoir à allouer ou redimensionner dynamiquement le stockage des comptes. En outre, la préallocation des données garantit que le compte dispose d’un espace suffisant pour stocker toutes les données requises, ce qui réduit le risque d’échec des transactions ou de failles de sécurité potentielles.
Dimensionnement des variables
Les besoins en espace varient selon les types de données. Voici un guide simplifié pour vous aider à les estimer :
- Types de base : les types de données simples tels que bool, u8, i8, u16, i16, u32, i32, u64, i64, u128 et i128 ont tous une taille fixe. Celle-ci va de 1 octet pour un
bool(bien qu’il n’utilise qu’un bit) à 16 octets pouru128/i128 - Tableaux : pour un tableau
[T;amount], l’espace est calculé en multipliant la taille deTpar le nombre d’éléments (c’est-à-direamount). Par exemple, un tableau de 16u16nécessiterait 32 octets - Pubkey : une clé publique occupe toujours 32 octets sur Solana
- Types dynamiques :
StringetVec<T>nécessitent une attention particulière. Ils ont tous deux besoin de 4 octets pour stocker leur longueur, en plus de l’espace réservé au contenu lui-même. Il est essentiel d’allouer suffisamment d’espace pour la taille maximale attendue. Pour unString, cela correspond à 4 octets plus la longueur en octets duString. Pour unVec<T>, cela correspond à 4 octets plus l’espace du type donné multiplié par le nombre d’éléments attendus (c’est-à-dire 4 + space(T) * amount) - Options et énumérations : un type
Option<T>nécessite 1 octet plus l’espace du typeT. Les énumérations nécessitent 1 octet pour le discriminateur de l’énumération, plus l’espace requis par la plus grande variante - Nombres à virgule flottante : les types tels que
f32etf64occupent respectivement 4 et 8 octets. Soyez prudent avec les valeurs NaN, car elles peuvent faire échouer la sérialisation
Le guide suivant ne s’applique qu’aux comptes qui n’utilisent pas la sérialisation zero-copy. La sérialisation zero-copy est indiquée par l’attribut #[zero_copy]. Elle exploite l’attribut repr(c) pour l’organisation de la mémoire, ce qui permet d’accéder aux données par conversion directe de pointeur. C’est un moyen efficace de travailler avec des données on-chain sans la surcharge de la désérialisation traditionnelle. #[zero_copy] est un raccourci qui applique #[derive(Copy, Clone)], #[derive(bytemuck::Zeroable)], #[derive(bytemuck::Pod)] et #[repr(C)]. Ces attributs garantissent que le compte peut être traité sans risque comme une séquence d’octets et qu’il est compatible avec la désérialisation zero-copy. La désérialisation zero-copy est essentielle pour les comptes qui nécessitent des tailles particulièrement importantes, c’est-à-dire les comptes qui ne peuvent pas être sérialisés efficacement à l’aide de Borsh ou des mécanismes de sérialisation par défaut d’Anchor sans atteindre les limites du tas ou de la pile.
Discriminateur interne d’Anchor
Les développeurs doivent ajouter 8 à la contrainte space pour le discriminateur interne d’Anchor. Par exemple, si un compte nécessite 32 octets, il en faudra 40. Il est recommandé de définir la contrainte d’espace sous la forme space = 8 + <account size> afin d’indiquer clairement que le discriminateur interne est pris en compte dans le calcul de l’espace.
À titre d’information, un discriminateur est un identifiant unique utilisé pour distinguer différents types de données. Il permet de différencier les divers types de structures de données de compte lors de l’exécution. Il sert également de préfixe aux instructions, facilitant leur routage vers les méthodes correspondantes dans un programme Anchor. Le discriminateur est un tableau de 8 octets représentant l’identifiant unique du type de données.
Calcul de l’espace initial
Le calcul de l’espace initial requis pour un compte peut être complexe. La macro InitSpace ajoute une constante INIT_SPACE qui peut être utilisée sur la structure du compte. Il n’est pas nécessaire que la structure contienne la macro #[account] pour générer cette constante. La documentation d’Anchor fournit l’exemple suivant :
#[account]
#[derive(InitSpace)]
pub struct ExampleAccount {
pub data: u64,
// max_len represents the length of the structure
#[max_len(50)]
pub string_one: String,
#[max_len(10, 5)]
pub nested: Vec<Vec<u8>>,
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
#[account(init, payer = payer, space = 8 + ExampleAccount::INIT_SPACE)]
pub data: Account<'info, ExampleAccount>,
}Dans cet exemple, ExampleAccount::INIT_SPACE calcule automatiquement l’espace nécessaire pour ExampleAccount. Il prend également en compte le discriminateur interne d’Anchor dans le calcul de l’espace.
Redimensionnement de l’espace du programme
La contrainte realloc sert à ajuster l’espace d’un compte de programme au début d’une instruction. Le compte doit être mutable (c’est-à-dire mut) et cette contrainte s’applique aux types Account ou AccountLoader. Elle est définie comme #[account(realloc = <space>, realloc::payer = <target>, realloc::zero = <bool>)]. Lorsque la longueur des données du compte augmente, des lamports sont transférés de realloc::payer vers le compte du programme afin de maintenir l’exemption de loyer. Si la longueur des données diminue, les lamports sont retransférés du compte du programme vers realloc::payer. La contrainte realloc::zero détermine si la mémoire nouvellement allouée doit être initialisée à zéro. Cette initialisation garantit que la nouvelle mémoire est propre et exempte de toute donnée résiduelle ou indésirable.
Il n’est pas recommandé d’utiliser AccountInfo::realloc manuellement plutôt que la contrainte realloc. En effet, aucun contrôle à l’exécution ne garantit que la réallocation ne dépasse pas la limite MAX_PERMITTED_DATA_INCREASE, ce qui pourrait entraîner l’écrasement des données d’autres comptes. La contrainte vérifie et empêche également les réallocations répétées au sein d’une même instruction.
Par exemple :
#[derive(Accounts)]
pub struct Data {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
mut,
seeds = [b"data"],
bump,
realloc = 8 + std::mem::size_of::<()>() + 48,
realloc::payer = payer,
realloc::zero = false
)]
pub update_account: Account<'info, NewData>,
system_program: Program<'info, System>,
}Erreurs
La gestion des erreurs est un aspect essentiel du développement de programmes. Il s’agit d’un mécanisme permettant d’identifier et de gérer les erreurs susceptibles d’interrompre l’exécution d’un programme. Elle doit être réfléchie et planifiée pour garantir la qualité, la maintenance et le bon fonctionnement du code. Anchor simplifie ce processus grâce à de solides mécanismes de gestion des erreurs. Les erreurs des programmes Anchor peuvent être divisées en AnchorErrors et en erreurs non-Anchor. Cette section se concentrera sur AnchorErrors, car les erreurs non-Anchor couvrent un large éventail d’erreurs Rust. Pour ces dernières, je vous conseille de consulter le chapitre du Rust Book sur la gestion des erreurs et la section de Rust By Example consacrée à la gestion des erreurs.
Le struct suivant définit AnchorError :
pub struct AnchorError {
pub error_name: String,
pub error_code_number: u32,
pub error_msg: String,
pub error_origin: Option<ErrorOrigin>,
pub compared_values: Option<ComparedValues>,
}Ces champs sont relativement simples. error_name est une chaîne qui représente le nom de l’erreur. error_code_number est un identifiant unique de l’erreur (c’est-à-dire un entier non signé unique occupant 32 bits). error_msg est un message descriptif qui explique l’erreur. error_origin est un champ facultatif qui fournit des informations sur l’origine de l’erreur, par exemple le fichier source ou le compte concerné. compared_values est un champ facultatif qui détaille les valeurs comparées lorsque l’erreur s’est produite. Il est extrêmement utile pour le débogage.
AnchorError implémente une méthode de journalisation. Celle-ci inclut des informations sur l’origine de l’erreur et les valeurs concernées, ce qui facilite le débogage et la résolution des erreurs. Cette méthode utilise error_origin et compared_values pour fournir ces informations.
Les AnchorErrors peuvent être subdivisées en erreurs internes d’Anchor et en erreurs personnalisées. Anchor dispose d’une longue liste de codes d’erreur internes susceptibles d’être renvoyés. Ces erreurs internes ne sont pas destinées à être utilisées par les utilisateurs. Il est toutefois utile de connaître la correspondance entre les codes et leurs causes. Elles sont généralement déclenchées lorsqu’une contrainte a été enfreinte. Les codes d’erreur internes suivent ce schéma :
- >= 100 correspondent aux codes d’erreur d’instruction
- >= 1000 correspondent aux codes d’erreur d’IDL
- >= 2000 correspondent aux codes d’erreur de contrainte
- >= 3000 correspondent aux codes d’erreur de compte
- >= 4100 correspondent aux codes d’erreur divers
- = 5000 correspondent aux codes d’erreur obsolètes.
Les erreurs personnalisées commencent à ERROR_CODE_OFFSET (c’est-à-dire 6000).
Les développeurs peuvent implémenter leurs propres erreurs personnalisées à l’aide de l’attribut error_code. Cet attribut s’applique à une énumération, dont les variantes peuvent être utilisées comme erreurs dans tout le programme. Un message peut être ajouté à chaque variante. Le client peut afficher ce message si l’erreur se produit. Par exemple :
#[error_code]
pub enum HeliusError {
#[msg(“This RPC provider is too good”)]
RPCTooGood
}Les macros err! et error! peuvent servir à déclencher ces erreurs. Par exemple :
require!(rpc.speed > 9000, HeliusError::RPCTooGood);Il est essentiel de noter qu’il existe plusieurs macros require parmi lesquelles choisir. La grande majorité de ces macros concernent des valeurs qui ne sont pas des clés publiques. Par exemple, la macro require_gte vérifie si la première valeur qui n’est pas une clé publique est supérieure ou égale à la seconde :
pub fn set_data(ctx: Context<SetData>, data: u64) -> Result<()> {
require_gte!(ctx.accounts.data.data, 1);
ctx.accounts.data.data = data;
Ok(());
}La comparaison de clés publiques comporte également quelques subtilités. Par exemple, les développeurs doivent utiliser require_keys_eq plutôt que require_eq, car cette dernière est plus coûteuse.
Tous les programmes renvoient une ProgramError. Ce type d’erreur comprend un champ spécifiquement réservé à un numéro d’erreur personnalisé, qu’Anchor utilise pour stocker ses codes d’erreur internes et personnalisés. Toutefois, il ne s’agit que d’un seul numéro, ce qui limite son utilité. La journalisation des AnchorErrors par Anchor, mentionnée précédemment, est bien plus utile. Les clients Anchor sont conçus pour analyser ces journaux. Cela peut toutefois s’avérer difficile dans certains cas. Par exemple, la récupération des journaux des transactions traitées lorsque les vérifications préalables sont désactivées n’est pas aussi simple. De même, Anchor utilise un mécanisme de secours pour les programmes non-Anchor ou anciens qui ne journalisent pas les AnchorErrors de manière standard. Anchor vérifie alors si le numéro d’erreur renvoyé par la transaction correspond à un code d’erreur interne d’Anchor ou à un numéro d’erreur défini dans l’IDL du programme. Lorsqu’une correspondance est trouvée, Anchor enrichit les informations sur l’erreur pour fournir davantage de contexte. Anchor essaie aussi, lorsque cela est possible, d’analyser la pile d’erreurs du programme afin de remonter à la cause initiale. ProgramError sert de type d’erreur fondamental, dont l’utilité est renforcée par les mécanismes de journalisation et d’analyse d’Anchor afin de fournir des informations détaillées sur les erreurs.
Appels interprogrammes (CPI)
Les appels interprogrammes (CPI) ont été évoqués tout au long de cet article. Il est donc naturel de leur consacrer une section. Les CPI sont essentiels à la composabilité de Solana, car ils permettent aux programmes d’appeler directement d’autres programmes. Pour les développeurs, l’écosystème Solana devient ainsi, en quelque sorte, une vaste API interconnectée. Par souci de concision, je vous recommande de consulter la documentation d’Anchor sur les CPI, qui fournit un exemple utile de CPI en action avec un programme de marionnette et de marionnettiste.
Un CPI peut néanmoins être défini comme l’appel d’un programme par un autre, ciblant une instruction précise du programme appelé. Le programme appelant est suspendu jusqu’à ce que le programme appelé ait terminé de traiter l’instruction.
Élévation des privilèges
Les CPI permettent à un programme appelant d’étendre ses privilèges de signataire au programme appelé. Cette extension est pratique, mais potentiellement très dangereuse. Si un CPI cible accidentellement un programme malveillant, ce dernier obtient les mêmes privilèges que l’appelant. Anchor atténue ce risque grâce à deux mesures de protection :
- Le type
Program<’info, T>garantit que le compte spécifié correspond au programme attendu (T) - Même si le type
Programn’est pas utilisé, la fonction CPI générée automatiquement vérifie que l’argumentcpi_programcorrespond au programme attendu
Exécution d’un CPI
Un programme peut exécuter un CPI à l’aide de invoke ou invoke_signed, issus de la crate solana_program. Anchor fournit également la structure CpiContext pour spécifier les entrées hors arguments des CPI.
invoke
La fonction invoke est utilisée lorsqu’un PDA n’est pas requis comme signature. Dans ce cas, l’environnement d’exécution étend la signature d’origine du programme appelant au programme appelé. La fonction est définie comme suit :
pub fn invoke(
instruction: &Instruction,
account_infos: &[AccountInfo<'_>]
) -> ProgramResultL’appel d’un autre programme implique la création d’une Instruction qui comprend l’ID du programme, les données d’instruction destinées au programme appelé et la liste des comptes auxquels ce dernier accédera. Un programme reçoit uniquement des valeurs AccountInfo de l’environnement d’exécution à son point d’entrée. Tout compte dont le programme appelé a besoin pour son invocation doit être inclus et fourni par le programme qui l’appelle. Par exemple, si le programme appelé doit modifier un compte précis, le programme appelant doit inclure ce compte dans la liste des valeurs AccountInfo. Cela vaut également pour l’ID du programme appelé : l’appelant doit indiquer explicitement quel programme il appelle en incluant son ID.
La Instruction est généralement construite dans le programme appelant, bien qu’elle puisse être désérialisée à partir d’une sortie externe.
L’intégralité de la transaction échoue immédiatement si le programme appelé rencontre une erreur ou s’interrompt. En effet, la fonction invoke ne renvoie qu’en cas de réussite. Utilisez les fonctions set_return_data ou get_return_data pour renvoyer des données comme résultat d’un CPI. Notez que le type renvoyé doit implémenter les traits AnchorSerialize et AnchorDeserialize. Vous pouvez également demander au programme appelé d’écrire les données dans un compte dédié
Bien qu’un programme puisse s’appeler lui-même de manière récursive, les appels récursifs indirects par un autre programme (c’est-à-dire la réentrance) entraînent immédiatement l’échec de la transaction.
Par exemple, si nous avions un programme qui transfère des tokens via un CPI, nous utiliserions invoke comme suit :
pub fn set_data(ctx: Context<SetData>, data: u64) -> Result<()> {
require_gte!(ctx.accounts.data.data, 1);
ctx.accounts.data.data = data;
Ok(());
}invoke_signed
invoke_signed est utilisé pour les CPI qui nécessitent un PDA comme signataire. Il permet à un programme appelant d’agir au nom d’un PDA en fournissant les seeds nécessaires à sa dérivation :
pub fn invoke_signed(
instruction: &Instruction,
account_infos: &[AccountInfo<'_>],
signers_seeds: &[&[&[u8]]]
) -> ProgramResultLes PDA peuvent également agir en tant que signataires dans un CPI. L’environnement d’exécution utilise les seeds fournies et le program_id du programme appelant pour générer le PDA en interne via create_program_address. Le PDA est ensuite validé par rapport aux adresses transmises avec l’instruction (c’est-à-dire account_infos) afin de confirmer qu’il s’agit d’un signataire valide.
Avec cette fonction, une invocation peut signer au nom d’un ou plusieurs PDA contrôlés par le programme appelant. Le programme appelé peut ainsi interagir avec les comptes concernés comme s’ils avaient été signés cryptographiquement. signer_seeds est composé de tranches de seeds utilisées pour dériver le PDA. Lors de l’invocation, l’environnement d’exécution considère comme « signé » tout compte correspondant dans account_info. Par exemple, si nous avions un programme qui crée un compte pour un PDA, nous appellerions invoke_signed comme suit :
invoke_signed(
&system_instruction::create_account(
&payer.key,
&vault_pda.key,
lamports,
vault_size,
&program_id,
),
&[
payer.clone(),
vault_pda.clone(),
],
&[
&[
b"vault",
payer.key.as_ref(),
&[vault_bump_seed],
],
]
)?;CpiContext
Anchor fournit CpiContext comme moyen simplifié d’effectuer des CPI, au lieu d’utiliser invoke ou invoke_signed. Cette structure spécifie les entrées hors arguments nécessaires aux CPI, en reproduisant étroitement les fonctionnalités de Context. Elle fournit des informations sur les comptes requis pour l’instruction, les éventuels comptes supplémentaires concernés, l’ID du programme invoqué et les seeds permettant de dériver les PDA si nécessaire. Utilisez CpiContext::new pour les CPI sans PDA et CpiContext::new_with_signer pour les CPI qui nécessitent des PDA signataires.
CpiContext est défini comme suit, T étant un type générique qui englobe tout objet implémentant les traits ToAccountMetas et ToAccountInfos<’info> :
pub struct CpiContext<'a, 'b, 'c, 'info, T>where
T: ToAccountMetas + ToAccountInfos<'info>,{
pub accounts: T,
pub remaining_accounts: Vec>,
pub program: AccountInfo<'info>,
pub signer_seeds: &'a [&'b [&'c [u8]]],
}Accounts est un type générique qui accepte tout objet implémentant les traits ToAccountMetas et ToAccountInfos<’info>. Cela est rendu possible par la macro d’attribut #[derive(Accounts)] afin de faciliter l’organisation du code et de renforcer la sûreté des types.
CpiContext simplifie l’invocation des programmes Anchor et non-Anchor. Pour les programmes Anchor, il suffit de déclarer une dépendance dans le fichier Cargo.toml du projet et d’utiliser le module cpi généré par Anchor :
[dependencies]
callee = { path = "../callee", features = ["cpi"]}Définir features = [“cpi”] permet au programme d’accéder au module callee::cpi. Anchor génère automatiquement ce module et expose les instructions du programme sous la forme d’une fonction Rust. Cette fonction accepte un CpiContext et toute donnée d’instruction supplémentaire, reprenant le format des fonctions d’instruction habituelles des programmes Anchor, mais en remplaçant Context par CpiContext. Le module cpi fournit également les structures de compte nécessaires pour appeler les instructions.
Par exemple, si le programme appelé comporte une instruction nommée hello_there qui nécessite des comptes précis définis dans la structure GeneralKenobi, invoquez-la comme suit :
// We assume "jedi" is an Anchor program with a published crate
use jedi::cpi::accounts::GeneralKenobi;
use jedi::cpi::hello_there;
use anchor_lang::prelude::*;
#[program]
pub mod fight_on_utapau {
use super::*;
pub fn call_hello_there(ctx: Context<CallGeneralKenobi>, data: GreetingParams) -> Result<()> {
let cpi_accounts = GeneralKenobi {
jedi: ctx.accounts.jedi.to_account_info(),
// Other account infos needed for the GeneralKenobi struct go here
};
let cpi_program = ctx.accounts.jedi_program.to_account_info();
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
hello_there(cpi_ctx, data);
}
#[derive(Accounts)]
pub struct CallGeneralKenobi<'info> {
pub jedi: UncheckedAccount<'info>,
pub jedi_program: Program<'info, Jedi>,
// Other required accounts
}
pub struct GreetingParams {
// Params required for the hello_there function
}Dans le module fight_on_utapau, un CPI est exécuté à l’aide de CpiContext. La fonction call_hello_there est conçue pour interagir avec le programme jedi. Elle crée un CpiContext avec les informations de compte requises pour la structure de compte GeneralKenobi du programme jedi, ainsi que les informations de compte du programme jedi. Ce contexte invoque hello_there en transmettant tous les paramètres supplémentaires requis définis par la structure GreetingParams. La structure CallGeneralKenobi définit les comptes nécessaires à cette fonction, ce qui simplifie le processus.
Enfin, lorsque vous invoquez des instructions de programmes non-Anchor, vérifiez si les mainteneurs du programme ont publié leur propre crate avec des fonctions utilitaires permettant d’appeler leur programme. S’il n’existe aucune fonction utilitaire pour le programme dont les instructions doivent être invoquées, utilisez invoke et invoke_signer comme solution de secours pour organiser et préparer les CPI.
Adresses dérivées de programme (PDA)
Rappel : les PDA sont hors courbe et ne possèdent pas de clé privée associée. Ils permettent aux programmes de signer des instructions et aux développeurs de créer on-chain des structures semblables à des tables de hachage. Un PDA est dérivé à partir d’une liste de seeds facultatives, d’une bump seed et d’un ID de programme.
Pour rappel, les contraintes suivantes permettent de vérifier qu’un compte donné est un PDA dérivé du programme en cours d’exécution, des seeds et, si elle est fournie, de la bump :
#[account(seeds = <seeds>, bump)]#[account(seeds = <seeds>, bump, seeds::program = <expr>)]#[account(seeds = <seeds>, bump = <expr>)]#[account(seeds = <seeds>, bump = <expr>, seeds::program = <expr>)]
Si la bump n’est pas fournie, Anchor utilise la bump canonique. Seeds::program = <expr> permet de dériver le PDA à partir d’un programme différent de celui en cours d’exécution.
L’utilisation des contraintes seeds et bump simplifie le processus de dérivation :
#[derive(Accounts)]
struct ExamplePDA<'info> {
#[account(seeds = [b"example"], bump)]
pub example_pda: Account<'info, AccountType>,
}Ici, la contrainte seeds est utilisée pour dériver le PDA. Anchor vérifie automatiquement que le compte transmis à l’instruction correspond au PDA dérivé des seeds. Anchor utilise par défaut la bump canonique lorsque la contrainte bump est employée sans valeur précise.
Anchor autorise également des seeds dynamiques basées sur d’autres champs de compte ou sur les données d’instruction. Pour cela, référencez d’autres champs de la structure ou utilisez la macro d’attribut #[instruction(...)] afin d’inclure les données d’instruction désérialisées. Par exemple, dans la structure suivante, example_pda est contraint d’utiliser une combinaison composée d’une seed statique, des données d’instruction et de la clé publique du signataire :
#[derive(Accounts)]
#[instruction(instruction_data: String)]
pub struct ExamplePDA<'info> {
#[account(seeds = [b"example", signor.key().as_ref(), instruction_data.as_bytes()], bump)]
pub example_pda: Account<'info, AccountType>,
#[account(mut)]
pub signoooorrr: Signer<'info>
}Conclusion
Dire qu’Anchor est un framework puissant serait un euphémisme. Sa capacité à simplifier le processus de développement ressort clairement de notre exploration des différentes macros et traits qu’Anchor utilise pour réduire la quantité de code. Il s’appuie sur une documentation bien tenue et sur un solide écosystème de tutoriels et de crates associés. Anchor est apprécié et utilisé par la grande majorité des développeurs Solana.
Cet article est un guide très, très complet sur le développement de programmes avec Anchor. Il explique comment installer Anchor, utiliser Solana Playground, ainsi que créer, compiler et déployer un programme Hello, World!. Nous avons ensuite exploré les méthodes d’abstraction efficaces d’Anchor, la structure d’un programme Anchor classique et les nombreux types de comptes et contraintes disponibles. Il aborde également l’importance de l’allocation de l’espace des comptes et de la gestion des erreurs. Enfin, nous avons étudié les CPI et les PDA. C’est l’article de référence sur Anchor : il contient tout ce dont vous avez besoin pour commencer dès aujourd’hui à développer des programmes sur Solana.
Si vous avez lu jusqu’ici, merci anon ! N’oubliez pas de saisir votre adresse e-mail ci-dessous pour ne manquer aucune actualité sur Solana. Prêt à aller plus loin ? Rejoignez-nous sur Discord pour commencer à développer des programmes Anchor.
Ressources supplémentaires
Articles associés
Abonnez-vous à Helius
Suivez les dernières actualités du développement sur Solana et recevez une notification à chaque publication


