L'Extension Metadata
L'extension Metadata
(Métadonnées) est une extension de compte de Mint
qui introduit la possibilité d'intégrer directement des métadonnées dans les comptes de Mint de manière native, sans avoir à utiliser un autre programme.
Initialisation du Compte de Mint
L'extension Metadata
est un peu différente de ce à quoi nous sommes habitués car elle est composée de deux extensions différentes qui sont toutes deux associées à un compte Mint
:
- L'extension
Metadata
qui contient toutes les métadonnées telles que le nom, le symbole, l'URI et les comptes supplémentaires. - L'extension
MetadataPointer
qui fait référence au compte deMint
où se trouve l'extensionMetadata
.
En général, lorsqu'elles sont utilisées, ces 2 extensions se trouvent dans le même compte de Mint
. C'est ce que nous allons faire dans cet exemple.
Commençons par quelques notions de base avant de nous plonger dans le code :
Bien que l'extension MetadataPointer
se trouve dans la crate anchor-spl
, pour initialiser les Metadata
nous devons utiliser la crate spl_token_metadata_interface
.
Installons donc la crate nécessaire :
cargo add spl_token_metadata_interface
De plus, l'extension Metadata
est l'une des "seules" extensions qui nécessite d'initialiser l'extension après avoir initialisé le compte de Mint
.
Cela s'explique par le fait que l'instruction d'initialisation des métadonnées alloue de manière dynamique l'espace nécessaire au contenu des métadonnées de longueur variable.
Dans le même temps, cela signifie que nous allons devoir initialiser le compte de Mint
avec suffisamment de lamports pour être exempt de rente avec l'extension Metadata
incluse mais en allouant suffisamment d'espace uniquement pour l'extension MetadataPointer
car l'instruction token_metadata_initialize()
augmente automatiquement l'espace de manière correcte.
Voici à quoi cela ressemble :
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,
}
Mise à Jour des Métadonnées
Il est possible de mettre à jour tous les champs des métadonnées à l'aide de la même instruction token_metadata_update_field()
.
Pour les additionalMetadata
cela fonctionne un peu différemment car nous pouvons mettre à jour un champ existant en transmettant juste le même Field
avec une nouvelle valeur ou juste ajouter un nouveau champ aux Metadata
.
Le programme utilise la même instruction mais avec différents drapeaux (flag) en fonction de ce que nous essayons de modifier. Cela signifie que nous pouvons modifier tous les champs comme ceci :
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()),
}
}
}
Nous pouvons également supprimer Field
dans la structure additionalMetadata
à l'aide de l'instruction removeKey
comme ceci :
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(())
}