diff --git a/content/guides/token-extensions/metadata-pointer.md b/content/guides/token-extensions/metadata-pointer.md new file mode 100644 index 000000000..312b5b73d --- /dev/null +++ b/content/guides/token-extensions/metadata-pointer.md @@ -0,0 +1,501 @@ +--- +date: Dec 21, 2023 +seoTitle: "Token Extensions: Metadata Pointer and Token Metadata" +title: How to use the Metadata Pointer extension +description: + "The Metadata Pointer extension enables a Mint Account to specify the address + of the account that stores its metadata. When used along with the Metadata + Extension, metadata can be stored directly on the Mint Account." +keywords: + - token 2022 + - token extensions + - token program +difficulty: beginner +tags: + - token 2022 + - token extensions +--- + +Before the Token Extensions Program and the +[Token Metadata Interface](https://github.com/solana-labs/solana-program-library/tree/master/token-metadata/interface), +the process of adding extra data to a Mint Account required creating a Metadata +Account through the +[Metaplex Metadata Program](https://developers.metaplex.com/token-metadata). + +The `MetadataPointer` extension now enables a Mint Account to specify the +address of its corresponding Metadata Account. This flexibility allows the Mint +Account to point to any account owned by a program that implements the Token +Metadata Interface. + +The Token Extensions Program directly implements the Token Metadata Interface, +made accessible through the `TokenMetadata` extension. With the `TokenMetadata` +extension, the Mint Account itself can now store the metadata. + +In this guide, we will demonstrate how to create a Mint Account that enables +both the `MetadataPointer` and `TokenMetadata` extensions. This setup simplifies +the process of adding metadata to a Mint Account by storing all the data on a +single account. Here is the +[final script](https://beta.solpg.io/65964e90cffcf4b13384ceca). + +## Token Metadata Interface Overview + +The +[Token Metadata Interface](https://github.com/solana-labs/solana-program-library/tree/master/token-metadata/interface) +is designed to standardize and simplify the process of adding metadata to tokens +by defining the data structure and set of instructions for handling metadata. + +The Token Metadata Interface can be implemented by any program. This allows +developers the flexibility to create custom Metadata Programs while reducing the +challenges related to ecosystem integration for their program. + +With this common interface, wallets, dApps, and on-chain programs can +universally access token metadata, and tools for creating or modifying metadata +become universally compatible. + +### Metadata Interface Fields + +The Token Metadata Interface defines a standard set of data fields for +[`TokenMetadata`](https://github.com/solana-labs/solana-program-library/blob/master/token-metadata/interface/src/state.rs#L25-L40), +as outlined below. Additionally, it allows for the inclusion of custom data +fields within the `additional_metadata` section, formatted as key-value pairs. + +```rust +pub struct TokenMetadata { + /// The authority that can sign to update the metadata + pub update_authority: OptionalNonZeroPubkey, + /// 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, + /// Any additional metadata about the token as key-value pairs. The program + /// must avoid storing the same key twice. + pub additional_metadata: Vec<(String, String)>, +} +``` + +### Metadata Interface Instructions + +The Metadata Interface specifies the following +[instructions](https://github.com/solana-labs/solana-program-library/blob/master/token-metadata/interface/src/instruction.rs): + +- [**Initialize**](https://github.com/solana-labs/solana-program-library/blob/master/token-metadata/interface/src/instruction.rs#L97): + Initialize the basic token metadata fields (name, symbol, URI). + +- [**UpdateField**](https://github.com/solana-labs/solana-program-library/blob/master/token-metadata/interface/src/instruction.rs#L120): + Updates an existing token metadata field or adds to the `additional_metadata` + if it does not already exist. Requires resizing the account to accommodate for + addition space. + +- [**RemoveKey**](https://github.com/solana-labs/solana-program-library/blob/master/token-metadata/interface/src/instruction.rs#L137): + Deletes a key-value pair from the `additional_metadata`. This instruction does + not apply to the required name, symbol, and URI fields. + +- [**UpdateAuthority**](https://github.com/solana-labs/solana-program-library/blob/master/token-metadata/interface/src/instruction.rs#L147): + Updates the authority allows to change the token metadata. + +- [**Emit**](https://github.com/solana-labs/solana-program-library/blob/master/token-metadata/interface/src/instruction.rs#L162): + Emits the token metadata in the format of the `TokenMetadata` struct. This + allows account data to be stored in a different format while maintaining + compatibility with the Interface standards. + +## Getting Started + +Start by opening this Solana Playground +[link](https://beta.solpg.io/656e19acfb53fa325bfd0c46) with the following +starter code. + +```javascript +// Client +console.log("My address:", pg.wallet.publicKey.toString()); +const balance = await pg.connection.getBalance(pg.wallet.publicKey); +console.log(`My balance: ${balance / web3.LAMPORTS_PER_SOL} SOL`); +``` + +If it is your first time using Solana Playground, you'll first need to create a +Playground Wallet and fund the wallet with devnet SOL. + + + +If you do not have a Playground wallet, you may see a type error within the +editor on all declarations of `pg.wallet.publicKey`. This type error will clear +after you create a Playground wallet. + + + +To get devnet SOL, run the `solana airdrop` command in the Playground's +terminal, or visit this [devnet faucet](https://faucet.solana.com/). + +``` +solana airdrop 5 +``` + +Once you've created and funded the Playground wallet, click the "Run" button to +run the starter code. + +## Add Dependencies + +Let's start by setting up our script. We'll be using the `@solana/web3.js`, +`@solana/spl-token`, and `@solana/spl-token-metadata` libraries. + +Replace the starter code with the following: + +```javascript +import { + Connection, + Keypair, + SystemProgram, + Transaction, + clusterApiUrl, + sendAndConfirmTransaction, +} from "@solana/web3.js"; +import { + ExtensionType, + TOKEN_2022_PROGRAM_ID, + createInitializeMintInstruction, + getMintLen, + createInitializeMetadataPointerInstruction, + getMint, + getMetadataPointerState, + getTokenMetadata, + TYPE_SIZE, + LENGTH_SIZE, +} from "@solana/spl-token"; +import { + createInitializeInstruction, + createUpdateFieldInstruction, + createRemoveKeyInstruction, + pack, + TokenMetadata, +} from "@solana/spl-token-metadata"; + +// Playground wallet +const payer = pg.wallet.keypair; + +// Connection to devnet cluster +const connection = new Connection(clusterApiUrl("devnet"), "confirmed"); + +// Transaction to send +let transaction: Transaction; +// Transaction signature returned from sent transaction +let transactionSignature: string; +``` + +## Mint Setup + +Next, define the properties of the Mint Account we'll be creating in the +following step. + +```javascript +// Generate new keypair for Mint Account +const mintKeypair = Keypair.generate(); +// Address for Mint Account +const mint = mintKeypair.publicKey; +// Decimals for Mint Account +const decimals = 2; +// Authority that can mint new tokens +const mintAuthority = pg.wallet.publicKey; +// Authority that can update the metadata pointer and token metadata +const updateAuthority = pg.wallet.publicKey; + +// Metadata to store in Mint Account +const metaData: TokenMetadata = { + updateAuthority: updateAuthority, + mint: mint, + name: "OPOS", + symbol: "OPOS", + uri: "https://raw.githubusercontent.com/solana-developers/opos-asset/main/assets/DeveloperPortal/metadata.json", + additionalMetadata: [["description", "Only Possible On Solana"]], +}; +``` + +Next, determine the size of the new Mint Account and calculate the minimum +lamports needed for rent exemption. + +In the code snippet below, we allocate 4 bytes for the `TokenMetadata` extension +and then calculate the space required by the metadata. + +```javascript +// Size of MetadataExtension 2 bytes for type, 2 bytes for length +const metadataExtension = TYPE_SIZE + LENGTH_SIZE; +// Size of metadata +const metadataLen = pack(metaData).length; + +// Size of Mint Account with extension +const mintLen = getMintLen([ExtensionType.MetadataPointer]); + +// Minimum lamports required for Mint Account +const lamports = await connection.getMinimumBalanceForRentExemption( + mintLen + metadataExtension + metadataLen, +); +``` + +With Token Extensions, the size of the Mint Account will vary based on the +extensions enabled. + +## Build Instructions + +Next, let's build the set of instructions to: + +- Create a new account +- Initialize the `MetadataPointer` extension +- Initialize the remaining Mint Account data +- Initialize the `TokenMetadata` extension and token metadata +- Update the token metadata with a custom field + +First, build the instruction to invoke the System Program to create an account +and assign ownership to the Token Extensions Program. + +```js +// Instruction to invoke System Program to create new account +const createAccountInstruction = SystemProgram.createAccount({ + fromPubkey: payer.publicKey, // Account that will transfer lamports to created account + newAccountPubkey: mint, // Address of the account to create + space: mintLen, // Amount of bytes to allocate to the created account + lamports, // Amount of lamports transferred to created account + programId: TOKEN_2022_PROGRAM_ID, // Program assigned as owner of created account +}); +``` + +Next, build the instruction to initialize the `MetadataPointer` extension for +the Mint Account. In this example, the metadata pointer will point to the Mint +address, indicating that the metadata will be stored directly on the Mint +Account. + +```js +// Instruction to initialize the MetadataPointer Extension +const initializeMetadataPointerInstruction = + createInitializeMetadataPointerInstruction( + mint, // Mint Account address + updateAuthority, // Authority that can set the metadata address + mint, // Account address that holds the metadata + TOKEN_2022_PROGRAM_ID, + ); +``` + +Next, build the instruction to initialize the rest of the Mint Account data. +This is the same as with the original Token Program. + +```js +// Instruction to initialize Mint Account data +const initializeMintInstruction = createInitializeMintInstruction( + mint, // Mint Account Address + decimals, // Decimals of Mint + mintAuthority, // Designated Mint Authority + null, // Optional Freeze Authority + TOKEN_2022_PROGRAM_ID, // Token Extension Program ID +); +``` + +Next, build the instruction to initialize the `TokenMetadata` extension and the +required metadata fields (name, symbol, URI). + +For this instruction, use the Token Extensions Program as the `programId`, which +functions as the "Metadata Program". Additionally, the Mint Account's address is +used as the `metadata` to indicate that the Mint itself is the "Metadata +Account". + +```js +// Instruction to initialize Metadata Account data +const initializeMetadataInstruction = createInitializeInstruction({ + programId: TOKEN_2022_PROGRAM_ID, // Token Extension Program as Metadata Program + metadata: mint, // Account address that holds the metadata + updateAuthority: updateAuthority, // Authority that can update the metadata + mint: mint, // Mint Account address + mintAuthority: mintAuthority, // Designated Mint Authority + name: metaData.name, + symbol: metaData.symbol, + uri: metaData.uri, +}); +``` + +Next, build the instruction to update the metadata with a custom field using the +`UpdateField` instruction from the Token Metadata Interface. + +This instruction will either update the value of an existing field or add it to +`additional_metadata` if it does not already exist. Note that you may need to +reallocate more space to the account to accommodate the additional data. In this +example, we allocated all the lamports required for rent up front when creating +the account. + +```js +// Instruction to update metadata, adding custom field +const updateFieldInstruction = createUpdateFieldInstruction({ + programId: TOKEN_2022_PROGRAM_ID, // Token Extension Program as Metadata Program + metadata: mint, // Account address that holds the metadata + updateAuthority: updateAuthority, // Authority that can update the metadata + field: metaData.additionalMetadata[0][0], // key + value: metaData.additionalMetadata[0][1], // value +}); +``` + +## Send Transaction + +Next, add the instructions to a new transaction and send it to the network. This +will create a Mint Account with the `MetadataPointer` and `TokenMetadata` +extensions enabled and store the metadata on the Mint Account. + + +Some token extension instructions are required to be atomically ordered before +initializing the mint. While others must be after. Having these instructions +"out of order" may result in your transaction failing. + + +```javascript +// Add instructions to new transaction +transaction = new Transaction().add( + createAccountInstruction, + initializeMetadataPointerInstruction, + // note: the above instructions are required before initializing the mint + initializeMintInstruction, + initializeMetadataInstruction, + updateFieldInstruction, +); + +// Send transaction +transactionSignature = await sendAndConfirmTransaction( + connection, + transaction, + [payer, mintKeypair], // Signers +); + +console.log( + "\nCreate Mint Account:", + `https://solana.fm/tx/${transactionSignature}?cluster=devnet-solana`, +); +``` + +## Read Metadata from Mint Account + +Next, check that the metadata has been stored on the Mint Account. + +Start by fetching the Mint Account and reading the `MetadataPointer` extension +portion of the account data: + +```js +// Retrieve mint information +const mintInfo = await getMint( + connection, + mint, + "confirmed", + TOKEN_2022_PROGRAM_ID, +); + +// Retrieve and log the metadata pointer state +const metadataPointer = getMetadataPointerState(mintInfo); +console.log("\nMetadata Pointer:", JSON.stringify(metadataPointer, null, 2)); +``` + +Next, read the Metadata portion of the account data: + +```js +// Retrieve and log the metadata state +const metadata = await getTokenMetadata( + connection, + mint, // Mint Account address +); +console.log("\nMetadata:", JSON.stringify(metadata, null, 2)); +``` + +Run the script by clicking the `Run` button. You can then inspect the +transaction details on SolanaFM. + +You should also see console output similar to the following: + +``` +Metadata Pointer: { + "authority": "3z9vL1zjN6qyAFHhHQdWYRTFAcy69pJydkZmSFBKHg1R", + "metadataAddress": "BFqmKEm12CrDbcFAncjL34Anu5w18LruxQrgvy7aExzV" +} + +Metadata: { + "updateAuthority": "3z9vL1zjN6qyAFHhHQdWYRTFAcy69pJydkZmSFBKHg1R", + "mint": "BFqmKEm12CrDbcFAncjL34Anu5w18LruxQrgvy7aExzV", + "name": "OPOS", + "symbol": "OPOS", + "uri": "https://raw.githubusercontent.com/solana-developers/opos-asset/main/assets/DeveloperPortal/metadata.json", + "additionalMetadata": [ + [ + "description", + "Only Possible On Solana" + ] + ] +} +``` + +## Remove Custom Field + +To delete a custom field from the metadata, use the `RemoveKey` instruction from +the Token Metadata Interface. + + + +The `idempotent` flag is used to specify whether the transaction should fail if +the key does not exist on the account. If the idempotent flag is set to `true`, +then the instruction will not error if the key does not exist. + + + +```js +// Instruction to remove a key from the metadata +const removeKeyInstruction = createRemoveKeyInstruction({ + programId: TOKEN_2022_PROGRAM_ID, // Token Extension Program as Metadata Program + metadata: mint, // Address of the metadata + updateAuthority: updateAuthority, // Authority that can update the metadata + key: metaData.additionalMetadata[0][0], // Key to remove from the metadata + idempotent: true, // If the idempotent flag is set to true, then the instruction will not error if the key does not exist +}); + +// Add instruction to new transaction +transaction = new Transaction().add(removeKeyInstruction); + +// Send transaction +transactionSignature = await sendAndConfirmTransaction( + connection, + transaction, + [payer], +); + +console.log( + "\nRemove Additional Metadata Field:", + `https://solana.fm/tx/${transactionSignature}?cluster=devnet-solana`, +); + +// Retrieve and log the metadata state +const updatedMetadata = await getTokenMetadata( + connection, + mint, // Mint Account address +); +console.log("\nUpdated Metadata:", JSON.stringify(updatedMetadata, null, 2)); + +console.log( + "\nMint Account:", + `https://solana.fm/address/${mint}?cluster=devnet-solana`, +); +``` + +Run the script by clicking the `Run` button. You can then inspect the +transaction details and Mint Account on SolanaFM. + +You should also see console output similar to the following: + +``` +Updated Metadata: { + "updateAuthority": "Ehqz1TAMboGbY5oBWqKKWmv5hhvQuwcpkaWbVjkU96cZ", + "mint": "9wdvSnsqgYo4HFBYMtiCvVNQfFBYdzSeACjLuxVCDcjB", + "name": "OPOS", + "symbol": "OPOS", + "uri": "https://raw.githubusercontent.com/solana-developers/opos-asset/main/assets/DeveloperPortal/metadata.json", + "additionalMetadata": [] +} +``` + +## Conclusion + +By enabling both the `MetadataPointer` and `TokenMetadata` extensions, the Mint +Account can now directly store token metadata. This feature simplifies the +process of adding metadata to a Mint Account.