Créez votre premier programme Solana

Ce démarrage rapide utilise le projet de démarrage du framework Anchor généré par anchor init. Vous allez créer le projet localement, exécuter ses tests, compiler le programme et parcourir le code du programme qu'Anchor génère.

Prérequis

Avant de commencer, installez les outils de développement Solana. L'installation inclut Rust, la CLI Solana et la CLI Anchor.

Utilisez la version 1.1.2 ou supérieure de la CLI Anchor pour ce modèle. Vérifiez votre version installée :

Terminal
$
anchor --version

Créer le projet

Exécutez les commandes suivantes dans votre terminal :

Terminal
$
anchor init my-program
$
cd my-program

Le projet de démarrage comprend un programme Solana sous programs/my-program. Le programme comprend deux instructions : une pour initialiser un compte compteur et une pour incrémenter le compteur.

Certaines parties du modèle illustrent des patterns courants des programmes Solana : la dérivation d'adresses de compte PDA, l'exécution d'un Cross Program Invocation (CPI) pour transférer des SOL, et l'utilisation de vérifications d'erreurs personnalisées pour arrêter une instruction lorsqu'une condition échoue.

Anchor.toml
Cargo.toml
Cargo.toml
lib.rs
constants.rs
error.rs
instructions.rs
initialize.rs
increment.rs
state.rs
test_initialize.rs

Compiler le programme

Exécutez anchor build pour compiler le programme de démarrage :

Terminal
$
anchor build

Le programme compilé est écrit dans target/deploy/my_program.so. Lorsque le programme est déployé, le contenu de ce fichier .so est stocké dans un compte on-chain.

Exécuter le test

Exécutez le test par défaut :

Terminal
$
anchor test

Le Anchor.toml de ce modèle utilise la commande de test Rust :

Anchor.toml
skip_local_validator = true
[scripts]
test = "cargo test"

Le test charge le programme compilé dans LiteSVM, créé un payeur, envoie les instructions initialize et increment, puis vérifie l'état du compte compteur.

L'exécution de anchor test compile également le programme, vous n'avez donc pas besoin d'exécuter anchor build en premier lors des tests en local.

Déployer le programme

Les tests locaux offrent la boucle de rétroaction la plus rapide. Lorsque vous êtes prêt à déployer sur un réseau, par exemple devnet, compilez d'abord, puis déployez sur un cluster.

Le déploiement d'un programme Solana nécessite des SOL car le programme est stocké dans un program account, et celui-ci doit payer pour l'espace qu'il utilise. Sur devnet, demandez des SOL devnet gratuits depuis le Solana Faucet ou via le Solana CLI :

Terminal
$
solana airdrop 2 --url devnet
Terminal
$
anchor build
$
anchor deploy --provider.cluster devnet

Fichiers sources

Le répertoire src contient le programme Solana. La documentation d'Anchor sur la Structure du programme explique les macros principales utilisées ici, notamment declare_id!, #[program], #[derive(Accounts)] et #[account]. Cette section parcourt les fichiers du modèle.

lib.rs

lib.rs est le point d'entrée du programme. Il relie les fichiers sources, définit l'adresse du programme et définit les instructions du programme que les utilisateurs peuvent appeler.

programs/my-program/src/lib.rs
pub mod constants;
pub mod error;
pub mod instructions;
pub mod state;
use anchor_lang::prelude::*;
pub use constants::*;
pub use instructions::*;
pub use state::*;
declare_id!("82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd");
#[program]
pub mod my_program {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
crate::instructions::initialize::handle_initialize(ctx)
}
pub fn increment(ctx: Context<Increment>) -> Result<()> {
crate::instructions::increment::handle_increment(ctx)
}
}
Anchor.toml
[programs.localnet]
my_program = "82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd"
lib.rs
declare_id!("82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd");

La même adresse de programme apparaît dans la configuration et dans le code. Anchor.toml indique à Anchor l'adresse à déployer ou à appeler pour un cluster. declare_id! définit l'adresse du programme dans le programme pour les vérifications de sécurité.

constants.rs

constants.rs regroupe les valeurs partagées en un seul endroit. Dans ce modèle, COUNTER_SEED dérive le PDA du compteur, HELLO_WORLD_LAMPORTS est transféré lors de l'initialisation, et MAX_COUNT est vérifié avant l'incrémentation.

programs/my-program/src/constants.rs
use anchor_lang::prelude::*;
#[constant]
pub const COUNTER_SEED: &[u8] = b"counter";
#[constant]
pub const HELLO_WORLD_LAMPORTS: u64 = 1;
#[constant]
pub const MAX_COUNT: u64 = 10;
initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
// ...
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
// ...
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
// ...
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
Ok(())
}

initialize.rs utilise deux constantes :

  • COUNTER_SEED dérive l'adresse PDA du compteur.
  • HELLO_WORLD_LAMPORTS définit le montant transféré du payeur vers le compte compteur.
increment.rs
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
// ...
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
Ok(())
}

increment.rs utilise MAX_COUNT comme limite supérieure du compteur. Si le compteur actuel a déjà atteint la valeur maximale, require! retourne CounterOverflow et les données du compte ne sont pas modifiées.

state.rs

state.rs définit des types de données personnalisés pour les comptes que le programme crée et possède. Le programme définit des instructions pour créer, initialiser et mettre à jour ces données, mais les données du compteur ne sont pas stockées dans le programme lui-même. Elles sont stockées dans un compte séparé possédant sa propre adresse.

programs/my-program/src/state.rs
use anchor_lang::prelude::*;
#[account]
#[derive(InitSpace)]
pub struct Counter {
pub count: u64,
pub authority: Pubkey,
}
initialize.rs
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
// ...
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
Ok(())
}
increment.rs
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
// ...
ctx.accounts.counter.count += 1;
Ok(())
}

state.rs définit les données du compte Counter. Les fichiers d'instruction utilisent ce type lors de la création et de la mise à jour du compte :

  • Counter::INIT_SPACE dimensionne le compte pour les champs définis dans state.rs.
  • count et authority sont les valeurs de champs écrites lors de l'initialisation du compte.
  • count += 1 met à jour la valeur du compteur stockée une fois la validation réussie.

error.rs

error.rs définit les erreurs personnalisées du programme. Dans ce modèle, les erreurs montrent comment les gestionnaires d'instructions s'arrêtent lorsqu'un appelant n'est pas autorisé à mettre à jour le compteur ou que celui-ci a déjà atteint MAX_COUNT.

programs/my-program/src/error.rs
use anchor_lang::prelude::*;
#[error_code]
pub enum ErrorCode {
#[msg("Only the counter authority can update this counter")]
Unauthorized,
#[msg("Counter has reached the maximum value")]
CounterOverflow,
}
increment.rs
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

error.rs nomme les erreurs que l'instruction peut retourner :

  • ErrorCode::Unauthorized est retourné lorsque le signataire n'est pas l'autorité stockée dans le compte du compteur.
  • ErrorCode::CounterOverflow est retourné lorsque le compteur a déjà atteint MAX_COUNT.

instructions.rs

instructions.rs connecte les fichiers d'instructions au crate du programme afin que lib.rs puisse accéder au code des instructions initialize et increment. Chaque fichier d'instructions définit les comptes requis par cette instruction ainsi que la logique du gestionnaire qui s'exécute après qu'Anchor a validé ces comptes.

programs/my-program/src/instructions.rs
pub mod initialize;
pub mod increment;
pub use initialize::*;
pub use increment::*;

initialize.rs

initialize.rs définit les comptes nécessaires à la création du compte compteur, puis écrit les premières valeurs du compte. La struct #[derive(Accounts)] utilise les contraintes de compte Anchor pour spécifier quels comptes sont requis et comment le nouveau compte compteur est créé.

programs/my-program/src/instructions/initialize.rs
use anchor_lang::prelude::*;
use crate::{constants::*, state::Counter};
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Account Context

La struct Initialize définit les comptes qui doivent être inclus lorsqu'un utilisateur appelle l'instruction initialize. Anchor vérifie ces comptes avant l'exécution du gestionnaire.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

Payer Account

Le compte payer paie la création du compte compteur. Le type Signer<'info> signifie que le payeur doit signer la transaction, et #[account(mut)] indique que le compte payeur peut être modifié car des lamports seront déduits.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

Counter Account

Le compte counter stocke les données Counter provenant de state.rs. init indique à Anchor de créer ce compte avant l'exécution du gestionnaire, et payer = payer indique à Anchor quel compte prend en charge les frais de création.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

Taille du compte

La contrainte space indique à Anchor la quantité de données à allouer pour le compte. Anchor stocke d'abord un discriminant de 8 octets, puis les octets nécessaires pour les champs Counter. Le discriminant permet à Anchor de reconnaître ce compte comme un compte Counter avant de désérialiser les données du compte.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

Adresse du compteur

Les contraintes seeds et bump définissent l'adresse PDA attendue pour le compte compteur. Anchor vérifie que le compte counter fourni correspond à cette adresse. Le modèle utilise un PDA afin que les utilisateurs puissent dériver l'adresse du compteur à partir de l'ID du programme et du seed, rendant ainsi l'adresse du compteur déterministe.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

System Program

Le compte system_program est requis car la création d'un nouveau compte utilise le System Program.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

Fonction de gestionnaire

La fonction handle_initialize s'exécute après qu'Anchor a validé les comptes dans Initialize. La valeur ctx donne au gestionnaire accès à ces comptes vérifiés.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Données initiales

Le gestionnaire écrit les premières valeurs dans le nouveau compte compteur. Le comptage commence à 0, et le payeur devient l'autorité autorisée à incrémenter le compteur ultérieurement.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Comptes de transfert

Ce CPI de transfert est inclus uniquement pour illustrer comment un CPI transmet des comptes à un autre programme. La structure Transfer liste les comptes utilisés par le transfert du System Program.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Contexte CPI

CpiContext::new combine le programme appelé avec les comptes transmis à ce programme. C'est la structure de base d'un CPI : choisir le programme à invoquer, collectionner les comptes attendus par ce programme, puis passer les deux dans l'invocation. Ici, le programme appelé est le System Program.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Invoke Transfer

anchor_lang::system_program::transfer invoque l'instruction de transfert du System Program. Dans ce modèle, le transfert est un exemple simple d'appel d'un autre programme depuis votre programme. Si le CPI de transfert échoue, l'instruction initialize échoue également.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Log Message

msg! écrit un message dans les journaux du programme. Ok(()) indique que l'instruction s'est terminée avec succès.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Account Context

La struct Initialize définit les comptes qui doivent être inclus lorsqu'un utilisateur appelle l'instruction initialize. Anchor vérifie ces comptes avant l'exécution du gestionnaire.

Payer Account

Le compte payer paie la création du compte compteur. Le type Signer<'info> signifie que le payeur doit signer la transaction, et #[account(mut)] indique que le compte payeur peut être modifié car des lamports seront déduits.

Counter Account

Le compte counter stocke les données Counter provenant de state.rs. init indique à Anchor de créer ce compte avant l'exécution du gestionnaire, et payer = payer indique à Anchor quel compte prend en charge les frais de création.

Taille du compte

La contrainte space indique à Anchor la quantité de données à allouer pour le compte. Anchor stocke d'abord un discriminant de 8 octets, puis les octets nécessaires pour les champs Counter. Le discriminant permet à Anchor de reconnaître ce compte comme un compte Counter avant de désérialiser les données du compte.

Adresse du compteur

Les contraintes seeds et bump définissent l'adresse PDA attendue pour le compte compteur. Anchor vérifie que le compte counter fourni correspond à cette adresse. Le modèle utilise un PDA afin que les utilisateurs puissent dériver l'adresse du compteur à partir de l'ID du programme et du seed, rendant ainsi l'adresse du compteur déterministe.

System Program

Le compte system_program est requis car la création d'un nouveau compte utilise le System Program.

Fonction de gestionnaire

La fonction handle_initialize s'exécute après qu'Anchor a validé les comptes dans Initialize. La valeur ctx donne au gestionnaire accès à ces comptes vérifiés.

Données initiales

Le gestionnaire écrit les premières valeurs dans le nouveau compte compteur. Le comptage commence à 0, et le payeur devient l'autorité autorisée à incrémenter le compteur ultérieurement.

Comptes de transfert

Ce CPI de transfert est inclus uniquement pour illustrer comment un CPI transmet des comptes à un autre programme. La structure Transfer liste les comptes utilisés par le transfert du System Program.

Contexte CPI

CpiContext::new combine le programme appelé avec les comptes transmis à ce programme. C'est la structure de base d'un CPI : choisir le programme à invoquer, collectionner les comptes attendus par ce programme, puis passer les deux dans l'invocation. Ici, le programme appelé est le System Program.

Invoke Transfer

anchor_lang::system_program::transfer invoque l'instruction de transfert du System Program. Dans ce modèle, le transfert est un exemple simple d'appel d'un autre programme depuis votre programme. Si le CPI de transfert échoue, l'instruction initialize échoue également.

Log Message

msg! écrit un message dans les journaux du programme. Ok(()) indique que l'instruction s'est terminée avec succès.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

increment.rs

increment.rs définit les comptes requis pour mettre à jour un compte compteur existant. Le handler vérifie que le signataire est l'autorité enregistrée, vérifie que le compteur n'a pas atteint la valeur MAX_COUNT spécifiée, puis incrémente le compteur.

programs/my-program/src/instructions/increment.rs
use anchor_lang::prelude::*;
use crate::{constants::*, error::ErrorCode, state::Counter};
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

Account Context

La structure Increment définit les comptes qui doivent être inclus lorsqu'un utilisateur appelle l'instruction increment. Anchor vérifie ces comptes avant l'exécution du handler.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

Counter Account

Le compte counter stocke les données Counter. La contrainte mut permet au handler de mettre à jour le compteur enregistré, et les contraintes seeds et bump vérifient l'adresse PDA du compteur.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

Authority Signer

Le compte authority doit signer la transaction. Le handler vérifie ensuite que ce signataire correspond à l'autorité enregistrée dans le compte compteur.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

Fonction du gestionnaire

La fonction handle_increment s'exécute après qu'Anchor valide les comptes dans Increment. La valeur ctx donne au gestionnaire accès à ces comptes vérifiés.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

Vérification de l'autorité

La première vérification s'assure que le signataire est autorisé à mettre à jour ce compteur. Si l'adresse du signataire ne correspond pas à counter.authority, l'instruction s'arrête avec ErrorCode::Unauthorized. Cela illustre une autorisation au niveau de l'application : le programme détient les données du compteur, mais il implémente une règle définissant quel signataire peut modifier ces données.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

Vérification du compteur maximum

La deuxième vérification empêche le compteur de dépasser MAX_COUNT. Si le compteur a déjà atteint la limite, l'instruction s'arrête avec ErrorCode::CounterOverflow. Cette limite est une règle artificielle dans le modèle afin que vous puissiez voir comment les erreurs personnalisées arrêtent une instruction avant que les données du compte ne soient modifiées.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

Mise à jour du compteur

Ce n'est qu'une fois les deux vérifications passées que le gestionnaire met à jour les données du compte. Cette ligne ajoute un à la valeur du compteur stockée.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

Message de journal

msg! écrit le compteur mis à jour dans les journaux du programme. Ok(()) indique que l'instruction s'est terminée avec succès.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

Account Context

La structure Increment définit les comptes qui doivent être inclus lorsqu'un utilisateur appelle l'instruction increment. Anchor vérifie ces comptes avant l'exécution du handler.

Counter Account

Le compte counter stocke les données Counter. La contrainte mut permet au handler de mettre à jour le compteur enregistré, et les contraintes seeds et bump vérifient l'adresse PDA du compteur.

Authority Signer

Le compte authority doit signer la transaction. Le handler vérifie ensuite que ce signataire correspond à l'autorité enregistrée dans le compte compteur.

Fonction du gestionnaire

La fonction handle_increment s'exécute après qu'Anchor valide les comptes dans Increment. La valeur ctx donne au gestionnaire accès à ces comptes vérifiés.

Vérification de l'autorité

La première vérification s'assure que le signataire est autorisé à mettre à jour ce compteur. Si l'adresse du signataire ne correspond pas à counter.authority, l'instruction s'arrête avec ErrorCode::Unauthorized. Cela illustre une autorisation au niveau de l'application : le programme détient les données du compteur, mais il implémente une règle définissant quel signataire peut modifier ces données.

Vérification du compteur maximum

La deuxième vérification empêche le compteur de dépasser MAX_COUNT. Si le compteur a déjà atteint la limite, l'instruction s'arrête avec ErrorCode::CounterOverflow. Cette limite est une règle artificielle dans le modèle afin que vous puissiez voir comment les erreurs personnalisées arrêtent une instruction avant que les données du compte ne soient modifiées.

Mise à jour du compteur

Ce n'est qu'une fois les deux vérifications passées que le gestionnaire met à jour les données du compte. Cette ligne ajoute un à la valeur du compteur stockée.

Message de journal

msg! écrit le compteur mis à jour dans les journaux du programme. Ok(()) indique que l'instruction s'est terminée avec succès.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

Fichier de test

programs/my-program/tests/test_initialize.rs est un test d'intégration Rust. Il ne démarre pas de validator local. À la place, il charge le fichier .so compilé dans LiteSVM, construit des transactions qui appellent le programme et lit le compte compteur après chaque transaction. Le test construit des instructions pour une transaction Solana en spécifiant l'ID du programme à invoquer, en fournissant des instruction data et en passant les comptes requis.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
test_initialize.rs
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Initialize {}.data(),
my_program::accounts::Initialize {
payer: payer.pubkey(),
counter,
system_program: system_program::ID,
}
.to_account_metas(None),
);

Le contexte de compte Initialize définit les comptes requis par l'instruction initialize. Le test passe ces mêmes comptes au helper my_program::accounts::Initialize généré :

  • payer est passé en tant que payer: payer.pubkey().
  • counter est passé en tant que counter.
  • system_program est passé en tant que system_program::ID.

my_program::instruction::Initialize {}.data() crée l'instruction data. C'est ici que les arguments d'instruction seraient encodés, mais cette instruction initialize ne nécessite aucun argument.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
test_initialize.rs
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Increment {}.data(),
my_program::accounts::Increment {
counter,
authority: payer.pubkey(),
}
.to_account_metas(None),
);

Le contexte de compte Increment définit les comptes requis par l'instruction increment. Le test passe ces mêmes comptes au helper my_program::accounts::Increment généré :

  • counter est passé en tant que counter.
  • authority est passé en tant que authority: payer.pubkey().

my_program::instruction::Increment {}.data() crée l'instruction data. C'est ici que les arguments d'instruction seraient encodés, mais cette instruction increment ne nécessite aucun argument.

programs/my-program/tests/test_initialize.rs
use {
anchor_lang::{
prelude::Pubkey,
solana_program::{instruction::Instruction, system_program},
AccountDeserialize, InstructionData, ToAccountMetas,
},
litesvm::LiteSVM,
solana_keypair::Keypair,
solana_message::{Message, VersionedMessage},
solana_signer::Signer,
solana_transaction::versioned::VersionedTransaction,
};
#[test]
fn test_initialize() {
let program_id = my_program::id();
let payer = Keypair::new();
let counter = Pubkey::find_program_address(
&[my_program::constants::COUNTER_SEED],
&program_id,
)
.0;
let mut svm = LiteSVM::new();
let bytes = include_bytes!(concat!(
env!("CARGO_TARGET_TMPDIR"),
"/../deploy/my_program.so"
));
svm.add_program(program_id, bytes).unwrap();
svm.airdrop(&payer.pubkey(), 1_000_000_000).unwrap();
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Initialize {}.data(),
my_program::accounts::Initialize {
payer: payer.pubkey(),
counter,
system_program: system_program::ID,
}
.to_account_metas(None),
);
let blockhash = svm.latest_blockhash();
let msg = Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &blockhash);
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &[&payer]).unwrap();
let res = svm.send_transaction(tx);
assert!(res.is_ok());
let counter_account = svm.get_account(&counter).unwrap();
let mut data: &[u8] = &counter_account.data;
let counter_state = my_program::state::Counter::try_deserialize(&mut data).unwrap();
assert_eq!(counter_state.count, 0);
assert_eq!(counter_state.authority, payer.pubkey());
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Increment {}.data(),
my_program::accounts::Increment {
counter,
authority: payer.pubkey(),
}
.to_account_metas(None),
);
let blockhash = svm.latest_blockhash();
let msg = Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &blockhash);
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &[&payer]).unwrap();
let res = svm.send_transaction(tx);
assert!(res.is_ok());
let counter_account = svm.get_account(&counter).unwrap();
let mut data: &[u8] = &counter_account.data;
let counter_state = my_program::state::Counter::try_deserialize(&mut data).unwrap();
assert_eq!(counter_state.count, 1);
assert_eq!(counter_state.authority, payer.pubkey());
}

Configuration du projet

Les fichiers racine du projet indiquent à Anchor et à Cargo comment compiler, tester et déployer le programme. Pour une référence complète, consultez la documentation Anchor pour la configuration Anchor.toml et l'Anchor CLI.

Anchor.toml
skip_local_validator = true
[toolchain]
[features]
resolution = true
skip-lint = false
[programs.localnet]
my_program = "82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd"
[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"
[scripts]
test = "cargo test"
[hooks]

Is this page helpful?

Table des matières

Modifier la page
© 2026 Fondation Solana. Tous droits réservés.