Comptes
Nous avons vu la macro #[account], mais naturellement sur Solana il existe différents types de comptes. Pour cette raison, il est utile de prendre un moment pour voir comment fonctionnent généralement les comptes sur Solana, mais plus en profondeur, comment ils fonctionnent avec Anchor.
Aperçu général
Sur Solana, chaque élément d'état réside dans un compte ; imaginez le registre comme un immense tableau où chaque ligne partage le même schéma de base :
pub struct Account {
/// lamports in the account
pub lamports: u64,
/// data held in this account
#[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
pub data: Vec<u8>,
/// the program that owns this account and can mutate its lamports or data.
pub owner: Pubkey,
/// `true` if the account is a program; `false` if it merely belongs to one
pub executable: bool,
/// the epoch at which this account will next owe rent (currently deprecated and is set to `0`)
pub rent_epoch: Epoch,
}Tous les comptes sur Solana partagent la même structure de base. Ce qui les distingue est :
Le propriétaire : le programme qui a les droits exclusifs de modifier les données et les lamports du compte.
Les données : utilisées par le programme propriétaire pour distinguer entre différents types de comptes.
Quand nous parlons de comptes du programme Token, nous faisons référence à un compte où le owner est le programme Token. Contrairement à un compte système dont le champ de données est vide, un compte du programme Token peut être soit un compte Mint soit un compte Token. Nous utilisons des discriminateurs pour les distinguer.
Tout comme le programme Token peut posséder des comptes, n'importe quel autre programme peut en faire autant, même le nôtre.
Comptes de programme
Les comptes de programme sont la base de la gestion d'état dans les programmes Anchor. Ils vous permettent de créer des structures de données personnalisées qui appartiennent à votre programme. Explorons comment travailler efficacement avec eux.
Structure de compte et discriminateurs
Chaque compte de programme dans Anchor a besoin d'un moyen d'identifier son type. Ceci est géré par des discriminateurs, qui peuvent être soit :
Discriminateurs par défaut : Un préfixe de 8 octets généré en utilisant
sha256("account:<StructName>")[0..8]pour les comptes, ousha256("global:<instruction_name>")[0..8]pour les instructions. Les seeds utilisent PascalCase pour les comptes et snake_case pour les instructions.
Discriminateurs personnalisés : À partir d'Anchor
v0.31.0, vous pouvez spécifier votre propre discriminateur :
#[account(discriminator = 1)] // single-byte
pub struct Escrow { … }Remarques importantes concernant les discriminateurs :
Ils doivent être uniques dans votre programme
L'utilisation de
[1]empêche d'utiliser[1, 2, …]car ils commencent également par1[0]ne peut pas être utilisé car il entre en conflit avec les comptes non initialisés
Création de comptes de programme
Pour créer un compte de programme, vous définissez d'abord votre structure de données :
use anchor_lang::prelude::*;
#[derive(InitSpace)]
#[account(discriminator = 1)]
pub struct CustomAccountType {
data: u64,
}Points clés concernant les comptes de programme :
La taille maximale est de 10 240 octets (10 Kio)
Pour les comptes plus volumineux, vous aurez besoin de
zero_copyet d'écritures fragmentéesLa macro
InitSpacecalcule automatiquement l'espace requisEspace total =
INIT_SPACE+DISCRIMINATOR.len()
L'espace total en octets nécessaire pour le compte est la somme de INIT_SPACE (taille de tous les champs combinés) et de la taille du discriminateur (DISCRIMINATOR.len()).
Les comptes Solana nécessitent un dépôt de loyer en lamports, qui dépend de la taille du compte. Connaître la taille nous aide à calculer combien de lamports nous devons déposer pour ouvrir le compte.
Voici comment nous allons initier le compte dans notre structure Account :
#[account(
init,
payer = <target_account>,
space = <num_bytes> // CustomAccountType::INIT_SPACE + CustomAccountType::DISCRIMINATOR.len(),
)]
pub account: Account<'info, CustomAccountType>,Voici quelques-uns des champs utilisés dans la macro #[account], au-delà des champs seeds et bump que nous avons déjà couverts, et ce qu'ils font :
init: indique à Anchor de créer le comptepayer: quel signataire finance le loyer (ici, le maker)space: combien d'octets allouer. C'est ici que le calcul du loyer se fait également
Après la création, vous pouvez modifier les données du compte. Si vous devez changer sa taille, utilisez la réallocation :
#[account(
mut, // Mark as mutable
realloc = <space>, // New size
realloc::payer = <target>, // Who pays for the change
realloc::zero = <bool> // Whether to zero new space
)]Remarque : Lors de la réduction de la taille du compte, définissez realloc::zero = true pour garantir que les anciennes données sont correctement effacées.
Enfin, lorsque le compte n'est plus nécessaire, nous pouvons le fermer pour récupérer le loyer :
#[account(
mut, // Mark as mutable
close = <target_account>, // Where to send remaining lamports
)]
pub account: Account<'info, CustomAccountType>,Nous pouvons ensuite ajouter des PDA, des adresses déterministes dérivées de seeds et d'un ID de programme qui sont particulièrement utiles pour créer des adresses de compte prévisibles, dans ces contraintes comme ceci :
#[account(
seeds = <seeds>, // Seeds for derivation
bump // Standard bump seed
)]
pub account: Account<'info, CustomAccountType>,Remarque : les PDA sont déterministes : les mêmes seeds + programme + bump produisent toujours la même adresse et le bump garantit que l'adresse est en dehors de la courbe ed25519
Comme le calcul du bump peut "brûler" beaucoup de CU, il est toujours bon de le sauvegarder dans le compte ou de le passer dans l'instruction et de le valider sans avoir à calculer comme ceci :
#[account(
seeds = <seeds>,
bump = <expr>
)]
pub account: Account<'info, CustomAccountType>,Et il est possible de dériver un PDA d'un autre programme en passant l'adresse du programme dont il est dérivé comme ceci :
#[account(
seeds = <seeds>,
bump = <expr>,
seeds::program = <expr>
)]
pub account: Account<'info, CustomAccountType>,Compte paresseux
À partir d'Anchor 0.31.0, LazyAccount fournit une façon plus performante de lire les données de compte. Contrairement au type standard Account qui désérialise l'ensemble du compte sur la pile, LazyAccount est un compte en lecture seule, alloué sur le tas, qui utilise seulement 24 octets de mémoire de pile et vous permet de charger sélectivement des champs spécifiques.
Commencez par activer la fonctionnalité dans votre Cargo.toml :
anchor-lang = { version = "0.31.1", features = ["lazy-account"] }Maintenant, nous pouvons l'utiliser comme ceci :
#[derive(Accounts)]
pub struct MyInstruction<'info> {
pub account: LazyAccount<'info, CustomAccountType>,
}
#[account(discriminator = 1)]
pub struct CustomAccountType {
pub balance: u64,
pub metadata: String,
}
pub fn handler(ctx: Context<MyInstruction>) -> Result<()> {
// Load specific field
let balance = ctx.accounts.account.get_balance()?;
let metadata = ctx.accounts.account.get_metadata()?;
Ok(())
}Lorsque les CPI modifient le compte, les valeurs mises en cache deviennent obsolètes. Pour cette raison, vous devez utiliser la fonction unload() pour rafraîchir :
// Load the initial value
let initial_value = ctx.accounts.my_account.load_field()?;
// Do CPI...
// We still have a reference to the account from `initial_value`, drop it before `unload`
drop(initial_value);
// Load the updated value
let updated_value = ctx.accounts.my_account.unload()?.load_field()?;Comptes de jetons
Le programme de jetons (Token Program), qui fait partie de la bibliothèque de programmes Solana (SPL), est la boîte à outils intégrée pour créer et déplacer tout actif qui n'est pas du SOL natif. Il dispose d'instructions pour créer des jetons, émettre de nouvelles unités, transférer des soldes, brûler, geler, et plus encore.
Ce programme possède deux types de comptes clés :
Compte Mint : stocke les métadonnées pour un jeton spécifique : l'offre, les décimales, l'autorité d'émission, l'autorité de gel, etc.
Compte de jetons : détient un solde de cette émission pour un propriétaire particulier. Seul le propriétaire peut réduire le solde (transfert, destruction, etc.), mais n'importe qui peut envoyer des jetons au compte, augmentant ainsi son solde
Comptes de jetons dans Anchor
Nativement, la bibliothèque Anchor de base ne regroupe que les aides CPI et Accounts pour le programme système. Si vous souhaitez le même accompagnement pour les jetons SPL, vous devez intégrer la bibliothèque anchor_spl.
anchor_spl ajoute :
Des constructeurs d'aide pour chaque instruction dans les programmes SPL Token et Token-2022
Des enveloppes de type qui facilitent la vérification et la désérialisation des comptes Mint et Token
Examinons comment les comptes Mint et Token sont structurés :
#[account(
mint::decimals = <expr>,
mint::authority = <target_account>,
mint::freeze_authority = <target_account>
mint::token_program = <target_account>
)]
pub mint: Account<'info, Mint>,
#[account(
mut,
associated_token::mint = <target_account>,
associated_token::authority = <target_account>,
associated_token::token_program = <target_account>
)]
pub maker_ata_a: Account<'info, TokenAccount>,Account<'info, Mint> et Account<'info, TokenAccount> indiquent à Anchor de :
confirmer que le compte est bien un compte Mint ou Token
désérialiser ses données pour que vous puissiez lire les champs directement
appliquer toutes les contraintes supplémentaires que vous spécifiez (
authority,decimals,mint,token_program, etc.)
Ces comptes liés aux jetons suivent le même modèle init utilisé précédemment. Comme Anchor connaît leur taille en octets fixe, nous n'avons pas besoin de spécifier une valeur space, seulement le payeur qui finance le compte.
Anchor offre également la macro init_if_needed : elle vérifie si le compte de jeton existe déjà et, si ce n'est pas le cas, le crée. Ce raccourci n'est pas sûr pour tous les types de comptes, mais il est parfaitement adapté aux comptes de jetons, nous allons donc l'utiliser ici.
Comme mentionné, anchor_spl crée des assistants pour les programmes Token et Token2022, ce dernier introduisant les extensions de jetons. Le principal défi est que même si ces comptes atteignent des objectifs similaires et ont des structures comparables, ils ne peuvent pas être désérialisés et vérifiés de la même manière car ils appartiennent à deux programmes différents.
Nous pourrions créer une logique plus "avancée" pour gérer ces différents types de comptes, mais heureusement Anchor prend en charge ce scénario grâce aux InterfaceAccounts :
use anchor_spl::token_interface::{Mint, TokenAccount};
#[account(
mint::decimals = <expr>,
mint::authority = <target_account>,
mint::freeze_authority = <target_account>
)]
pub mint: InterfaceAccounts<'info, Mint>,
#[account(
mut,
associated_token::mint = <target_account>,
associated_token::authority = <target_account>,
associated_token::token_program = <target_account>
)]
pub maker_ata_a: InterfaceAccounts<'info, TokenAccount>,La différence clé ici est que nous utilisons InterfaceAccounts au lieu de Account. Cela permet à notre programme de fonctionner avec les comptes Token et Token2022 sans avoir besoin de gérer les différences dans leur logique de désérialisation. L'interface fournit un moyen commun d'interagir avec les deux types de comptes tout en maintenant la sécurité des types et une validation appropriée.
Cette approche est particulièrement utile lorsque vous souhaitez que votre programme soit compatible avec les deux standards de jetons, car elle élimine la nécessité d'écrire une logique séparée pour chaque programme. L'interface gère toute la complexité liée aux différentes structures de comptes en arrière-plan.
Si vous souhaitez en savoir plus sur l'utilisation de anchor-spl, vous pouvez suivre les cours Programme SPL-Token avec Anchor ou Programme Token2022 avec Anchor.
Types de comptes supplémentaires
Naturellement, les comptes système, les comptes de programme et les comptes de jetons ne sont pas les seuls types de comptes que nous pouvons avoir dans Anchor. Nous allons donc voir ici d'autres types de comptes que nous pouvons avoir :
Signer
Le type Signer est utilisé lorsque vous devez vérifier qu'un compte a signé une transaction. C'est crucial pour la sécurité car cela garantit que seuls les comptes autorisés peuvent effectuer certaines actions. Vous utiliserez ce type chaque fois que vous devez garantir qu'un compte spécifique a approuvé une transaction, comme lors du transfert de fonds ou de la modification de données de compte qui nécessitent une autorisation explicite. Voici comment l'utiliser :
#[derive(Accounts)]
pub struct InstructionAccounts<'info> {
#[account(mut)]
pub signer: Signer<'info>,
}Le type Signer vérifie automatiquement si le compte a signé la transaction. Si ce n'est pas le cas, la transaction échouera. C'est particulièrement utile lorsque vous devez vous assurer que seuls des comptes spécifiques peuvent effectuer certaines opérations.
AccountInfo & UncheckedAccount
AccountInfo et UncheckedAccount sont des types de compte de bas niveau qui fournissent un accès direct aux données du compte sans validation automatique. Ils sont identiques en fonctionnalité, mais UncheckedAccount est le choix préféré car son nom reflète mieux son objectif.
Ces types sont utiles dans trois scénarios principaux :
Travailler avec des comptes qui n'ont pas de structure définie
Implémenter une logique de validation personnalisée
Interagir avec des comptes d'autres programmes qui n'ont pas de définitions de type Anchor
Comme ces types contournent les vérifications de sécurité d'Anchor, ils sont intrinsèquement non sécurisés et nécessitent une reconnaissance explicite en utilisant le commentaire /// CHECK. Ce commentaire sert de documentation indiquant que vous comprenez les risques et avez mis en œuvre une validation appropriée.
Voici un exemple de leur utilisation :
#[derive(Accounts)]
pub struct InstructionAccounts<'info> {
/// CHECK: This is an unchecked account
pub account: UncheckedAccount<'info>,
/// CHECK: This is an unchecked account
pub account_info: AccountInfo<'info>,
}Option
Le type Option dans Anchor vous permet de rendre les comptes optionnels dans votre instruction. Lorsqu'un compte est enveloppé dans Option, il peut être fourni ou omis dans la transaction. C'est particulièrement utile pour :
Construire des instructions flexibles qui peuvent fonctionner avec ou sans certains comptes
Implémenter des paramètres optionnels qui ne sont pas toujours nécessaires
Créer des instructions rétrocompatibles qui peuvent fonctionner avec des structures de compte nouvelles ou anciennes
Lorsqu'un compte Option est défini comme None, Anchor utilisera l'ID du programme comme adresse du compte. Il est important de comprendre ce comportement lorsque vous travaillez avec des comptes optionnels.
Voici comment l'implémenter :
#[derive(Accounts)]
pub struct InstructionAccounts<'info> {
pub optional_account: Option<Account<'info, CustomAccountType>>,
}Box
Le type Box est utilisé pour stocker des comptes sur le tas (heap) plutôt que sur la pile (stack). Cela est nécessaire dans plusieurs scénarios :
Lorsque vous traitez de grandes structures de compte qui seraient inefficaces à stocker sur la pile
Lorsque vous travaillez avec des structures de données récursives
Lorsque vous devez travailler avec des comptes dont la taille ne peut pas être déterminée au moment de la compilation
L'utilisation de Box aide à gérer la mémoire plus efficacement dans ces cas en allouant les données du compte sur le tas. Voici un exemple :
#[derive(Accounts)]
pub struct InstructionAccounts<'info> {
pub boxed_account: Box<Account<'info, LargeAccountType>>,
}Program
Le type Program est utilisé pour valider et interagir avec d'autres programmes Solana. Anchor peut facilement identifier les comptes de programme car ils ont leur indicateur executable défini sur true. Ce type est particulièrement utile lorsque :
Vous devez effectuer des invocations inter-programmes (CPI)
Vous voulez vous assurer que vous interagissez avec le bon programme
Vous devez vérifier la propriété du programme sur les comptes
Il existe deux façons principales d'utiliser le type Program :
Utiliser les types de programme intégrés (recommandé lorsqu'ils sont disponibles) :
use anchor_spl::token::Token;
#[derive(Accounts)]
pub struct InstructionAccounts<'info> {
pub system_program: Program<'info, System>,
pub token_program: Program<'info, Token>,
}Utiliser une adresse de programme personnalisée lorsque le type de programme n'est pas disponible :
// Address of the Program
const PROGRAM_ADDRESS: Pubkey = pubkey!("22222222222222222222222222222222222222222222")
#[derive(Accounts)]
pub struct InstructionAccounts<'info> {
#[account(address = PROGRAM_ADDRESS)]
/// CHECK: this is fine since we're checking the address
pub program: UncheckedAccount<'info>,
}Remarque : Lorsque vous travaillez avec des programmes de jetons, vous pourriez avoir besoin de prendre en charge à la fois le programme de jetons hérité (Legacy Token Program) et le programme Token-2022. Dans de tels cas, utilisez le type Interface au lieu de Program :
use anchor_spl::token_interface::TokenInterface;
#[derive(Accounts)]
pub struct InstructionAccounts<'info> {
pub program: Interface<'info, TokenInterface>,
}Validation de compte personnalisée
Anchor fournit un ensemble puissant de contraintes qui peuvent être appliquées directement dans l'attribut #[account]. Ces contraintes aident à garantir la validité du compte et à appliquer les règles du programme au niveau du compte, avant l'exécution de votre logique d'instruction. Voici les contraintes disponibles :
Contrainte d'adresse
La contrainte address vérifie que la clé publique d'un compte correspond à une valeur spécifique. C'est essentiel lorsque vous devez vous assurer que vous interagissez avec un compte connu, comme un PDA spécifique ou un compte de programme :
#[account(
address = <expr>, // Basic usage
address = <expr> @ CustomError // With custom error
)]
pub account: Account<'info, CustomAccountType>,Contrainte de propriétaire
La contrainte owner garantit qu'un compte appartient à un programme spécifique. C'est une vérification de sécurité critique lors du travail avec des comptes appartenant à des programmes, car elle empêche l'accès non autorisé aux comptes qui devraient être gérés par un programme particulier :
#[account(
owner = <expr>, // Basic usage
owner = <expr> @ CustomError // With custom error
)]
pub account: Account<'info, CustomAccountType>,Contrainte d'exécutabilité
La contrainte executable vérifie qu'un compte est un compte de programme (son indicateur executable est défini sur true). C'est particulièrement utile lors des invocations inter-programmes (CPI) pour s'assurer que vous interagissez avec un programme plutôt qu'avec un compte de données :
#[account(executable)]
pub account: Account<'info, CustomAccountType>,Contrainte de mutabilité
La contrainte mut marque un compte comme mutable, permettant la modification de ses données pendant l'instruction. C'est nécessaire pour tout compte qui sera mis à jour, car Anchor impose l'immutabilité par défaut pour des raisons de sécurité :
#[account(
mut, // Basic usage
mut @ CustomError // With custom error
)]
pub account: Account<'info, CustomAccountType>,Contrainte de signataire
La contrainte signer vérifie qu'un compte a signé la transaction. C'est crucial pour la sécurité lorsqu'un compte doit autoriser une action, comme le transfert de fonds ou la modification de données. C'est une façon plus explicite d'exiger des signatures par rapport à l'utilisation du type Signer :
#[account(
signer, // Basic usage
signer @ CustomError // With custom error
)]
pub account: Account<'info, CustomAccountType>,Contrainte Has One
La contrainte has_one vérifie qu'un champ spécifique sur la structure du compte correspond à la clé publique d'un autre compte. C'est utile pour maintenir des relations entre les comptes, comme s'assurer qu'un compte de jeton appartient au bon propriétaire :
#[account(
has_one = data @ Error::InvalidField
)]
pub account: Account<'info, CustomAccountType>,Contrainte personnalisée
Lorsque les contraintes intégrées ne répondent pas à vos besoins, vous pouvez écrire une expression de validation personnalisée. Cela permet une logique de validation complexe qui ne peut pas être exprimée avec d'autres contraintes, comme vérifier la longueur des données du compte ou valider les relations entre plusieurs champs :
#[account(
constraint = data == account.data @ Error::InvalidField
)]
pub account: Account<'info, CustomAccountType>,Ces contraintes peuvent être combinées pour créer des règles de validation puissantes pour vos comptes. En plaçant la validation au niveau du compte, vous gardez vos vérifications de sécurité proches des définitions de compte et évitez de disperser les appels require!() dans toute votre logique d'instruction.
Remaining Accounts
Parfois, lors de l'écriture de programmes, la structure fixe des comptes d'instruction ne fournit pas la flexibilité dont votre programme a besoin.
Les comptes restants (Remaining accounts) résolvent ce problème en vous permettant de passer des comptes supplémentaires au-delà de la structure d'instruction définie, permettant un comportement dynamique basé sur les conditions d'exécution.
Directive d'implémentation
Les définitions d'instruction traditionnelles vous obligent à spécifier exactement quels comptes seront utilisés :
#[derive(Accounts)]
pub struct Transfer<'info> {
pub from: Account<'info, TokenAccount>,
pub to: Account<'info, TokenAccount>,
pub authority: Signer<'info>,
}Cela fonctionne très bien pour les opérations uniques, mais que faire si vous souhaitez effectuer plusieurs transferts de jetons en une seule instruction ? Vous devriez appeler l'instruction plusieurs fois, augmentant les coûts de transaction et la complexité.
Les comptes restants vous permettent de passer des comptes supplémentaires qui ne font pas partie de la structure d'instruction fixe, ce qui signifie que votre programme pourrait parcourir ces comptes et appliquer une logique répétitive de manière dynamique.
Au lieu d'exiger des instructions séparées pour chaque transfert, vous pouvez concevoir une seule instruction qui gère "N" transferts :
#[derive(Accounts)]
pub struct BatchTransfer<'info> {
pub from: Account<'info, TokenAccount>,
pub to: Account<'info, TokenAccount>,
pub authority: Signer<'info>,
}
pub fn batch_transfer(ctx: Context<BatchTransfer>, amounts: Vec<u64>) -> Result<()> {
// Handle the first transfer using fixed accounts
transfer_tokens(&ctx.accounts.from, &ctx.accounts.to, amounts[0])?;
let remaining_accounts = &ctx.remaining_accounts;
// CRITICAL: Validate remaining accounts schema
// For batch transfers, we expect pairs of accounts
require!(
remaining_accounts.len() % 2 == 0,
TransferError::InvalidRemainingAccountsSchema
);
// Process remaining accounts in groups of 2 (from_account, to_account)
for (i, chunk) in remaining_accounts.chunks(2).enumerate() {
let from_account = &chunk[0];
let to_account = &chunk[1];
let amount = amounts[i + 1];
// Apply the same transfer logic to remaining accounts
transfer_tokens(from_account, to_account, amount)?;
}
Ok(())
}Regrouper les instructions signifie :
Taille d'instruction réduite : les comptes et données répétitifs n'ont pas besoin d'être inclus
Efficacité : chaque CPI coûte 1000 CU, ce qui signifie qu'une personne utilisant votre programme pourrait l'invoquer une seule fois au lieu de 3 si elle a besoin d'exécuter des instructions par lots
Implémentation côté client
Nous pouvons facilement transmettre les comptes restants en utilisant le SDK Anchor généré une fois que nous faisons anchor build. Comme ce sont des comptes "bruts", nous devrons spécifier s'ils doivent être transmis en tant que signataire et/ou mutables comme ceci :
await program.methods.someMethod().accounts({
// some accounts
})
.remainingAccounts([
{
isSigner: false,
isWritable: true,
pubkey: new Pubkey().default
}
])
.rpc();