Anchor
Token2022 avec Anchor

Token2022 avec Anchor

L'extension Metadata

L'extension Metadata est une extension de compte Mint qui introduit la possibilité d'intégrer des métadonnées directement dans les comptes de mint de manière native et sans avoir à utiliser un autre programme.

Initializing the Mint Account

L'extension Metadata est un peu différente de ce que nous avons l'habitude de faire car elle est composée de 2 extensions différentes qui vont toutes les deux sur un compte Mint :

  • L'extension Metadata qui contient toutes les informations de métadonnées comme le nom, le symbole, l'URI et les comptes supplémentaires.

  • L'extension MetadataPointer qui fait référence au compte Mint où réside l'extension Metadata.

Habituellement, lorsqu'elles sont utilisées, ces 2 extensions résident sur le même compte Mint ; et nous allons faire de même pour cet exemple.

Commençons par quelques bases avant de plonger dans le code :

Bien que l'extension MetadataPointer se trouve dans le crate anchor-spl, pour initialiser le Metadata nous devons utiliser le crate spl_token_metadata_interface.

Installons donc le package requis :

text
cargo add spl_token_metadata_interface

De plus, l'extension Metadata est l'une des "seules" extensions qui nécessite que vous initialisiez l'extension après avoir initialisé le compte Mint.

C'est parce que l'instruction d'initialisation des métadonnées alloue dynamiquement l'espace nécessaire pour le contenu des métadonnées de longueur variable.

En même temps, cela signifie que nous allons devoir initialiser le compte Mint avec suffisamment de lamports pour être exempt de loyer avec l'extension Metadata incluse, mais en allouant assez d'espace uniquement pour l'extension MetadataPointer puisque l'instruction initializeMetadata() augmente correctement l'espace.

Dans le code, cela ressemble à ceci :

ts
// Metadata struct
const metadata: TokenMetadata = {
    mint: mint.publicKey,
    name: "Test Token",
    symbol: "TST",
    uri: "https://example.com/metadata.json",
    additionalMetadata: [["customField", "customValue"]],
};

// Size of Mint Account with extensions
const mintLen = getMintLen([ExtensionType.MetadataPointer]);

// Size of the Metadata Extension
const metadataLen = TYPE_SIZE + LENGTH_SIZE + pack(metadata).length;

// Minimum lamports required for Mint Account
const lamports = await connection.getMinimumBalanceForRentExemption(mintLen + metadataLen);

Maintenant que nous sommes familiers avec ces concepts, plongeons dans l'ajout de l'extension MetadataPointer et Metadata sur le compte de mint.

rust
use anchor_lang::prelude::*;
use anchor_lang::solana_program::rent::{
    DEFAULT_EXEMPTION_THRESHOLD, DEFAULT_LAMPORTS_PER_BYTE_YEAR,
};
use anchor_lang::system_program::{transfer, Transfer};
use anchor_spl::token_interface::{
    token_metadata_initialize, Mint, Token2022, TokenMetadataInitialize,
};
use spl_token_metadata_interface::state::TokenMetadata;
use spl_type_length_value::variable_len_pack::VariableLenPack;

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,

    #[account(
        init,
        payer = payer,
        mint::decimals = 2,
        mint::authority = payer,
        extensions::metadata_pointer::authority = payer,
        extensions::metadata_pointer::metadata_address = mint_account,
    )]
    pub mint_account: InterfaceAccount<'info, Mint>,
    pub token_program: Program<'info, Token2022>,
    pub system_program: Program<'info, System>,
}

pub fn process_initialize(ctx: Context<Initialize>, args: TokenMetadataArgs) -> Result<()> {
    let TokenMetadataArgs { name, symbol, uri } = args;

    // Define token metadata
    let token_metadata = TokenMetadata {
        name: name.clone(),
        symbol: symbol.clone(),
        uri: uri.clone(),
        ..Default::default()
    };

    // Add 4 extra bytes for size of MetadataExtension (2 bytes for the discriminator, 2 bytes for length)
    let data_len = 4 + token_metadata.get_packed_len()?;

    // Calculate lamports required for the additional metadata
    let lamports =
        data_len as u64 * DEFAULT_LAMPORTS_PER_BYTE_YEAR * DEFAULT_EXEMPTION_THRESHOLD as u64;

    // Transfer additional lamports to mint account
    transfer(
        CpiContext::new(
            ctx.accounts.system_program.to_account_info(),
            Transfer {
                from: ctx.accounts.payer.to_account_info(),
                to: ctx.accounts.mint_account.to_account_info(),
            },
        ),
        lamports,
    )?;

    // Initialize token metadata
    token_metadata_initialize(
        CpiContext::new(
            ctx.accounts.token_program.to_account_info(),
            TokenMetadataInitialize {
                token_program_id: ctx.accounts.token_program.to_account_info(),
                mint: ctx.accounts.mint_account.to_account_info(),
                metadata: ctx.accounts.mint_account.to_account_info(),
                mint_authority: ctx.accounts.payer.to_account_info(),
                update_authority: ctx.accounts.payer.to_account_info(),
            },
        ),
        name,
        symbol,
        uri,
    )?;
    Ok(())
}

#[derive(AnchorDeserialize, AnchorSerialize)]
pub struct TokenMetadataArgs {
    pub name: String,
    pub symbol: String,
    pub uri: String,
}

L'instruction initialize pour le metadata-interface ne permet pas le additionalMetdata. Pour cette raison, si nous voulons créer un asset qui en possède, nous devrons utiliser l'instruction updateField comme nous l'avons fait dans cet exemple

Mise à jour des métadonnées

Il est possible de mettre à jour tous les champs des métadonnées en utilisant la même instruction updateField().

Pour le additionalMetadata, cela fonctionne un peu différemment car nous pouvons mettre à jour un champ existant en passant simplement le même Field avec une nouvelle valeur ou simplement ajouter un nouveau champ au Metadata.

En coulisses, le programme utilise la même instruction avec différents drapeaux selon ce que nous essayons de modifier. Cela signifie que nous pouvons changer tous les champs comme ceci :

rust
use anchor_lang::prelude::*;
use anchor_lang::system_program::{transfer, Transfer};
use anchor_spl::{
    token_2022::spl_token_2022::{
        extension::{BaseStateWithExtensions, PodStateWithExtensions},
        pod::PodMint,
    },
    token_interface::{token_metadata_update_field, Mint, Token2022, TokenMetadataUpdateField},
};
use spl_token_metadata_interface::state::{Field, TokenMetadata};

#[derive(Accounts)]
pub struct UpdateField<'info> {
    #[account(mut)]
    pub authority: Signer<'info>,

    #[account(
        mut,
        extensions::metadata_pointer::metadata_address = mint_account,
    )]
    pub mint_account: InterfaceAccount<'info, Mint>,
    pub token_program: Program<'info, Token2022>,
    pub system_program: Program<'info, System>,
}

pub fn process_update_field(ctx: Context<UpdateField>, args: UpdateFieldArgs) -> Result<()> {
    let UpdateFieldArgs { field, value } = args;

    // Convert to Field type from spl_token_metadata_interface
    let field = field.to_spl_field();
    msg!("Field: {:?}, Value: {}", field, value);

    let (current_lamports, required_lamports) = {
        // Get the current state of the mint account
        let mint = &ctx.accounts.mint_account.to_account_info();
        let buffer = mint.try_borrow_data()?;
        let state = PodStateWithExtensions::<PodMint>::unpack(&buffer)?;

        // Get and update the token metadata
        let mut token_metadata = state.get_variable_len_extension::<TokenMetadata>()?;
        token_metadata.update(field.clone(), value.clone());
        msg!("Updated TokenMetadata: {:?}", token_metadata);

        // Calculate the new account length with the updated metadata
        let new_account_len =
            state.try_get_new_account_len_for_variable_len_extension(&token_metadata)?;

        // Calculate the required lamports for the new account length
        let required_lamports = Rent::get()?.minimum_balance(new_account_len);
        // Get the current lamports of the mint account
        let current_lamports = mint.lamports();

        msg!("Required lamports: {}", required_lamports);
        msg!("Current lamports: {}", current_lamports);

        (current_lamports, required_lamports)
    };

    // Transfer lamports to mint account for the additional metadata if needed
    if required_lamports > current_lamports {
        let lamport_difference = required_lamports - current_lamports;
        transfer(
            CpiContext::new(
                ctx.accounts.system_program.to_account_info(),
                Transfer {
                    from: ctx.accounts.authority.to_account_info(),
                    to: ctx.accounts.mint_account.to_account_info(),
                },
            ),
            lamport_difference,
        )?;
        
        msg!(
            "Transferring {} lamports to metadata account",
            lamport_difference
        );
    }

    // Update token metadata
    token_metadata_update_field(
        CpiContext::new(
            ctx.accounts.token_program.to_account_info(),
            TokenMetadataUpdateField {
                token_program_id: ctx.accounts.token_program.to_account_info(),
                metadata: ctx.accounts.mint_account.to_account_info(),
                update_authority: ctx.accounts.authority.to_account_info(),
            },
        ),
        field,
        value,
    )?;
    Ok(())
}

// Custom struct to implement AnchorSerialize and AnchorDeserialize
// This is required to pass the struct as an argument to the instruction
#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct UpdateFieldArgs {
    /// Field to update in the metadata
    pub field: AnchorField,
    /// Value to write for the field
    pub value: String,
}

// Need to do this so the enum shows up in the IDL
#[derive(AnchorSerialize, AnchorDeserialize, Debug)]
pub enum AnchorField {
    /// The name field, corresponding to `TokenMetadata.name`
    Name,
    /// The symbol field, corresponding to `TokenMetadata.symbol`
    Symbol,
    /// The uri field, corresponding to `TokenMetadata.uri`
    Uri,
    /// A custom field, whose key is given by the associated string
    Key(String),
}

// Convert AnchorField to Field from spl_token_metadata_interface
impl AnchorField {
    fn to_spl_field(&self) -> Field {
        match self {
            AnchorField::Name => Field::Name,
            AnchorField::Symbol => Field::Symbol,
            AnchorField::Uri => Field::Uri,
            AnchorField::Key(s) => Field::Key(s.clone()),
        }
    }
}

Comme vous pouvez le voir, nous devons toujours nous assurer que le compte est exempt de loyer, c'est pourquoi nous effectuons tous les calculs concernant le loyer au début et transférons des lamports supplémentaires si nécessaire.

Nous pouvons également supprimer des Field dans la structure additionalMetadata en utilisant l'instruction removeKey comme ceci :

rust
use anchor_lang::prelude::*;
use anchor_lang::solana_program::program::invoke;
use anchor_spl::token_interface::{Mint, Token2022};
use spl_token_metadata_interface::instruction::remove_key;

#[derive(Accounts)]
pub struct RemoveKey<'info> {
    #[account(mut)]
    pub update_authority: Signer<'info>,

    #[account(
        mut,
        extensions::metadata_pointer::metadata_address = mint_account,
    )]
    pub mint_account: InterfaceAccount<'info, Mint>,
    pub token_program: Program<'info, Token2022>,
    pub system_program: Program<'info, System>,
}

// Invoke the remove_key instruction from spl_token_metadata_interface directly
// There is not an anchor CpiContext for this instruction
pub fn process_remove_key(ctx: Context<RemoveKey>, key: String) -> Result<()> {
    invoke(
        &remove_key(
            &ctx.accounts.token_program.key(),    // token program id
            &ctx.accounts.mint_account.key(),     // "metadata" account
            &ctx.accounts.update_authority.key(), // update authority
            key,                                  // key to remove
            true, // idempotent flag, if true transaction will not fail if key does not exist
        ),
        &[
            ctx.accounts.token_program.to_account_info(),
            ctx.accounts.mint_account.to_account_info(),
            ctx.accounts.update_authority.to_account_info(),
        ],
    )?;
    Ok(())
}

Nous invoquons l'instruction remove_key depuis spl_token_metadata_interface directement puisqu'il n'y a pas d'CpiContext anchor pour cette instruction

Blueshift © 2025Commit: e573eab