A Extensão Metadata
A extensão Metadata é uma extensão de conta Mint que introduz a capacidade de embutir metadata diretamente nas contas mint nativamente e sem precisar usar outro programa.
Inicializando a Conta Mint
A extensão Metadata é um pouco diferente do que estamos acostumados a fazer porque é composta de 2 extensões diferentes que ambas vão em uma conta Mint:
A extensão
Metadataque contém toda a informação de metadata como nome, símbolo, uri e contas adicionais.A extensão
MetadataPointerque referência a contaMintonde a extensãoMetadatareside.
Normalmente, quando usadas, estas 2 extensões residem na mesma conta Mint; e vamos fazer o mesmo para este exemplo.
Vamos começar com alguns conceitos básicos antes de mergulhar no código:
Enquanto a extensão MetadataPointer reside no pacote @solana/spl-token, para inicializar o Metadata precisamos usar o pacote @solana/spl-token-metadata.
Então vamos instalar o pacote necessário:
npm i @solana/spl-token-metadataAdicionalmente, a extensão Metadata é uma das "únicas" extensões que exige que você inicialize a extensão após ter inicializado a conta Mint.
Isso porque a instrução de inicialização de metadata aloca dinamicamente o espaço necessário para o conteúdo de metadata de comprimento variável.
Ao mesmo tempo, isso significa que vamos precisar inicializar a conta Mint com lamports suficientes para ser isenta de aluguel com a extensão Metadata incluída, mas alocando espaço suficiente apenas para a extensão MetadataPointer, já que a instrução token_metadata_initialize() realmente aumenta o espaço corretamente.
No código, isso fica assim:
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"]],
};
// Tamanho da conta Mint com extensões
const mintLen = getMintLen([ExtensionType.MetadataPointer]);
// Tamanho da Extensão Metadata
const metadataLen = TYPE_SIZE + LENGTH_SIZE + pack(metadata).length;
// Lamports mínimos necessários para a conta Mint
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 criada! Veja sua TX aqui: https://explorer.solana.com/tx/${signature}?cluster=devnet`);Atualizando a Metadata
É possível atualizar todos os campos da metadata usando a mesma instrução token_metadata_update_field().
Para o additionalMetadata isso funciona um pouco diferente porque podemos atualizar um campo existente apenas passando o mesmo Field com um novo valor ou simplesmente adicionar um novo campo ao Metadata.
Por baixo dos panos, o programa usa a mesma instrução com flag diferente baseada no que estamos tentando mudar. Isso significa que podemos mudar todos os campos assim:
const newMetadata: TokenMetadata = {
mint: mint.publicKey,
name: "New Name",
symbol: "TST2",
uri: "https://example.com/metadata2.json",
additionalMetadata: [
["customField1", "customValue1"],
["customField2", "customValue2"],
],
};
// Tamanho da conta Mint com extensões
const mintLen = getMintLen([ExtensionType.MetadataPointer]);
// Tamanho da Extensão Metadata
const metadataLen = TYPE_SIZE + LENGTH_SIZE + pack(newMetadata).length;
// Lamports mínimos necessários para a conta Mint
const lamports = await connection.getMinimumBalanceForRentExemption(mintLen + metadataLen);
// Obter o saldo antigo do keypair
const oldBalance = await connection.getBalance(mint.publicKey)
console.log(`Saldo antigo: ${oldBalance}`);
console.log(`Lamports: ${lamports}`);
// Adicionar lamports à Mint se necessário para cobrir a nova isenção de aluguel da metadata
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 adicionados à Mint! Veja sua TX aqui: 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 atualizada! Veja sua TX aqui: https://explorer.solana.com/tx/${signature}?cluster=devnet`);Podemos remover Field na struct additionalMetadata também usando a instrução RemoveKey assim:
const removeMetadataKeyInstructions = createRemoveKeyInstruction({
metadata: mint.publicKey,
updateAuthority: keypair.publicKey,
programId: TOKEN_2022_PROGRAM_ID,
key: "customField", // Field | string
idempotent: true,
});