2

Question 1

  1. I was wondering how do I create a metadata PDA account that matches 1:1 with the T22 Metadata Interface. Here is my attempt so far:
#[account]
#[derive(Default, Debug)]
pub struct Mint22 {
    /// The authority that can sign to update the metadata
    pub update_authority: Option<Pubkey>,
    /// The associated mint, used to counter spoofing to be sure that metadata
    /// belongs to a particular mint
    pub mint: Pubkey,
    /// The longer name of the token
    pub name: String,
    /// The shortened symbol for the token
    pub symbol: String,
    /// The URI pointing to richer metadata
    pub uri: String,
    // TODO, does this match Vec<(String, String)>?
    /// Any additional metadata about the token as key-value pairs. The program
    /// must avoid storing the same key twice.
    pub additional_metadata: Vec<[String;2]>,
}

For reference here is the T22 Metadata interface https://github.com/solana-labs/solana-program-library/blob/56aaa6732670824273414bb14bbea12c83a46829/token-metadata/interface/src/state.rs#L25

So the sub-questions are:

Part 1 a) How do I ensure OptionalNonZeroPubkey type which is 4 bytes instead of the 1 byte Option type?

Part 1 b) Also the Anchor IDL is not happy about Vec<[String;2]> as a replacement for Vec<(String, String)>. How to accommodate this also?

Part 1 c) I most likely also need to store the bump, right? In that case, wouldn't this create a mismatch on the interface?

Part 1 d) If I adhere to this T22 Metadata interface, I assume I would be able to do something like getMetadataPointerState from the client-side using the @solana/spl-token library to fetch the account data?

Question 2

  1. How do I ensure that my account adheres to dynamic sizing depending on the contents of additional_metadata? Would there be a clean way of doing so?

Maybe something like this:

space = ExtraAccountMetaList::size_of(metas.len()).unwrap(),

Part 2 a) Although, I am curious to know if there are more examples like so?

1 Answer 1

5

Here's my attempt to build an Anchor program using the token metadata interface that only implements the Initialize and UpdateField instructions from the interface.

Alternatively, you can use the Metadata Pointer and Token Metadata extensions together to store metadata directly on the mint account. This is probably the simpler option, unless there's a specific reason you need to create your own metadata accounts. Here's an example

Part 1 a) You can use the spl_pod crate to convert the type

use spl_pod::optional_keys::OptionalNonZeroPubkey;
let update_authority_key = ctx
    .accounts
    .update_authority
    .as_ref()
    .map(|account| *account.key);

let update_authority = OptionalNonZeroPubkey::try_from(update_authority_key)
    .map_err(|_| ProgramError::InvalidArgument)?;
pub update_authority: Option<UncheckedAccount<'info>>,

Part 1 b) You can use spl_token_metadata_interface and spl_type_length_value crates to initialize and update the TokenMetadata

use spl_token_metadata_interface::{
    error::TokenMetadataError,
    instruction::TokenMetadataInstruction,
    state::{Field, TokenMetadata},
};
use spl_type_length_value::state::{
    realloc_and_pack_first_variable_len, TlvState, TlvStateBorrowed, TlvStateMut,
};

For example, to initialize the account data:

pub fn initialize(ctx: Context<Initialize>, data: InitializeData) -> Result<()> {
    // Check if update authority is provided
    let update_authority_key = ctx
        .accounts
        .update_authority
        .as_ref()
        .map(|account| *account.key);
    let update_authority = OptionalNonZeroPubkey::try_from(update_authority_key)
        .map_err(|_| ProgramError::InvalidArgument)?;

    // Construct token metadata
    let token_metadata = TokenMetadata {
        name: data.name,
        symbol: data.symbol,
        uri: data.uri,
        update_authority,
        mint: ctx.accounts.mint.key(),
        ..Default::default()
    };
    msg!("TokenMetadata: {:?}", token_metadata);

    // Calculate size and lamports for the metadata account
    let size = TokenMetadata::tlv_size_of(&token_metadata)?;
    let lamports = Rent::get()?.minimum_balance(size as usize);

    // Create metadata account
    let mint = ctx.accounts.mint.key();
    let signer_seeds: &[&[&[u8]]] = &[&[b"metadata", mint.as_ref(), &[ctx.bumps.metadata]]];
    create_account(
        CpiContext::new(
            ctx.accounts.system_program.to_account_info(),
            CreateAccount {
                from: ctx.accounts.payer.to_account_info(),
                to: ctx.accounts.metadata.to_account_info(),
            },
        )
        .with_signer(signer_seeds),
        lamports,
        size as u64,
        &id(),
    )?;

    // Initialize metadata account data
    let instance_size = get_instance_packed_len(&token_metadata)?;
    let mut buffer = ctx.accounts.metadata.try_borrow_mut_data()?;
    let mut state = TlvStateMut::unpack(&mut buffer)?;
    state.alloc::<TokenMetadata>(instance_size, false)?;
    state.pack_first_variable_len_value(&token_metadata)?;
    Ok(())
}

Had to add the extra code below to get the instruction data for the UpdateField interface instruction to work with Anchor and show up on IDL.

pub struct UpdateFieldData {
    /// 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)]
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 user field, whose key is given by the associated string
    Key(String),
}

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

Part 1 c) If you want to store the bump on the metadata account, there's probably a way to include additional data on the account that isn't part of the interface definition. Share if you figure out how!

Part 1 d)

getMetadataPointerState only gets the state of the metadata pointer extension (returning the metadata extension authority and the address of the metadata account)

@solana/spl-token v0.3.10 includes a getTokenMetadata helper function, but it only works to deserialize metadata stored directly only the mint account with the Token Metadata extension.

You can use the unpack helper function from @solana/spl-token-metadata to deserialize the TokenMetadata. However, you would need to know where the metadata starts on the account data.

    const metadataAccount = await connection.getAccountInfo(metadataPDA);
    // Metadata starts with offset of 12 bytes
    // 8 byte discriminator + 4 byte extra offset? (not sure)
    let unpackedData = unpack(metadataAccount.data.subarray(12));
    console.log("\nMetadata:", JSON.stringify(unpackedData, null, 2));

Part 2 a)

There's probably a simpler way, but here is the update_field instruction that handles resizing of the account.

pub fn update_field(ctx: Context<UpdateField>, data: UpdateFieldData) -> Result<()> {
    // Get current TokenMetadata.
    let mut token_metadata = {
        let buffer = ctx.accounts.metadata.try_borrow_data()?;
        let state = TlvStateBorrowed::unpack(&buffer)?;
        state.get_first_variable_len_value::<TokenMetadata>()?
    };

    // Check update authority.
    let update_authority = Option::<Pubkey>::from(token_metadata.update_authority)
        .ok_or_else(|| ProgramError::Custom(TokenMetadataError::ImmutableMetadata as u32))?;
    msg!("Update authority: {:?}", update_authority);
    if update_authority != *ctx.accounts.update_authority.key {
        return Err(
            ProgramError::Custom(TokenMetadataError::IncorrectUpdateAuthority as u32).into(),
        );
    }

    // Perform the update on the TokenMetadata.
    let field = data.field.to_field();
    token_metadata.update(field, data.value);
    msg!("TokenMetadata: {:?}", token_metadata);

    // Calculate the required size and lamports for the updated metadata.
    let new_size = TokenMetadata::tlv_size_of(&token_metadata)?;
    let required_lamports = Rent::get()?.minimum_balance(new_size as usize);

    // Get current state of the metadata account.
    let metadata_account_info = ctx.accounts.metadata.to_account_info();
    let current_lamports = metadata_account_info.lamports();

    // Transfer lamports if required.
    if required_lamports != current_lamports {
        let lamport_difference =
            (required_lamports as isize - current_lamports as isize).abs() as u64;
        if required_lamports > current_lamports {
            // Transfer additional lamports to metadata account.
            msg!(
                "Transferring {} lamports to metadata account",
                lamport_difference
            );
            transfer(
                CpiContext::new(
                    ctx.accounts.system_program.to_account_info(),
                    Transfer {
                        from: ctx.accounts.payer.to_account_info(),
                        to: metadata_account_info.clone(),
                    },
                ),
                lamport_difference,
            )?;
        } else {
            // Transfer excess lamports back to payer.
            msg!("Transferring {} lamports back to payer", lamport_difference);
            // Modify lamports directly because metadata account is owned by this program (and not System Program)
            ctx.accounts.metadata.sub_lamports(lamport_difference)?;
            ctx.accounts.payer.add_lamports(lamport_difference)?;
        }
    }

    // Reallocate and update the metadata account data.
    realloc_and_pack_first_variable_len(
        &ctx.accounts.metadata.to_account_info(),
        &token_metadata,
    )?;
    Ok(())
}
4

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.