65 stable releases

5.1.0 Nov 8, 2024
4.1.2 Mar 7, 2024
3.3.0 Dec 29, 2023
3.2.3 Nov 28, 2023
1.2.5 Feb 24, 2022

#753 in Magic Beans

Download history 21240/week @ 2024-07-31 15117/week @ 2024-08-07 9179/week @ 2024-08-14 8283/week @ 2024-08-21 7810/week @ 2024-08-28 6606/week @ 2024-09-04 6033/week @ 2024-09-11 5547/week @ 2024-09-18 5364/week @ 2024-09-25 4766/week @ 2024-10-02 4174/week @ 2024-10-09 5673/week @ 2024-10-16 7505/week @ 2024-10-23 10629/week @ 2024-10-30 7034/week @ 2024-11-06 6167/week @ 2024-11-13

32,434 downloads per month
Used in 150 crates (102 directly)

Custom license

3MB
68K SLoC

Metaplex Token Metadata SDK

Rust library for interacting with Metaplex Token Metadata program.

Why using a client library (SDK)?

Using a program crate as a dependency has its caveats. The main one is that you are bound to the same dependencies of that program, which tend to be quite a few. In many cases, this leads to (unnecessary) dependency problems when trying to update crate versions. Secondly, the program crate is generated from the program source code, which its main purpose is to offer the functionality of the program, not necessarily a friendly client API.

Enter an SDK crate: minimal dependencies, useful helpers. By autogenerating a client SDK to include all accounts, types, instructions and errors from a program using the IDL, we can significantly reduce the number of dependencies. The autogenerated code can be refined by adding (manually-written) helpers.

Although the SDK crate has 5 dependencies, in practice the only "real" dependency is the solana-program crate since the remaining dependencies are also dependencies of solana-program.

Getting started

From your project folder:

cargo add mpl-token-metadata

Note If you are using a solana-program version prior to 1.16, first add the solana-program dependency to your project and then add mpl-token-metadata. This will make sure you only have a single copy of the borsh crate.

Structure

The client SDK is divided into several modules:

  • accounts: structs representing the accounts of the program
  • errors: enums representing the program errors
  • instructions: structs to facilitate the creation of instructions, instruction arguments and CPI instructions
  • types: structs representing types used by the program

Instruction Builders

One of the main features of the client SDK is to facilitate the creation of instructions. There are two "types" of instruction builders automatically generated – both support passing accounts by name and optional positional

Client instruction builders

These are intended to be used by off-chain client code. Each instruction is represented by a corresponding struct – e.g., CreateV1:

pub struct CreateV1 {
    /// Unallocated metadata account with address as pda of ['metadata', program id, mint id]
    pub metadata: solana_program::pubkey::Pubkey,
    /// Unallocated edition account with address as pda of ['metadata', program id, mint, 'edition']
    pub master_edition: Option<solana_program::pubkey::Pubkey>,
    /// Mint of token asset
    pub mint: (solana_program::pubkey::Pubkey, bool),
    /// Mint authority
    pub authority: solana_program::pubkey::Pubkey,
    /// Payer
    pub payer: solana_program::pubkey::Pubkey,
    /// Update authority for the metadata account
    pub update_authority: (solana_program::pubkey::Pubkey, bool),
    /// System program
    pub system_program: solana_program::pubkey::Pubkey,
    /// Instructions sysvar account
    pub sysvar_instructions: solana_program::pubkey::Pubkey,
    /// SPL Token program
    pub spl_token_program: solana_program::pubkey::Pubkey,
}

After filling in the instruction account fields, you can use the instruction(...) method to generate the corresponding solana_program::instruction::Instruction:

// instruction args
let args = CreateV1InstructionArgs {
    name: String::from("pNFT"),
    symbol: String::from(""),
    uri: String::from("http://my.pnft"),
    seller_fee_basis_points: 500,
    primary_sale_happened: false,
    is_mutable: true,
    token_standard: TokenStandard::ProgrammableNonFungible,
    collection: None,
    uses: None,
    collection_details: None,
    creators: None,
    rule_set: None,
    decimals: Some(0),
    print_supply: Some(PrintSupply::Zero),
};

// instruction accounts
let create_ix = CreateV1 {
    metadata,
    master_edition: Some(master_edition),
    mint: (mint_pubkey, true),
    authority: payer_pubkey,
    payer: payer_pubkey,
    update_authority: (payer_pubkey, true),
    system_program: system_program::ID,
    sysvar_instructions: solana_program::sysvar::instructions::ID,
    spl_token_program: spl_token::ID,
};

// creates the instruction
let create_ix = create_ix.instruction(args);

Alternatively, you can use the CreateV1Builder to create the appropriate instruction:

let create_ix = CreateV1Builder::new()
    .metadata(metadata)
    .master_edition(Some(master_edition))
    .mint(mint_pubkey, true)
    .authority(payer_pubkey)
    .payer(payer_pubkey)
    .update_authority(payer_pubkey, true)
    .is_mutable(true)
    .primary_sale_happened(false)
    .name(String::from("pNFT"))
    .uri(String::from("http://my.pnft"))
    .seller_fee_basis_points(500)
    .token_standard(TokenStandard::ProgrammableNonFungible)
    .print_supply(PrintSupply::Zero)
    .instruction();

CPI instruction builders

These are builders to be used by on-chain code, which will CPI into Token Metadata. Similarly to "off-chain" builders, each instruction has a struct to invoke CPI instructions – e.g., TransferV1Cpi:

pub struct TransferV1Cpi<'a> {
    /// The program to invoke.
    pub __program: &'a solana_program::account_info::AccountInfo<'a>,
    /// Token account
    pub token: &'a solana_program::account_info::AccountInfo<'a>,
    /// Token account owner
    pub token_owner: &'a solana_program::account_info::AccountInfo<'a>,
    /// Destination token account
    pub destination_token: &'a solana_program::account_info::AccountInfo<'a>,
    /// Destination token account owner
    pub destination_owner: &'a solana_program::account_info::AccountInfo<'a>,
    /// Mint of token asset
    pub mint: &'a solana_program::account_info::AccountInfo<'a>,
    /// Metadata (pda of ['metadata', program id, mint id])
    pub metadata: &'a solana_program::account_info::AccountInfo<'a>,
    /// Edition of token asset
    pub edition: Option<&'a solana_program::account_info::AccountInfo<'a>>,
    /// Owner token record account
    pub token_record: Option<&'a solana_program::account_info::AccountInfo<'a>>,
    /// Destination token record account
    pub destination_token_record: Option<&'a solana_program::account_info::AccountInfo<'a>>,
    /// Transfer authority (token owner or delegate)
    pub authority: &'a solana_program::account_info::AccountInfo<'a>,
    /// Payer
    pub payer: &'a solana_program::account_info::AccountInfo<'a>,
    /// System Program
    pub system_program: &'a solana_program::account_info::AccountInfo<'a>,
    /// Instructions sysvar account
    pub sysvar_instructions: &'a solana_program::account_info::AccountInfo<'a>,
    /// SPL Token Program
    pub spl_token_program: &'a solana_program::account_info::AccountInfo<'a>,
    /// SPL Associated Token Account program
    pub spl_ata_program: &'a solana_program::account_info::AccountInfo<'a>,
    /// Token Authorization Rules Program
    pub authorization_rules_program: Option<&'a solana_program::account_info::AccountInfo<'a>>,
    /// Token Authorization Rules account
    pub authorization_rules: Option<&'a solana_program::account_info::AccountInfo<'a>>,
    /// The arguments for the instruction.
    pub __args: TransferV1InstructionArgs,
}

After filling in the program, instruction accounts and argument fields, you can use the invoke() or invoke_signed(...) method to perform the CPI:

// instruction args
let mut args = TransferV1InstructionArgs {
    amount,
    authorization_data: None,
};

// instruction accounts
let cpi_transfer = TransferV1Cpi::new(
    metadata_program_info,
    TransferV1CpiAccounts {
        token: owner_token_info,
        token_owner: owner_info,
        destination_token: destination_token_info,
        destination_owner: destination_info,
        mint: mint_info,
        metadata: metadata_info,
        authority: vault_info,
        payer: payer_info,
        system_program: system_program_info,
        sysvar_instructions: sysvar_instructions_info,
        spl_token_program: spl_token_program_info,
        spl_ata_program: spl_ata_program_info,
        edition: edition_info,
        token_record: None,
        destination_token_record: None,
        authorization_rules: None,
        authorization_rules_program: None,
    },
    args,
);

// performs the CPI
cpi_transfer.invoke_signed(&[&signer_seeds])

You can also use the TransferV1CpiBuilder to simplify the process:

let cpi_transfer = TransferV1CpiBuilder::new(metadata_program_info)
    .token(owner_token_info)
    .token_owner(owner_info)
    .destination_token(destination_token_info)
    .destination_owner(destination_info)
    .mint(mint_info)
    .metadata(metadata_info)
    .edition(edition_info)
    .authority(vault_info)
    .payer(payer_info)
    .system_program(system_program_info)
    .sysvar_instructions(sysvar_instructions_info)
    .spl_token_program(spl_token_program_info)
    .spl_ata_program(spl_ata_program_info)
    .amount(amount);

// performs the CPI
cpi_transfer.invoke_signed(&[&signer_seeds])

Note > *Builder provide a simplified way to create the required structs, since they take advantage of any default value set on the Kinobi config and do not require to set a None value to optional fields.

PDA helpers

Account types (e.g., Metadata) have associated functions to find PDA or to create PDA Pubkeys:

impl Metadata {
    pub fn find_pda(mint: Pubkey) -> (solana_program::pubkey::Pubkey, u8) {
        solana_program::pubkey::Pubkey::find_program_address(
            &[
                "metadata".as_bytes(),
                crate::MPL_TOKEN_METADATA_ID.as_ref(),
                mint.as_ref(),
            ],
            &crate::MPL_TOKEN_METADATA_ID,
        )
    }
    pub fn create_pda(
        mint: Pubkey,
        bump: u8,
    ) -> Result<solana_program::pubkey::Pubkey, solana_program::pubkey::PubkeyError> {
        solana_program::pubkey::Pubkey::create_program_address(
            &[
                "metadata".as_bytes(),
                crate::MPL_TOKEN_METADATA_ID.as_ref(),
                mint.as_ref(),
                &[bump],
            ],
            &crate::MPL_TOKEN_METADATA_ID,
        )
    }
}

If a bump seed is known, it is cheaper (in terms of compute units) to use the create_pda function, in particular for on-chain code.

Testing

To run the SDK tests, run the following from the root directory of the repository:

pnpm install

and then:

pnpm clients:rust:test

Documentation

The crate documentation can be found here.

Dependencies

~11–19MB
~293K SLoC