소스 검색

anchor token metadata extension example

John 1 년 전
부모
커밋
159244da9c

+ 7 - 0
tokens/token-2022/metadata/anchor/.gitignore

@@ -0,0 +1,7 @@
+.anchor
+.DS_Store
+target
+**/*.rs.bk
+node_modules
+test-ledger
+.yarn

+ 7 - 0
tokens/token-2022/metadata/anchor/.prettierignore

@@ -0,0 +1,7 @@
+.anchor
+.DS_Store
+target
+node_modules
+dist
+build
+test-ledger

+ 18 - 0
tokens/token-2022/metadata/anchor/Anchor.toml

@@ -0,0 +1,18 @@
+[toolchain]
+
+[features]
+resolution = true
+skip-lint = false
+
+[programs.localnet]
+metadata = "BJHEDXSQfD9kBFvhw8ZCGmPFRihzvbMoxoHUKpXdpn4D"
+
+[registry]
+url = "https://api.apr.dev"
+
+[provider]
+cluster = "Localnet"
+wallet = "~/.config/solana/id.json"
+
+[scripts]
+test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

+ 14 - 0
tokens/token-2022/metadata/anchor/Cargo.toml

@@ -0,0 +1,14 @@
+[workspace]
+members = [
+    "programs/*"
+]
+resolver = "2"
+
+[profile.release]
+overflow-checks = true
+lto = "fat"
+codegen-units = 1
+[profile.release.build-override]
+opt-level = 3
+incremental = false
+codegen-units = 1

+ 12 - 0
tokens/token-2022/metadata/anchor/migrations/deploy.ts

@@ -0,0 +1,12 @@
+// Migrations are an early feature. Currently, they're nothing more than this
+// single deploy script that's invoked from the CLI, injecting a provider
+// configured from the workspace's Anchor.toml.
+
+const anchor = require("@coral-xyz/anchor");
+
+module.exports = async function (provider) {
+  // Configure client to use the provider.
+  anchor.setProvider(provider);
+
+  // Add your deploy script here.
+};

+ 20 - 0
tokens/token-2022/metadata/anchor/package.json

@@ -0,0 +1,20 @@
+{
+  "scripts": {
+    "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w",
+    "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check"
+  },
+  "dependencies": {
+    "@coral-xyz/anchor": "^0.30.0",
+    "@solana/spl-token-metadata": "^0.1.4"
+  },
+  "devDependencies": {
+    "@types/bn.js": "^5.1.0",
+    "@types/chai": "^4.3.0",
+    "@types/mocha": "^9.0.0",
+    "chai": "^4.3.4",
+    "mocha": "^9.0.3",
+    "prettier": "^2.6.2",
+    "ts-mocha": "^10.0.0",
+    "typescript": "^4.3.5"
+  }
+}

+ 24 - 0
tokens/token-2022/metadata/anchor/programs/metadata/Cargo.toml

@@ -0,0 +1,24 @@
+[package]
+name = "metadata"
+version = "0.1.0"
+description = "Created with Anchor"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib", "lib"]
+name = "metadata"
+
+[features]
+default = []
+cpi = ["no-entrypoint"]
+no-entrypoint = []
+no-idl = []
+no-log-ix-name = []
+idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"]
+
+[dependencies]
+anchor-lang = "0.30.0"
+anchor-spl = "0.30.0"
+spl-token-metadata-interface = "0.3.3"
+spl-type-length-value = "0.4.3"
+

+ 2 - 0
tokens/token-2022/metadata/anchor/programs/metadata/Xargo.toml

@@ -0,0 +1,2 @@
+[target.bpfel-unknown-unknown.dependencies.std]
+features = []

+ 28 - 0
tokens/token-2022/metadata/anchor/programs/metadata/src/instructions/emit.rs

@@ -0,0 +1,28 @@
+use anchor_lang::prelude::*;
+use anchor_lang::solana_program::program::invoke;
+use anchor_spl::token_interface::{Mint, Token2022};
+use spl_token_metadata_interface::instruction::emit;
+
+#[derive(Accounts)]
+pub struct Emit<'info> {
+    pub mint_account: InterfaceAccount<'info, Mint>,
+    pub token_program: Program<'info, Token2022>,
+}
+
+// Invoke the emit instruction from spl_token_metadata_interface directly
+// There is not an anchor CpiContext for this instruction
+pub fn process_emit(ctx: Context<Emit>) -> Result<()> {
+    invoke(
+        &emit(
+            &ctx.accounts.token_program.key(), // token program id
+            &ctx.accounts.mint_account.key(),  // "metadata" account
+            None,
+            None,
+        ),
+        &[
+            ctx.accounts.token_program.to_account_info(),
+            ctx.accounts.mint_account.to_account_info(),
+        ],
+    )?;
+    Ok(())
+}

+ 84 - 0
tokens/token-2022/metadata/anchor/programs/metadata/src/instructions/initialize.rs

@@ -0,0 +1,84 @@
+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 type, 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,
+}

+ 10 - 0
tokens/token-2022/metadata/anchor/programs/metadata/src/instructions/mod.rs

@@ -0,0 +1,10 @@
+pub use initialize::*;
+pub mod initialize;
+pub use update_field::*;
+pub mod update_field;
+pub use remove_key::*;
+pub mod remove_key;
+pub use emit::*;
+pub mod emit;
+pub use update_authority::*;
+pub mod update_authority;

+ 38 - 0
tokens/token-2022/metadata/anchor/programs/metadata/src/instructions/remove_key.rs

@@ -0,0 +1,38 @@
+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(())
+}

+ 44 - 0
tokens/token-2022/metadata/anchor/programs/metadata/src/instructions/update_authority.rs

@@ -0,0 +1,44 @@
+use anchor_lang::prelude::*;
+use anchor_spl::token_interface::{
+    spl_pod::optional_keys::OptionalNonZeroPubkey, token_metadata_update_authority, Mint,
+    Token2022, TokenMetadataUpdateAuthority,
+};
+
+#[derive(Accounts)]
+pub struct UpdateAuthority<'info> {
+    pub current_authority: Signer<'info>,
+    pub new_authority: Option<UncheckedAccount<'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_authority(ctx: Context<UpdateAuthority>) -> Result<()> {
+    let new_authority_key = match &ctx.accounts.new_authority {
+        Some(account) => OptionalNonZeroPubkey::try_from(Some(account.key()))?,
+        None => OptionalNonZeroPubkey::try_from(None)?,
+    };
+
+    // Change update authority
+    token_metadata_update_authority(
+        CpiContext::new(
+            ctx.accounts.token_program.to_account_info(),
+            TokenMetadataUpdateAuthority {
+                token_program_id: ctx.accounts.token_program.to_account_info(),
+                metadata: ctx.accounts.mint_account.to_account_info(),
+                current_authority: ctx.accounts.current_authority.to_account_info(),
+
+                // new authority isn't actually needed as account in the CPI
+                // using current_authority as a placeholder to satisfy the struct
+                new_authority: ctx.accounts.current_authority.to_account_info(),
+            },
+        ),
+        new_authority_key,
+    )?;
+    Ok(())
+}

+ 127 - 0
tokens/token-2022/metadata/anchor/programs/metadata/src/instructions/update_field.rs

@@ -0,0 +1,127 @@
+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()),
+        }
+    }
+}

+ 31 - 0
tokens/token-2022/metadata/anchor/programs/metadata/src/lib.rs

@@ -0,0 +1,31 @@
+use anchor_lang::prelude::*;
+
+use instructions::*;
+mod instructions;
+
+declare_id!("BJHEDXSQfD9kBFvhw8ZCGmPFRihzvbMoxoHUKpXdpn4D");
+
+#[program]
+pub mod metadata {
+    use super::*;
+
+    pub fn initialize(ctx: Context<Initialize>, args: TokenMetadataArgs) -> Result<()> {
+        process_initialize(ctx, args)
+    }
+
+    pub fn update_field(ctx: Context<UpdateField>, args: UpdateFieldArgs) -> Result<()> {
+        process_update_field(ctx, args)
+    }
+
+    pub fn remove_key(ctx: Context<RemoveKey>, key: String) -> Result<()> {
+        process_remove_key(ctx, key)
+    }
+
+    pub fn emit(ctx: Context<Emit>) -> Result<()> {
+        process_emit(ctx)
+    }
+
+    pub fn update_authority(ctx: Context<UpdateAuthority>) -> Result<()> {
+        process_update_authority(ctx)
+    }
+}

+ 117 - 0
tokens/token-2022/metadata/anchor/tests/metadata.ts

@@ -0,0 +1,117 @@
+import * as anchor from "@coral-xyz/anchor";
+import { Program } from "@coral-xyz/anchor";
+import { Metadata } from "../target/types/metadata";
+import { unpack } from "@solana/spl-token-metadata";
+
+describe("metadata", () => {
+  const provider = anchor.AnchorProvider.env();
+  anchor.setProvider(provider);
+
+  const program = anchor.workspace.Metadata as Program<Metadata>;
+
+  const mintKeypair = new anchor.web3.Keypair();
+
+  const metadata = {
+    name: "OPOS",
+    symbol: "OPOS",
+    uri: "https://raw.githubusercontent.com/solana-developers/opos-asset/main/assets/DeveloperPortal/metadata.json",
+  };
+
+  it("Create Mint with MetadataPointer and TokenMetadata Extensions", async () => {
+    const tx = await program.methods
+      .initialize(metadata)
+      .accounts({ mintAccount: mintKeypair.publicKey })
+      .signers([mintKeypair])
+      .rpc({ skipPreflight: true });
+    console.log("Your transaction signature", tx);
+  });
+
+  it("Update existing metadata field", async () => {
+    // Add your test here.
+    const tx = await program.methods
+      .updateField({
+        field: { name: {} }, // Update the name field
+        value: "Solana",
+      })
+      .accounts({ mintAccount: mintKeypair.publicKey })
+      .rpc({ skipPreflight: true });
+    console.log("Your transaction signature", tx);
+  });
+
+  it("Update metadata with custom field", async () => {
+    const tx = await program.methods
+      .updateField({
+        field: { key: { 0: "color" } }, // Add a custom field named "color"
+        value: "red",
+      })
+      .accounts({ mintAccount: mintKeypair.publicKey })
+      .rpc({ skipPreflight: true });
+    console.log("Your transaction signature", tx);
+  });
+
+  it("Remove custom field", async () => {
+    const tx = await program.methods
+      .removeKey("color") // Remove the custom field named "color"
+      .accounts({ mintAccount: mintKeypair.publicKey })
+      .rpc({ skipPreflight: true });
+    console.log("Your transaction signature", tx);
+  });
+
+  it("Change update authority", async () => {
+    const tx = await program.methods
+      .updateAuthority()
+      .accounts({
+        mintAccount: mintKeypair.publicKey,
+        newAuthority: null, // Set the update authority to null
+      })
+      .rpc({ skipPreflight: true });
+    console.log("Your transaction signature", tx);
+  });
+
+  it("Emit metadata, decode transaction logs", async () => {
+    const txSignature = await program.methods
+      .emit()
+      .accounts({ mintAccount: mintKeypair.publicKey })
+      .rpc({ commitment: "confirmed", skipPreflight: true });
+    console.log("Your transaction signature", txSignature);
+
+    // Fetch the transaction response
+    const transactionResponse = await provider.connection.getTransaction(
+      txSignature,
+      {
+        commitment: "confirmed",
+      }
+    );
+
+    // Extract the log message that starts with "Program return:"
+    const prefix = "Program return: ";
+    let log = transactionResponse.meta.logMessages.find((log) =>
+      log.startsWith(prefix)
+    );
+    log = log.slice(prefix.length);
+    const [_, data] = log.split(" ", 2);
+
+    // Decode the data from base64 and unpack it into TokenMetadata
+    const buffer = Buffer.from(data, "base64");
+    const metadata = unpack(buffer);
+    console.log("Metadata", metadata);
+  });
+
+  it("Emit metadata, decode simulated transaction", async () => {
+    const simulateResponse = await program.methods
+      .emit()
+      .accounts({ mintAccount: mintKeypair.publicKey })
+      .simulate();
+
+    // Extract the log message that starts with "Program return:"
+    const prefix = "Program return: ";
+    let log = simulateResponse.raw.find((log) => log.startsWith(prefix));
+    log = log.slice(prefix.length);
+    const [_, data] = log.split(" ", 2);
+
+    // Decode the data from base64 and unpack it into TokenMetadata
+    const buffer = Buffer.from(data, "base64");
+    const metadata = unpack(buffer);
+    console.log("Metadata", metadata);
+  });
+});

+ 10 - 0
tokens/token-2022/metadata/anchor/tsconfig.json

@@ -0,0 +1,10 @@
+{
+  "compilerOptions": {
+    "types": ["mocha", "chai"],
+    "typeRoots": ["./node_modules/@types"],
+    "lib": ["es2015"],
+    "module": "commonjs",
+    "target": "es6",
+    "esModuleInterop": true
+  }
+}

+ 3 - 2
tokens/token-2022/transfer-fees/anchor/programs/transfer-fee/src/lib.rs

@@ -1,6 +1,7 @@
 use anchor_lang::prelude::*;
-pub mod instructions;
-pub use instructions::*;
+
+mod instructions;
+use instructions::*;
 
 declare_id!("4evptdGtALCNT8uTxJhbWBRZpBE8w5oNtmgfSyfQu7td");