Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
April 5, 2022 11:29 pm GMT

French Solana dev 1: Dvelopper un program (smart-contract) sur la blockchain Solana avec le framework Anchor

Prambule

Assez rcemment, je me suis rendu compte du manque de ressources crites en franais dans le domaine du dveloppement et du web3.

Mme si de plus en plus de ressources commencent voir le jour pour lcriture de smart-contracts avec le populaire Solidity, il nen est pas de mme pour les blockchains ntant pas EVM-compatible.

En effet, ces blockchains utilisent toutes des architectures diffrentes, et ntant pas aussi populaire quEthereum lheure o jcris ces lignes, il est plus compliqu de mettre la main sur de bonnes ressources dapprentissage dans sa langue dorigine.

Alors si vous tes allergiques langlais, je vous invite lire cet article qui, jespre, pourra vous accompagner et vous aider comprendre le fonctionnement dun program sur Solana

Pr-requis

Nous utiliserons le langage Rust pour crire notre program, je vous conseille donc davoir dj les bases du langage. The Rust Book est la ressource de rfrence pour se familiariser avec Rust (aussi disponible "partiellement" en franais !).

Il est videmment primordial que vous soyez dj familier avec larchitecture dune blockchain ainsi que son fonctionnement.

Vous devrez aussi lire la documentation developer de Solana pour bien comprendre de ce que lon parle, mme si je reviendrai sur certains points pour essayer dapporter une vision plus vulgaris.

Installations

Rust

Vous pouvez installer Rust l'aide de ces trois commandes:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shsource $HOME/.cargo/envrustup component add rustfmt

Pour une installation plus dtaille, rfrez vous au Rust Book.

Solana

Le client solana vous permet dinteragir avec les diffrents rseaux Solana, de gnrer et grer vos diffrents comptes (accounts) et divers autres utilits...

Sur macOS et Linux:

sh -c "$(curl -sSfL https://release.solana.com/v1.10.4/install)"

Linstallation dtaille est disponible sur la documentation de Solana.

Pour la suite, vous aurez besoin dun account pour lutiliser avec anchor.
solana-keygen nous permet de gnrer une paire de cls publique/prive:

solana-keygen new

Yarn

Yarn est utilis par Anchor. Si vous ne lavez pas sur votre machine, il est possible de linstaller via NPM:

npm i -g corepack

Anchor

Anchor est un framework simplifiant normment la vie des dveloppeurs sur Solana et contenant une panoplie de features telles que:

  • Des crates et une librairie Rust
  • Une IDL complte pour nos programs
  • Un package TypeScript pour utiliser nos programs avec lIDL
  • une CLI et un gestionnaire despace de travail pour dvelopper des applications du backend au frontend

Il ne sagit ici que de crer notre program. Les testes et linteraction avec notre program dploy en front tant plus propice une future partie.

Anchor est comparable Truffle ou bien Hardhat, les deux frameworks les plus utiliss pour travailler sur des smart-contracts en Solidity.

Pour installer anchor sur votre machine, il est prfrable dutiliser le gestionnaire de version danchor (AVM).

Celui-ci est installer via cargo:

cargo install --git https://github.com/project-serum/anchor avm --locked --force

Vous pouvez ensuite installer la dernire version danchor via lavm:

avm install latestavm use latest

Pour vrifier quanchor est bien install:

anchor --version

Dautres mthodes dinstallation sont disponibles sur lanchor book.

Cration du projet

Pour crer un nouveau projet anchor, il suffit dutiliser la commande suivante:

anchor init <nom-du-projet>

Cela cre un dossier avec le nom de votre projet pass en argument et une base de projet partir de laquelle vous pouvez commencer travailler.

Structure dun projet Anchor

Il est important de comprendre les fichiers et dossiers que compose un projet Anchor:

  • Le dossier .anchor contient un rseau local ainsi que divers logs lis celui-ci.
  • Le dossier app peut accueillir le front-end li vos programs si vous dsirer travailler dans un seul repository.
  • Le dossier migrations contient nos scripts de migrations et de dploiement.
  • Le dossier programs contient tout nos programs. En effet, on peut crire de multiples programs pour notre projet. Notez quanchor a dj cr un program avec le nom de votre projet, qui contient un code minimaliste dexemple dans lib.rs.
  • Le dossier target est typique Rust et contient tous les builds et les fichiers compils. Pas besoin de toucher ce dossier dans 99% des cas.
  • Le dossier tests contient tout nos scripts crits pour tester nos programs.

A la racine se situe aussi le fichier de configuration danchor Anchor.toml contenant une configuration de base:

  • [programs.localnet] contient les IDs de nos diffrents programs, nous y reviendront juste aprs
  • [registry] vous permet de push votre projet vers un registre de programs.
  • [provider] contient le rseau utiliser pour excuter vos scripts de tests ainsi que laccount utiliser.
  • [scripts] contient la commande que anchor test excute pour vos scripts de tests

Structure dun program

Un program Solana crit avec Anchor se compose en plusieurs parties distinctes:

  • Une macro declare_id! qui dfinit lID de notre program.
  • Un module comportant lattribut #[program] o est dfinit tous les points dentres (entrypoints) de notre program. Une entrypoint est une fonction que lon peut appeler de lextrieur, dans une transaction, par exemple par RPC.On appelle plus frquemment ces fonctions des Insctructions et elles modifient ltat de la blockchain.
  • Des structures implmentant lattribut #[derive(Accounts)] qui dfinissent tous les accounts dont une instruction a besoin. Ces structures sont alors passes dans le Context de nos instructions.
  • Des structures implmentant lattribut #[account] vous permettent de stocker des donnes dans votre program.Je rappelle que toutes les donnes sont stockes sous la forme dun account et donc que chaque donnes ont au moins une cl publique. Nous y reviendront par la suite.

Pour la suite, on travaillera sur la base dun program ayant pour but de dfinir une identit pour chaque utilisateur. Un utilisateur pourra crer son identit, et ensuite modifier certaines parties de son identit. Il pourra aussi supprimer son identit de la blockchain, mais seulement 2 ans aprs sa cration !

Analyse du program dexemple

Aprs avoir cr votre projet anchor, vous pouvez vous rendre dans votre premier program qui a t gnr avec le nom de votre projet dans le chemin suivant:

/programs/<nom-du-projet>/src/lib.rs

lib.rs comporte dj un code dexemple minime qui devrait ressembler au code suivant:

use anchor_lang::prelude::*;/// Notre macro de dclaration d'ID pour notre programdeclare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");/// Notre module dfinissant les diffrentes instructions de notre program#[program]pub mod identity {    use super::*;    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {        Ok(())    }}/// Une structure comportant les accounts  passer dans le Context de notre instruction#[derive(Accounts)]pub struct Initialize {}

Comme vous pouvez le remarquer, nous avons trois des quatre parties que je vous ai prsents juste au dessus. Sachant quun program nest pas oblig de stocker des donnes, nous navons pas de structures implmentant lattribut #[account] pour le moment.

Ce program dexemple comporte un point dentre, linstruction initialize().
Toutes les instructions reoivent en paramtres au moins un Context<T> contenant des donnes sur le contexte actuel. T tant une structure comportant lattribut #[derive(Accounts)].

Une instruction retourne un Result<T, Error>. Result est un lment typique de Rust, qui peut comporter dans notre cas un Ok<T>, qui est renvoy si notre instruction russit, ou sinon un Err(Error) en cas dchec. Ici, Error est le type derreur que Anchor fournit. (Nous verrons comment faire nos propres erreurs plus tard).

Cette instruction ne fait donc rien hormis renvoyer un Ok(()) pour signaler la russite de notre instruction (heureusement vu quelle ne fait rien...)

Modification de lID de notre program

Quand anchor gnre ce program, lID fournit au program est toujours le mme:

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS")

Bien que pour le moment, cela ne drange pas, mieux vaut que notre program soit unique et donc, que lID soit aussi unique !

Pour ce faire, nous allons modifier cet ID par la cl publique de laccount gnr pour notre program.

En effet, anchor nous gnre dj un account avec sa paire de cl publique/priv pour chaque program que lon cre.
Ces accounts sont accessibles en tapant la commande suivante:

anchor keys list

Vous devriez voir apparaitre votre program ainsi que sa cl publique.

 anchor keys listidentity: GxyJLSDuC7BkeorKoMg87uhaXvuaUxDjDKT7iQWCbxXJ

Je pense que vous laviez devin, on va remplacer lID par dfaut de notre program par la cl publique de laccount gnr pour notre program.

declare_id!("GxyJLSDuC7BkeorKoMg87uhaXvuaUxDjDKT7iQWCbxXJ");

Il faut aussi remplacer lID par dfaut dans le fichier de configuration danchor, Anchor.toml la racine de notre projet:

[programs.localnet]identity = "GxyJLSDuC7BkeorKoMg87uhaXvuaUxDjDKT7iQWCbxXJ"

Une fois cela fait, on peut commencer de rentrer dans le vif du sujet

Dfinition dune identit

Avant de commencer travailler sur nos diffrentes instructions et nos diffrents contexts, il est plus cohrent de dfinir notre structure Identity qui dfinira comment une identit est stock par notre program.

Etant assez maniaque sur lorganisation du code, je conseille de sparer la dfinition de notre identit dans un fichier diffrent. Pour ma part, ce sera dans un module que jai nomm identites.rs.

Pour importer notre nouveau module dans notre program et pouvoir utiliser son contenu:

mod identites;

Si vous vous rappelez bien, on dfinit une structure stockant des donnes avec lattribut #[account]:

use anchor_lang::prelude::*;/// Dfinit la structure d'une identit d'un utilisateur#[account]pub struct Identity {}

On souhaite quune identit contienne un prnom, un nom, un pseudonyme, sa date de naissance, sa date de cration et lutilisateur peut aussi spcifier une adresse mail sans tre obligatoire.

Notre structure se dfinit donc comme suit:

/// Dfinit la structure d'une identit d'un utilisateur#[account]pub struct Identity {    pub first_name: String, // max 128 bytes    pub last_name: String,  // max 128 bytes    pub username: String,   // max 128 bytes    pub birth: i64,    pub mail: Option<String>, // max 128 bytes    pub created: i64}

Plusieurs remarques:

  • Les champs contenant une chaine de caractres doivent avoir une taille maximum prdfinie, bien que le type String de Rust ne soit pas forcment limit, cest nous de dfinir un maximum, nous verrons pourquoi juste aprs !Dans notre cas, sachant quun caractre encod en UTF-8 peut mesurer 1 4 bytes, 128 bytes nous assure 32 caractres dans le pire des cas.
  • Les champs birth et created contiennent une date sous la forme dun Unix timestamp. Pour une optimisation de taille, i32 serait prfrable i64, mais i32 est dangereux et pourrait rendre le program inutilisable le jour o lunix timestamp dpasse la limite de taille dun i32 (en lan 2038...). i64 nous assure une utilisation sans problme jusquen lan 2262 !
  • Notre champ mail tant optionnel, le typique Option<T> de Rust est le plus adapt ici.

Il manque un point important aborder pour nos accounts de donnes... lespace fix !

La taille de notre structure identit

Si vous vous rappelez, chaque account sur la blockchain Solana est initialis avec une taille maximum prdfinie, ce qui permet la blockchain de savoir combien slve le montant devoir payer o garder sur laccount pour tre rent-exempt.

Mme si vous nutilisez pas tout lespace disponible sur un account, vous payez quand mme pour le maximum ! Il faut donc faire attention rgler une taille maximum cohrente sur vos accounts de donnes pour ne pas faire fuir vos utilisateurs...

Il faut dterminer la taille maximum que peut prendre une identit, et pour cela, il faut se rfrencer au tableau suivant disponible sur le Anchor Book:

spaces array

Par rapport notre structure Identity, cela nous donne:

/// Dfinit la structure d'une identit d'un utilisateur#[account]pub struct Identity {    pub first_name: String, // 128 + 4 = 132    pub last_name: String,  // 128 + 4 = 132    pub username: String,   // 128 + 4 = 132    pub birth: i64, // 8    pub mail: Option<String>, // 128 + 1 = 129    pub created: i64 // 8}

Pour finir, on peut placer ces donnes dans des constantes de notre structure Identity:

impl Identity {    pub const MAX_STRING_SIZE: usize = 128;    pub const MAX_IDENTITY_SIZE: usize = 132 + 132 + 132 + 8 + 129 + 8;}

Dfinition de nos entrypoints

On va ensuite dfinir nos diffrentes instructions pour grer notre identit.

On peut supprimer linstruction initialize() dexemple car nous ne lutiliserons pas.

En reprenant le cahier des charges que jai nonc plus haut, jai dfini toutes ces instructions:

#[program]pub mod identity {    use super::*;    /// Permet  un utilisateur sans identit de crer son identit    pub fn create_identity(        ctx: Context<Initialize>,        first_name: String,        last_name: String,        username: String,        birth: i64,        mail: Option<String>    ) -> Result<()> {        // TODO        Ok(())    }    /// Permet  un utilisateur de mettre  jour son prnom    pub fn update_name(ctx: Context<Initialize>, first_name: String) -> Result<()> {        // TODO        Ok(())    }    /// Permet  un utilisateur de mettre  jour son pseudonyme    pub fn update_username(ctx: Context<Initialize>, username: String) -> Result<()> {        // TODO        Ok(())    }    /// Permet  un utilisateur de mettre  jour ou supprimer son mail    pub fn update_mail(ctx: Context<Initialize>, mail: Option<String>) -> Result<()> {        // TODO        Ok(())    }        /// Permet  un utilisateur ayant une identit depuis plus de 2 ans        /// de supprimer son identit         pub fn delete_identity(ctx: Context<Initialize>) -> Result<()> {        // TODO        Ok(())    }}

Nos instructions sont assez explicites je pense, pas besoin de revenir dessus.

Il faut maintenant dfinir nos diffrents Accounts passer nos instructions...

Dfinition des struct Accounts passer notre instructions

Bien que cela puisse paratre surprenant, nous auront besoin de dfinir seulement trois structures Accounts diffrentes !

3 structures pour 5 instructions ? Eh oui !

Cest parce que nos instructions update utiliseront toutes la mme logique

Commenons par dfinir les Accounts que notre instruction create_identity() besoin.

Pour rappel, on dfinit une structure Accounts avec lattribut #[derive(Accounts)], comme tel:

#[derive(Accounts)]pub struct CreateIdentity<'info> {}

Puis on ajoute pour chaque nouveau champ, le type daccount qui est attendu.

Il existe plusieurs types que vous pouvez renseigner, en voici une liste non-exhaustive:

  • Le type Account<info, T>, qui assure que T est une donne dont notre program est propritaire (Par exemple: notre structure Identity)
  • Le type Signer<info>, qui assure que laccount spcifi bien signer la transaction.
  • Le type Program<info, T>, qui assure que laccount spcifi est bien un program T o T est lID du program voulu.
  • Le type UncheckedAccount<info>, qui ne procde aucune vrification sur laccount spcifi.

En plus de ces diffrents types, il est possible dutiliser des contraintes (constraints) sur nos accounts pour procder dautres vrifications. Il est possible dajouter ces contraintes en ajoutant un attribut #[account()] au dessus du champ de laccount, en ajoutant dans les parenthses les paramtres voulus.

En voici une list non-exhaustive (Plus de dtails ici) :

  • #[account(mut)] rend laccount mutable et permet de modifier son tat (Par exemple: lui faire dpenser des SOL).
  • #[account(address = <expr>)] vrifie que la cl publique de laccount correspond expr.
  • #[account(init,payer = <target_account>,space = <bytes_size>] permet dinitialiser laccount spcifi. Un payer doit tre spcifi pour rgler le montant requis lors du stockage des donnes, ainsi que lespace maximum que la donne prend. Nous lutiliserons juste aprs pour crer notre identit

Il en existe encore un autre dassez important, mais je ne vais pas vous embter pour le moment avec a, on y viendra assez vite dans tout les cas ! Voyez a comme le boss final du dveloppement de program sur Solana

Nous avons maintenant toutes les cls en mains pour implmenter notre structure CreateIdentity.

Pour rsumer, nous avons besoin du compte de lutilisateur, qui devra signer la transaction pour crer son identit, ainsi que payer la cration de ses donnes. Nous avons aussi besoin du System Program pour crer notre account Identity qui stockera lidentit de notre utilisateur.

Voici quoi ressemble notre Context CreateIdentity:

#[derive(Accounts)]pub struct CreateIdentity<'info> {    #[account(mut)]    pub user: Signer<'info>,    #[account(init, payer = user, space = Identity::MAX_IDENTITY_SIZE + 8)]    pub identity: Account<'info, Identity>,    pub system_program: SystemAccount<'info>}

Remarquez que lon rend laccount de lutilisateur mutable, requis par le system program pour crer laccount identity et payer la rent de notre account identity.
De plus, vous vous demandez peut-tre quel est ce + 8 pour le paramtre space ?
Je ne vais pas rentrer dans des dtails trop technique, mais ce sont 8 bytes requis par anchor lors de la dserialisation de notre account Identity.

Dfinition des autres structures Accounts

Pour les instructions update_name(), update_username() et update_mail(), nous avons besoin du compte de lutilisateur, de sa signature, de son account identity, et cest tout !

Notez que laccount identity doit tre mutable vu qu'on va modifier ses donnes

#[derive(Accounts)]pub struct UpdateIdentity<'info> {    pub user: Signer<'info>,    #[account(mut)]    pub identity: Account<'info, Identity>}

Peut-tre que vous avez dj remarqu que quelque chose nallait pas... nous y reviendront aprs ;)
Pour les plus malins, faites comme si tout allait bien et continuons avec limplmentation de la logique de nos instructions.

Implmentation de nos instructions

create_identity

Commenons par crire la premire instruction quun utilisateur doit appeler, ce qui va initialiser son account Identity et stocker son identit.

Pas besoin de grer linitialisation de laccount Identity car celle-ci est effectue avec notre contraintes init comme vu plus haut (une bonne chose de faite ! ).

Il faut dabords vrifier que les String fournis par lutilisateur ne dpasses pas la limite fix, cest dire 128 bytes comme dfinit plus tt. Si vous tes familier avec Solidity, anchor propose des macros require pour retourner vrifier une condition entre deux variables et retourner une erreur au choix.

On pourrait (et est recommand) faire dautres vrifications pour optimiser la scurit de notre program, mais je vais faire limpasse histoire dallger pour le moment.

// Check des infos fournit par l'utilisateurrequire_gte!(Identity::MAX_STRING_SIZE, first_name.len());require_gte!(Identity::MAX_STRING_SIZE, last_name.len());require_gte!(Identity::MAX_STRING_SIZE, username.len());if mail.is_some() {    require_gte!(Identity::MAX_STRING_SIZE, mail.as_ref().unwrap().len());}

Il ne reste plus qu enregistrer ces donnes dans notre tout nouvel account Identity.

Pour cela, rien de plus simple, on passe juste les valeurs fournit lors du call de l'instruction par lutilisateur pour chaque champs de notre account identity.

On peux accder aux donnes et infos des diffrents Accounts que lon a pass notre Context avec ctx.accounts.

// Enregistrement des donnes dans notre account Identitylet user_identity = &mut ctx.accounts.identity;user_identity.first_name = first_name;user_identity.last_name = last_name;user_identity.birth = birth;user_identity.mail = mail;user_identity.created = Clock::get().unwrap().unix_timestamp;
/// Permet  un utilisateur sans identit de crer son identit    pub fn create_identity(        ctx: Context<CreateIdentity>,        first_name: String,        last_name: String,        username: String,        birth: i64,        mail: Option<String>    ) -> Result<()> {        // Check des infos fournit par l'utilisateur        require_gte!(Identity::MAX_STRING_SIZE, first_name.len());        require_gte!(Identity::MAX_STRING_SIZE, last_name.len());        require_gte!(Identity::MAX_STRING_SIZE, username.len());        if mail.is_some() {            require_gte!(Identity::MAX_STRING_SIZE, mail.as_ref().unwrap().len());        }        // Enregistrement des donnes dans notre account Identity        let user_identity = &mut ctx.accounts.identity;        user_identity.first_name = first_name;        user_identity.last_name = last_name;        user_identity.birth = birth;        user_identity.mail = mail;        user_identity.created = Clock::get().unwrap().unix_timestamp;        Ok(())    }

Et voila ! Notre premire instruction est pleinement implmente et fonctionnelle.... enfin presque.

Vous vous souvenez du boss final dont jai rapidement voqu plus haut ? Il est venu le temps d'y faire face !

Laissez moi vous parler des PDA.

Program Derived Address

Le problme actuel

Pour commencer, analysons le problme qui se pose actuellement sur notre program didentit.

Je vous rappelle que chaque utilisateur a sa propre identit, cest dire que pour chaque utilisateur/cl publique, un account Identity doit tre cr et exister.

Premirement, cela veut dire quun utilisateur doit dabord gnrer une nouvelle paire de cl publique/prive qui sera son account Identity si il souhaite une identit, ce qui nest pas trs pratique. Cela permettrait en plus davoir un nombre infini didentit pour une seule cl publique.
Ensuite, mme si les utilisateurs tait daccord avec ce systme, cela comporte un norme problme de scurit. Regardons sur nos Accounts que lon passe pour nos instructions update:

#[derive(Accounts)]pub struct UpdateIdentity<'info> {    pub user: Signer<'info>,    #[account(mut)]    pub identity: Account<'info, Identity>}

A quoi sert user ? Eh bien... pour le moment, rien.
En effet, peu importe qui signe la transaction, et peu importe la cl publique de laccount identity, les modifications seront pris en compte pour laccount identity....

Le problme ici est que nous navons aucun lien entre lutilisateur signant la transaction et laccount identity fourni, et quil est impossible de vrifier la proprit et lunicit dune identit par rapport la cl publique dun utilisateur.

Bon, rassurez-vous, si je vous parle de tout a, cest que le PDA rsout tout ces problmes

Explication des PDA

Avant toute chose, le PDA est l'un des principes les plus fourbes, mais aussi des plus important pour un dveloppeur Solana, ne vous dcouragez pas maintenant !

Les PDA, pour Program Derived Address o bien Adresse drive de programme, sont des adresses gnres partir de lID dun program et de plusieurs seeds.

Une PDA a une certaine particularit, elle ne doit pas appartenir la courbe ed25519 !
Ce qui veut dire quune PDA a la forme dune cl publique, MAIS NA PAS de cl prive associe. Il est donc impossible pour un utilisateur de gnrer une signature valide pour un account avec une PDA en tant que cl publique !

Le PDA est un remplaant direct au Mapping qu'on pourrait connaitre sur Solidity pour associer une adresse une donne. (Pour ceux qui se posent la question, HashMap de Rust nest pas fonctionnelle dans un program sur Solana pour le moment).

Maintenant, penchons nous sur la fonction suivante:

findProgramDerivedAddress(programId, seeds)

Cette fonction retourne ladresse trouve partir de lID du program fournit ainsi quune seed fournit. Le problme tant que cette fonction a une chance de russite denviron 50%.

Souvenez-vous quune PDA valide ne doit pas appartenir la courbe ed25519, de ce fait, nous devons tre certain que notre fonction renvoie une adresse valide.

Pour ce faire, il faut ajouter un troisime argument, qu'on appelle le bump. Ce bump est un entier qui sera incrment chaque fois quune adresse non-valide est retourne. La fonction va alors se rpter en incrmentant le bump jusqu trouver une adresse ne se trouvant pas sur la courbe.

findProgramDerivedAddress(programId, seeds, bump)

Grce au PDA, nous pouvons dsormais crer un account Identity unique pour chaque utilisateur sans devoir stocker quoi que ce soit car cette adresse est calculable directement par notre program ou nos utilisateurs !

Nous pourrons aussi dfinir des contraintes pour faire en sorte que lutilisateur qui signe la transaction ne puisse accder qu son propre account Identity et la modifier en consquence.

Implmentation du PDA

CreateIdentity

Cette implmentation se fait au niveau de nos structures Accounts.

Regardons du cot de nos Accounts que lon passe pour notre instruction de cration didentit:

#[derive(Accounts)]pub struct CreateIdentity<'info> {    #[account(mut)]    pub user: Signer<'info>,    #[account(        init,        payer = user,         space = Identity::MAX_IDENTITY_SIZE + 8,        // PDA  implmenter    )]    pub identity: Account<'info, Identity>,    pub system_program: SystemAccount<'info>}

Bien que la notion de PDA puisse tre complexe comprendre, limplmentation de celle-ci se fait en quelques lignes !

Nous devons ajouter une contrainte seeds lors de linitialisation de notre account identity qui permet de calculer la PDA partir de la seeds fournit, et de refuser toute autre adresse passe si ladresse nest pas la PDA calcule.

Nous devons maintenant savoir quelle seed utiliser. En rgle gnrale, notre seed sera base sur au moins trois paramtres:

  • Une chaine de caractres pour pouvoir diffrencier la gnration dune PDA dun certain type daccount dautres.
  • La cl publique de notre utilisateur signant la transaction. Cest ce qui permet de faire le lien entre son identit et sa cl publique ! En effet, chaque cl publique gnrera une PDA diffrente .
  • Le bump, essentiel pour assurer notre program de trouver une PDA avec nos deux premires seeds fournies. Notre program utilisera le premier bump valide, aussi appel bump canonique (canonical bump)

Retranscrit au niveau de notre code, voici ce que lon obtient:

seeds = [b"Identity", user.key().as_ref()], bump

Il faut ensuite sauvegarder le bump trouv par notre program pour sassurer que ladresse gnre plus tard pour accder notre account Identity sera toujours la mme.

/// Dfinit la structure d'une identit d'un utilisateur#[account]pub struct Identity {    pub first_name: String, // 128 + 4 = 132    pub last_name: String,  // 128 + 4 = 132    pub username: String,   // 128 + 4 = 132    pub birth: i64, // 8    pub mail: Option<String>, // 128 + 1 = 129    pub created: i64, // 8    pub bump: u8 // 1}

Noubliez pas de rajouter lespace que prends le bump dans notre structure Identity sur la constante MAX_IDENTITY_SIZE.

Il ne reste plus qu stocker le bump trouv par notre program lors de la cration de notre identit en limplmentant dans notre instruction. Les bumps calculs par notre program sont accessibles par ctx.bumps.get(<account>).

user_identity.bump = *ctx.bumps.get("identity").unwrap();

UpdateIdentity

Limplmentation est presque identique pour nos Accounts dupdate.
Il faut vrifier que ladresse passe est bien la PDA calcule pour lutilisateur qui signe la transaction.

#[account(    mut,    seeds = [b"Identity", user.key().as_ref()], bump = identity.bump)]pub identity: Account<'info, Identity>

CloseIdentity

#[account(    mut,    close = user,    seeds = [b"Identity", user.key().as_ref()], bump = identity.bump)]pub identity: Account<'info, Identity>

Et voila ! Notre logique de PDA est bien implmente et chacun de nos utilisateurs ne peut grer quune seule identit et seulement la leur

--

Implmentation de nos instructions Part. 2

Implmentons maintenant la logique de nos instructions de modifications.
Cela sera le mme modle pour nos trois instructions:

  1. Vrification de la donne
  2. Stockage de la donne

update_name()

// Permet  un utilisateur de mettre  jour son prnompub fn update_name(ctx: Context<UpdateIdentity>, first_name: String) -> Result<()> {    require_gte!(Identity::MAX_STRING_SIZE, first_name.len());    ctx.accounts.identity.first_name = first_name;    Ok(())}

update_username()

/// Permet  un utilisateur de mettre  jour son pseudonymepub fn update_username(ctx: Context<UpdateIdentity>, username: String) -> Result<()> {    require_gte!(Identity::MAX_STRING_SIZE, username.len());    ctx.accounts.identity.username = username;    Ok(())}

update_mail()

/// Permet  un utilisateur de mettre  jour ou supprimer son mailpub fn update_mail(ctx: Context<UpdateIdentity>, mail: Option<String>) -> Result<()> {    if mail.is_some() {        require_gte!(Identity::MAX_STRING_SIZE, mail.as_ref().unwrap().len());    }    ctx.accounts.identity.mail = mail;    Ok(())}

delete_identity()

Ltape finale consiste implmenter linstruction qui permettra un utilisateur de supprimer son identit si celle-ci existe depuis plus de deux ans.

La fermeture de laccount tant dj gre par la contrainte close, il sagira ici seulement de vrifier que lidentit existe depuis au moins 2 ans, ce sans quoi la transaction sera revert et donc laccount ne sera pas ferm.

/// Permet  un utilisateur ayant une identit depuis plus de 2 ans/// de supprimer son identit pub fn delete_identity(ctx: Context<CloseIdentity>) -> Result<()> {    let now = Clock::get().unwrap().unix_timestamp;    let created = ctx.accounts.identity.created;    let since = now - created;    require_gt!(since, CAN_DELETE_AFTER);    Ok(())}

Dfinition et mission d'un vnement (Event)

Les Events sont des lments importants mettre en place pour garder un historique efficace de certaines donnes et faciliter la communication avec nos applications off-chain.
Celles-ci pourront souscrire l'event li un certain contexte de notre program, et excuter une ou des actions en consquence chaque nouvel vnement mit.

Nous allons mettre en place un event qui sera mit chaque cration d'une nouvelle identit.
Un event se dfinit par une structure comportant l'attribut #[event] propos par anchor. Cette structure peut accueillir divers champs qui dfiniront les donnes que notre event contiendra.

Dans notre cas, nous souhaitons que notre event contienne la cl publique de l'utilisateur ayant cr son identit, son pseudonyme ainsi que la date et l'heure de la cration de l'identit.

#[event]pub struct IdentityCreated {    pub pubkey: Pubkey,    pub username: String,    pub timestamp: i64,}

Rien de sorcier ici, il ne nous reste plus qu' mettre notre vnement la fin de notre instruction.
La macro emit!() fournit par anchor nous permet d'mettre une structure comportant l'attribut #[event] comme suit:

// Emet un `Event` signifiant qu'une nouvelle identit est cre        emit!(event::IdentityCreated {            pubkey: ctx.accounts.user.key(),            username,            timestamp: ctx.accounts.identity.created        });

Dfinition de nos erreurs personnalises

Pour finaliser notre program, nous pouvons dfinir et implmenter des erreurs personnalises avec des messages plus explicites pour nos utilisateurs suivant les diffrents contextes.

Anchor propose un attribut #[error_codes] qui permet dimplmenter le type Error de anchor une numration derreurs personnalises.

Encore une fois, je vais dfinir les erreurs dans un fichier diffrent que jappellerai error.rs

use anchor_lang::error_code;#[error_code]pub enum IdentityError {    StringTooLarge,    TimeNotPassed}

Rien de bien compliqu ici

Pour dfinir un message personnalis sur une erreur, nous pouvons utiliser lattribut #[msg]:

#[msg("Specified string is higher than the expected maximum space")]StringTooLarge,#[msg("2 year is needed since the creation of the identity to be closed")]TimeNotPassed

Il ne reste plus qu implmenter nos erreurs personnalises dans nos instructions !

require_gt!(since, CAN_DELETE_AFTER, IdentityError::TimeNotPassed);

Et voila ! Notre program est dsormais termin, bien quencore amliorable, mais ce nest pas le but de cet article

mod identites;mod error;use anchor_lang::prelude::*;use identites::Identity;use error::IdentityError;declare_id!("GxyJLSDuC7BkeorKoMg87uhaXvuaUxDjDKT7iQWCbxXJ");#[program]pub mod identity {    use super::*;    pub const CAN_DELETE_AFTER: i64 = 31556926 * 2;    /// Permet  un utilisateur sans identit de crer son identit    pub fn create_identity(        ctx: Context<CreateIdentity>,        first_name: String,        last_name: String,        username: String,        birth: i64,        mail: Option<String>    ) -> Result<()> {        // Check des infos fournit par l'utilisateur        require_gte!(Identity::MAX_STRING_SIZE, first_name.len(), IdentityError::StringTooLarge);        require_gte!(Identity::MAX_STRING_SIZE, last_name.len(), IdentityError::StringTooLarge);        require_gte!(Identity::MAX_STRING_SIZE, username.len(), IdentityError::StringTooLarge);        if mail.is_some() {            require_gte!(Identity::MAX_STRING_SIZE, mail.as_ref().unwrap().len(), IdentityError::StringTooLarge);        }        // Enregistrement des donnes dans notre account Identity        let user_identity = &mut ctx.accounts.identity;        user_identity.first_name = first_name;        user_identity.last_name = last_name;        user_identity.birth = birth;        user_identity.mail = mail;        user_identity.created = Clock::get().unwrap().unix_timestamp;        user_identity.bump = *ctx.bumps.get("identity").unwrap();        Ok(())    }    /// Permet  un utilisateur de mettre  jour son prnom    pub fn update_name(ctx: Context<UpdateIdentity>, first_name: String) -> Result<()> {        require_gte!(Identity::MAX_STRING_SIZE, first_name.len(), IdentityError::StringTooLarge);        ctx.accounts.identity.first_name = first_name;        Ok(())    }    /// Permet  un utilisateur de mettre  jour son pseudonyme    pub fn update_username(ctx: Context<UpdateIdentity>, username: String) -> Result<()> {        require_gte!(Identity::MAX_STRING_SIZE, username.len(), IdentityError::StringTooLarge);        ctx.accounts.identity.username = username;        Ok(())    }    /// Permet  un utilisateur de mettre  jour ou supprimer son mail    pub fn update_mail(ctx: Context<UpdateIdentity>, mail: Option<String>) -> Result<()> {        if mail.is_some() {            require_gte!(Identity::MAX_STRING_SIZE, mail.as_ref().unwrap().len(), IdentityError::StringTooLarge);        }        ctx.accounts.identity.mail = mail;        Ok(())    }    /// Permet  un utilisateur ayant une identit depuis plus de 2 ans    /// de supprimer son identit     pub fn delete_identity(ctx: Context<CloseIdentity>) -> Result<()> {        let now = Clock::get().unwrap().unix_timestamp;        let created = ctx.accounts.identity.created;        let since = now - created;        require_gt!(since, CAN_DELETE_AFTER, IdentityError::TimeNotPassed);        Ok(())    }}#[derive(Accounts)]pub struct CreateIdentity<'info> {    #[account(mut)]    pub user: Signer<'info>,    #[account(        init,        payer = user,         space = Identity::MAX_IDENTITY_SIZE + 8,        seeds = [b"Identity", user.key().as_ref()], bump    )]    pub identity: Account<'info, Identity>,    pub system_program: SystemAccount<'info>}#[derive(Accounts)]pub struct UpdateIdentity<'info> {    pub user: Signer<'info>,    #[account(        mut,        seeds = [b"Identity", user.key().as_ref()], bump = identity.bump    )]    pub identity: Account<'info, Identity>}#[derive(Accounts)]pub struct CloseIdentity<'info> {    pub user: Signer<'info>,    #[account(        mut,        close = user,        seeds = [b"Identity", user.key().as_ref()], bump = identity.bump    )]    pub identity: Account<'info, Identity>}
use anchor_lang::prelude::*;/// Dfinit la structure d'une identit d'un utilisateur#[account]pub struct Identity {    pub first_name: String, // 128 + 4 = 132    pub last_name: String,  // 128 + 4 = 132    pub username: String,   // 128 + 4 = 132    pub birth: i64, // 8    pub mail: Option<String>, // 128 + 1 = 129    pub created: i64, // 8    pub bump: u8 // 1}impl Identity {    pub const MAX_STRING_SIZE: usize = 128;    pub const MAX_IDENTITY_SIZE: usize = 132 + 132 + 132 + 8 + 129 + 8 + 1;}
use anchor_lang::error_code;#[error_code]pub enum IdentityError {    #[msg("Specified string is higher than the expected maximum space")]    StringTooLarge,    #[msg("2 year is needed since the creation of the identity to be closed")]    TimeNotPassed}

Conclusion

Vous devriez dsormais avoir les cls en mains pour faire vos propres programs Solana !

Une version plus "rustic" du projet est disponible sur mon github.

Dans une prochaine partie, jexpliquerai comment tester les programs que vous produisez, toujours via anchor, avec Typescript + Chai.

Jvoquerai aussi prochainement les tokens sur Solana (SPL), les Cross-Program Invocations (CPI) ou divers articles sur d'autres technologies web3.

Si vous aimez mon contenu et/ou que celui-ci vous aide en tant que dveloppeur, vous tes le bienvenue sur mes diffrents rseaux

LinkedIn
Twitter
Instagram


Original Link: https://dev.to/sailorsnow/french-solana-dev-1-developper-un-program-smart-contract-sur-la-blockchain-solana-avec-le-framework-anchor-4il

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To