Typescript
使用 Web3.js 的 Token2022

使用 Web3.js 的 Token2022

元数据扩展

Metadata 扩展是一种 Mint 账户扩展,它引入了将元数据直接嵌入到 mint account 中的能力,无需使用其他程序。

初始化 mint account

Metadata 扩展与我们习惯的方式略有不同,因为它由两个不同的扩展组成,这两个扩展都应用于 Mint 账户:

  • 包含所有元数据信息(如名称、符号、URI 和附加账户)的 Metadata 扩展。
  • 引用 Mint 账户的 MetadataPointer 扩展,其中 Metadata 扩展存在。

通常情况下,当使用时,这两个扩展都存在于同一个 Mint 账户中;在本示例中我们也将这样做。

在深入代码之前,让我们先了解一些基础知识:

虽然 MetadataPointer 扩展位于 anchor-spl crate 中,但要初始化 Metadata,我们需要使用 spl_token_metadata_interface crate。

因此,让我们安装所需的包:

 
npm i @solana/spl-token-metadata

此外,Metadata 扩展是少数需要在初始化 Mint 账户后再初始化扩展的扩展之一。

这是因为元数据初始化指令会动态分配可变长度元数据内容所需的空间。

同时,这意味着我们需要为 Mint 账户分配足够的 lamport,以使其在包含 Metadata 扩展时免租金,但仅为 MetadataPointer 扩展分配足够的空间,因为 token_metadata_initialize() 指令实际上会正确增加空间。

代码中看起来是这样的:

ts
import {
    Keypair,
    SystemProgram,
    Transaction,
    sendAndConfirmTransaction,
} from '@solana/web3.js';
import {
    createInitializeMintInstruction,
    TYPE_SIZE,
    LENGTH_SIZE,
    createInitializeMetadataPointerInstruction,
    getMintLen,
    ExtensionType,
    TOKEN_2022_PROGRAM_ID,
} from '@solana/spl-token';
 
import { 
    createInitializeInstruction, 
    pack, 
    TokenMetadata 
} from "@solana/spl-token-metadata";
 
const mint = Keypair.generate();
 
const metadata: TokenMetadata = {
    mint: mint.publicKey,
    name: "Test Token",
    symbol: "TST",
    uri: "https://example.com/metadata.json",
    additionalMetadata: [["customField", "customValue"]],
};
 
// Size of Mint Account with extensions
const mintLen = getMintLen([ExtensionType.MetadataPointer]);
 
// Size of the Metadata Extension
const metadataLen = TYPE_SIZE + LENGTH_SIZE + pack(metadata).length;
 
// Minimum lamports required for Mint Account
const lamports = await connection.getMinimumBalanceForRentExemption(mintLen + metadataLen);
 
const createAccountInstruction = SystemProgram.createAccount({
    fromPubkey: keypair.publicKey,
    newAccountPubkey: mint.publicKey,
    space: mintLen,
    lamports,
    programId: TOKEN_2022_PROGRAM_ID,
});
 
const initializeMetadataPointer = createInitializeMetadataPointerInstruction(
    mint.publicKey,
    keypair.publicKey,
    mint.publicKey,
    TOKEN_2022_PROGRAM_ID,
);
 
const initializeMintInstruction = createInitializeMintInstruction(
    mint.publicKey,
    6,
    keypair.publicKey,
    null,
    TOKEN_2022_PROGRAM_ID,
);
 
const initializeMetadataInstruction = createInitializeInstruction(
    {
        programId: TOKEN_2022_PROGRAM_ID,
        mint: mint.publicKey,
        metadata: mint.publicKey,
        name: metadata.name,
        symbol: metadata.symbol,
        uri: metadata.uri,
        mintAuthority: keypair.publicKey,
        updateAuthority: keypair.publicKey,
    }
);
 
const updateMetadataFieldInstructions = createUpdateFieldInstruction({
    metadata: mint.publicKey,
    updateAuthority: keypair.publicKey,
    programId: TOKEN_2022_PROGRAM_ID,
    field: metadata.additionalMetadata[0][0],
    value: metadata.additionalMetadata[0][1],
    });
 
const transaction = new Transaction().add(
    createAccountInstruction,
    initializeMetadataPointer,
    initializeMintInstruction,
    initializeMetadataInstruction,
    updateMetadataFieldInstructions,
);
 
const signature = await sendAndConfirmTransaction(connection, transaction, [keypair, mint]);
 
console.log(`Mint created! Check out your TX here: https://explorer.solana.com/tx/${signature}?cluster=devnet`);

initialize 指令对于 metadata-interface 不允许 additionalMetdata,因此如果我们想创建一个包含它的 asset,我们需要使用将在下一节中看到的 token_metadata_update_field() 指令。

更新元数据

可以使用相同的 token_metadata_update_field() 指令更新元数据的所有字段。

对于 additionalMetadata,操作稍有不同,因为我们可以通过传递相同的 Field 并赋予新值来更新现有字段,或者直接向 Metadata 添加新字段。

在底层,程序根据我们试图更改的内容使用相同的指令但设置了不同的标志,这意味着我们可以像这样更改所有字段:

ts
const newMetadata: TokenMetadata = {
    mint: mint.publicKey,
    name: "New Name",
    symbol: "TST2",
    uri: "https://example.com/metadata2.json",
    additionalMetadata: [
        ["customField1", "customValue1"],
        ["customField2", "customValue2"],
    ],
};
 
// Size of Mint Account with extensions
const mintLen = getMintLen([ExtensionType.MetadataPointer]);
 
// Size of the Metadata Extension
const metadataLen = TYPE_SIZE + LENGTH_SIZE + pack(newMetadata).length;
 
// Minimum lamports required for Mint Account
const lamports = await connection.getMinimumBalanceForRentExemption(mintLen + metadataLen);
 
// Get the old balance of the keypair
const oldBalance = await connection.getBalance(mint.publicKey)
 
console.log(`Old balance: ${oldBalance}`);
console.log(`Lamports: ${lamports}`);
 
// Add lamports to the Mint if needed to cover the new metadata rent exemption
if (oldBalance < lamports) {
    const transferInstruction = SystemProgram.transfer({
        fromPubkey: keypair.publicKey,
        toPubkey: mint.publicKey,
        lamports: lamports - oldBalance,
    });
 
    const transaction = new Transaction().add(transferInstruction);
 
    const signature = await sendAndConfirmTransaction(connection, transaction, [keypair], {commitment: "finalized"});
 
    console.log(`Lamports added to Mint! Check out your TX here: https://explorer.solana.com/tx/${signature}?cluster=devnet`);
}
 
const updateMetadataNameInstructions = createUpdateFieldInstruction({
    metadata: mint.publicKey,
    updateAuthority: keypair.publicKey,
    programId: TOKEN_2022_PROGRAM_ID,
    field: "Name", // Field | string
    value: "New Name",
});
 
const updateMetadataSymbolInstructions = createUpdateFieldInstruction({
    metadata: mint.publicKey,
    updateAuthority: keypair.publicKey,
    programId: TOKEN_2022_PROGRAM_ID,
    field: "Symbol", // Field | string
    value: "TST2",
});
 
const updateMetadataUriInstructions = createUpdateFieldInstruction({
    metadata: mint.publicKey,
    updateAuthority: keypair.publicKey,
    programId: TOKEN_2022_PROGRAM_ID,
    field: "Uri", // Field | string
    value: "https://example.com/metadata2.json",
});
 
const updateMetadataAdditionalMetadataInstructions = createUpdateFieldInstruction({
    metadata: mint.publicKey,
    updateAuthority: keypair.publicKey,
    programId: TOKEN_2022_PROGRAM_ID,
    field: "customField2", // Field | string
    value: "customValue2",
});
 
const transaction = new Transaction().add(
    updateMetadataNameInstructions,
    updateMetadataSymbolInstructions,
    updateMetadataUriInstructions,
    updateMetadataAdditionalMetadataInstructions,
);
 
const signature = await sendAndConfirmTransaction(connection, transaction, [keypair]);
 
console.log(`Metadata updated! Check out your TX here: https://explorer.solana.com/tx/${signature}?cluster=devnet`);

As you can see we always need to make sure that the account is rent exempt this is why we make all the calculation about rent at the start and transfer additional lamports if needed.

We can remove Field in the additionalMetadata struct as well using the RemoveKey instruction like so:

ts
const removeMetadataKeyInstructions = createRemoveKeyInstruction({
    metadata: mint.publicKey,
    updateAuthority: keypair.publicKey,
    programId: TOKEN_2022_PROGRAM_ID,
    key: "customField", // Field | string
    idempotent: true,
});
Blueshift © 2025Commit: 0ce3b0d