19 minutos de lectura
Resumen
Las extensiones de tokens (antes conocidas como Token 2022) aportan un nuevo y emocionante conjunto de funcionalidades y opciones de personalización al programa de tokens SPL de Solana. Este programa incluye todas las características de su predecesor (mantiene la compatibilidad con las instrucciones originales de Token y los diseños de cuenta) al tiempo que ofrece nuevas instrucciones y funcionalidades. En esta guía, exploraremos dos nuevas extensiones para crear un NFT dinámico utilizando únicamente el programa de tokens SPL:
- Metadatos, y
- Puntero de metadatos
Tus funciones
- Crea un token de Solana con campos de metadatos personalizados, incluido un campo «puntos»
- Añadir o eliminar campos de metadatos personalizados
- Retirar la facultad de acuñación
- Incrementar el campo de metadatos «puntos» de un token
Lo que necesitarás
- Conocimientos básicos sobre los fundamentos de Solana
- Conocimientos básicos sobre carteras Web3
- Conceptos básicos sobre las extensiones de tokens (¿Qué son las extensiones de tokens de Solana y cómo empezar?)
| Dependencia | Versión |
|---|---|
| node.js | 20.9.0 |
| tsc | 5.0.2 |
| ts-node | 10.9.1 |
| @solana/web3.js | ^1.87.6 |
| @solana/spl-token | ^0.3.11 |
| @solana/spl-token-metadata | ^0.1.2 |
Paso 1: Configurar el entorno
Creemos un nuevo proyecto de Node.js e instalemos la biblioteca Solana-Web3.js. En tu terminal, introduce los siguientes comandos en este orden:
mkdir extension-nft && cd extension-nft
npm init -y # o yarn init -y
npm install @solana/web3.js@1 @solana/spl-token @solana/spl-token-metadata # o bien yarn add @solana/web3.js@1 @solana/spl-token @solana/spl-token-metadata
echo > app.ts
Abre el app.ts Abre el archivo en tu editor favorito y añade las siguientes importaciones:
// Import necessary functions and constants from the Solana web3.js and SPL Token packages
import {
Connection,
Keypair,
SystemProgram,
Transaction,
LAMPORTS_PER_SOL,
sendAndConfirmTransaction,
TransactionSignature,
SignatureStatus,
TransactionConfirmationStatus
} from '@solana/web3.js';
import {
TOKEN_2022_PROGRAM_ID,
createInitializeMintInstruction,
mintTo,
createAssociatedTokenAccountIdempotent,
AuthorityType,
createInitializeMetadataPointerInstruction,
TYPE_SIZE,
LENGTH_SIZE,
getMintLen,
ExtensionType,
getMint,
getMetadataPointerState,
getTokenMetadata,
createSetAuthorityInstruction,
} from '@solana/spl-token';
import {
createInitializeInstruction,
createUpdateFieldInstruction,
createRemoveKeyInstruction,
pack,
TokenMetadata,
} from '@solana/spl-token-metadata';
Importamos las dependencias necesarias desde @solana/web3.js, @solana/spl-token, y @solana/spl-token-metadata bibliotecas. Las utilizaremos para crear, acuñar y recuperar datos de los tokens SPL con extensiones. Aquí hay varios elementos nuevos (entre los que destaca el spl-token-metadata paquete); los iremos analizando sobre la marcha.
A continuación, vamos a establecer una conexión con el clúster local. Si prefieres utilizar devnet o mainnet, es posible que tengas que realizar algunos pequeños ajustes (por ejemplo, el lanzamiento aéreo (La función que aparece a continuación no estará disponible en la red principal y podría estar sujeta a limitaciones de velocidad en la red de desarrollo). También tendrás que cambiar la URL de conexión por el punto final RPC de tu Quicknode (regístrate para crear una cuenta aquí). Además de crear las cuentas de autorización necesarias y definir los metadatos de nuestros tokens, hemos añadido dos funciones auxiliares:
- una para generar una URL de la transacción en Solana FM (que actualmente admite metadatos de extensión de tokens). Esto resultará útil para consultar los detalles de la transacción.
- Alguien que pueda transferir algo de SOL a nuestra cuenta de pagos. Esto es necesario para pagar las comisiones de la transacción y el alquiler de la nueva cuenta.
const connection = new Connection('http://127.0.0.1:8899', 'confirmed');
const payer = Keypair.generate();
const authority = Keypair.generate();
const owner = Keypair.generate();
const mintKeypair = Keypair.generate();
const mint = mintKeypair.publicKey;
const tokenMetadata: TokenMetadata = {
updateAuthority: authority.publicKey,
mint: mint,
name: 'QN Pixel',
symbol: 'QNPIX',
uri: "https://qn-shared.quicknode-ipfs.com/ipfs/QmQFh6WuQaWAMLsw9paLZYvTsdL5xJESzcoSxzb6ZU3Gjx",
additionalMetadata: [["Background", "Blue"], ["WrongData", "DeleteMe!"], ["Points", "0"]],
};
const decimals = 0;
const mintAmount = 1;
function generateExplorerUrl(identifier: string, isAddress: boolean = false): string {
if (!identifier) return '';
const baseUrl = 'https://solana.fm';
const localSuffix = '?cluster=localnet-solana';
const slug = isAddress ? 'address' : 'tx';
return `${baseUrl}/${slug}/${identifier}${localSuffix}`;
}
async function airdropLamports() {
const airdropSignature = await connection.requestAirdrop(payer.publicKey, 2 * LAMPORTS_PER_SOL);
async function confirmTransaction(
connection: Connection,
signature: TransactionSignature,
desiredConfirmationStatus: TransactionConfirmationStatus = 'confirmed',
timeout: number = 30000,
pollInterval: number = 1000,
searchTransactionHistory: boolean = false
): Promise<SignatureStatus> {
const start = Date.now();
while (Date.now() - start < timeout) {
const { value: statuses } = await connection.getSignatureStatuses([signature], { searchTransactionHistory });
if (!statuses || statuses.length === 0) {
throw new Error('Failed to get signature status');
}
const status = statuses[0];
if (status === null) {
await new Promise(resolve => setTimeout(resolve, pollInterval));
continue;
}
if (status.err) {
throw new Error(`Transaction failed: ${JSON.stringify(status.err)}`);
}
if (status.confirmationStatus && status.confirmationStatus === desiredConfirmationStatus) {
return status;
}
if (status.confirmationStatus === 'finalized') {
return status;
}
await new Promise(resolve => setTimeout(resolve, pollInterval));
}
throw new Error(`Transaction confirmation timeout after ${timeout}ms`);
}
await confirmTransaction(connection, airdropSignature);
}
Ten en cuenta que hemos definido un tokenMetadata objeto con los siguientes campos especificados en el Interfaz TokenMetadata:
export interface TokenMetadata {
// The authority that can sign to update the metadata
updateAuthority?: PublicKey;
// The associated mint, used to counter spoofing to be sure that metadata belongs to a particular mint
mint: PublicKey;
// The longer name of the token
name: string;
// The shortened symbol for the token
symbol: string;
// The URI pointing to richer metadata
uri: string;
// Any additional metadata about the token as key-value pairs
additionalMetadata: [string, string][];
}
El metadatos adicionales El campo es una matriz de pares clave-valor que se puede utilizar para almacenar metadatos personalizados. Hemos añadido tres campos:
- «Antecedentes», un campo estático
- «WrongData», un campo que queremos eliminar (a efectos de demostración)
- «Puntos», un campo que modificaremos más adelante
Si prefieres utilizar tus propios metadatos, puedes sustituir el tokenMetadata objeto con tus propios metadatos personalizados. Para subir un .json y subir un archivo de imagen a IPFS, puedes utilizar un Quicknode IPFS Gateway.
Por último, crea una función asíncrona llamada principal y añade el siguiente código:
async function main() {
try {
await airdropLamports();
// 1. Create Token and Mint
const [initSig, mintSig] = await createTokenAndMint();
console.log(`Token created and minted:`);
console.log(` ${generateExplorerUrl(initSig)}`);
console.log(` ${generateExplorerUrl(mintSig)}`);
// 2. Remove Metadata Field
const cleanMetaTxId = await removeMetadataField();
console.log(`Metadata field removed:`);
console.log(` ${generateExplorerUrl(cleanMetaTxId)}`);
// 3. Remove Authority
const removeAuthTxId = await removeTokenAuthority();
console.log(`Authority removed:`);
console.log(` ${generateExplorerUrl(removeAuthTxId)}`);
// 4. Increment Points
const incrementPointsTxId = await incrementPoints(10);
console.log(`Points incremented:`);
console.log(` ${generateExplorerUrl(incrementPointsTxId)}`);
// Log New NFT
console.log(`New NFT:`);
console.log(` ${generateExplorerUrl(mint.toBase58(), true)}`);
} catch (err) {
console.error(err);
}
}
Vamos a describir los pasos que seguiremos para crear, acuñar y gestionar nuestro NFT. Ya hemos añadido el primer paso: recibir un airdrop de SOL en nuestra cuenta de pagador. Vamos a desarrollar los pasos restantes.
Paso 1: Crear un nuevo token
En primer lugar, crearemos nuestro createTokenAndMint función. Esta función creará un nuevo token con las extensiones que hayamos especificado y lo acuñará en una nueva cuenta de tokens asociada, propiedad de la propietario monedero que hemos generado anteriormente. Añade el siguiente código a tu app.ts archivo:
async function createTokenAndMint(): Promise<[string, string]> {
}
Devolveremos una promesa con dos cadenas de caracteres: la firma de la transacción para la inicialización del token y la emisión del token.
Para crear nuestra cuenta de acuñación, tendremos que determinar cuánto espacio necesitará dicha cuenta. Podemos utilizar el getMintLen función para calcular el saldo mínimo de la cuenta de la Casa de la Moneda. getMintLen Aún no admite extensiones de tamaño variable (por ejemplo, Metadatos, que varía en función de los metadatos específicos), por lo que debemos calcular manualmente el saldo mínimo de la cuenta de acuñación sumando el tamaño de dicha cuenta y el de los metadatos. Podemos utilizar el obtenerSaldoMínimoParaExenciónDeAlquiler Función para calcular el saldo mínimo de la cuenta «mint». Añade el siguiente código a tu createTokenAndMint función:
async function createTokenAndMint(): Promise<[string, string]> {
// Calculate the minimum balance for the mint account
const mintLen = getMintLen([ExtensionType.MetadataPointer]);
const metadataLen = TYPE_SIZE + LENGTH_SIZE + pack(tokenMetadata).length;
const mintLamports = await connection.getMinimumBalanceForRentExemption(mintLen + metadataLen);
}
A continuación, vamos a crear nuestra transacción de inicialización de Mint. La transacción deberá realizar varias acciones:
- Crear la cuenta de Mint
- Inicializa la cuenta como una cuenta de puntero de metadatos, lo que permitirá que nuestra cuenta de acuñación especifique la dirección de su cuenta de metadatos correspondiente (en nuestro caso, escribiremos los metadatos en la propia cuenta de acuñación y apuntaremos a ella).
- Inicializar la cuenta como cuenta de Mint
- Inicializar la cuenta con los metadatos del token
- Actualiza los metadatos del token con los campos de metadatos personalizados
⛔️ Si alguno de estos valores es incorrecto, la transacción fallará debido a datos de cuenta erróneos. Asegúrate de comprobar dos veces los parámetros que pasas a estas funciones.
Añade el siguiente código a tu createTokenAndMint función después de la mintLamports declaración:
async function createTokenAndMint(): Promise<[string, string]> {
// After calculating the minimum balance for the mint account...
// Prepare transaction
const transaction = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: mint,
space: mintLen,
lamports: mintLamports,
programId: TOKEN_2022_PROGRAM_ID,
}),
createInitializeMetadataPointerInstruction(
mint,
authority.publicKey,
mint,
TOKEN_2022_PROGRAM_ID,
),
createInitializeMintInstruction(
mint,
decimals,
authority.publicKey,
null,
TOKEN_2022_PROGRAM_ID,
),
createInitializeInstruction({
programId: TOKEN_2022_PROGRAM_ID,
metadata: mint,
updateAuthority: authority.publicKey,
mint: mint,
mintAuthority: authority.publicKey,
name: tokenMetadata.name,
symbol: tokenMetadata.symbol,
uri: tokenMetadata.uri,
}),
createUpdateFieldInstruction({
programId: TOKEN_2022_PROGRAM_ID,
metadata: mint,
updateAuthority: authority.publicKey,
field: tokenMetadata.additionalMetadata[0][0],
value: tokenMetadata.additionalMetadata[0][1],
}),
createUpdateFieldInstruction({
programId: TOKEN_2022_PROGRAM_ID,
metadata: mint,
updateAuthority: authority.publicKey,
field: tokenMetadata.additionalMetadata[1][0],
value: tokenMetadata.additionalMetadata[1][1],
}),
createUpdateFieldInstruction({
programId: TOKEN_2022_PROGRAM_ID,
metadata: mint,
updateAuthority: authority.publicKey,
field: tokenMetadata.additionalMetadata[2][0],
value: tokenMetadata.additionalMetadata[2][1],
}),
);
}
Lo único que estamos haciendo aquí es crear una nueva transacción y añadirle una serie de instrucciones utilizando .add(). Estamos utilizando el helper de la @solana/spl-token y @solana/spl-token-metadata bibliotecas para crear las instrucciones necesarias:
SystemProgram.createAccountpara crear la nueva cuenta con el espacio y los paquetes necesarioscreateInitializeMetadataPointerInstructionfunción para inicializar la cuenta de Mint como puntero de metadatoscreateInitializeMintInstructionfunción para inicializar la cuenta de MintcreateInitializeInstructionfunción para inicializar la cuenta de Mint con los metadatos del tokencreateUpdateFieldInstructionfunción para actualizar los metadatos del token con los campos de metadatos personalizados (ya no es necesario añadir cada campo personalizado por separado)
Ten mucho cuidado con los parámetros que pasas a estas funciones. Quizás te interese consultar las definiciones de TypeScript de estas funciones para asegurarte de que estás pasando los parámetros correctos. Puedes encontrar las definiciones haciendo clic con el botón derecho del ratón sobre la función y seleccionando «Ir a la definición» en tu editor.
Recordatorio: al utilizar extensiones de token, debes pasar el TOKEN_2022_PROGRAM_ID como el programId en cada instrucción.
Por último, enviaremos y confirmaremos la transacción. Añade el siguiente código a tu createTokenAndMint función después de la transacción definición:
async function createTokenAndMint(): Promise<[string, string]> {
// Calculate the minimum balance for the mint account...
// Prepare the transaction...
// Initialize NFT with metadata
const initSig = await sendAndConfirmTransaction(connection, transaction, [payer, mintKeypair, authority]);
// Create associated token account
const sourceAccount = await createAssociatedTokenAccountIdempotent(connection, payer, mint, owner.publicKey, {}, TOKEN_2022_PROGRAM_ID);
// Mint NFT to associated token account
const mintSig = await mintTo(connection, payer, mint, sourceAccount, authority, mintAmount, [], undefined, TOKEN_2022_PROGRAM_ID);
return [initSig, mintSig];
}
En realidad, aquí estamos enviando tres transacciones a la red:
- La primera transacción inicializa la cuenta de la casa de moneda con los campos de metadatos y de metadatos personalizados (utilizando el
transacción(que hemos creado en el paso anterior) - La segunda transacción crea una cuenta de tokens asociada para el propietario del NFT.
- La tercera transacción crea el NFT en la cuenta de tokens asociada.
Por último, nuestra función devuelve las firmas de las transacciones de emisión y de inicialización.
Paso 2: Eliminar un campo de metadatos
¡Vaya! Hemos añadido por error un campo a nuestros metadatos que no queremos. Vamos a eliminarlo. Crearemos una nueva función llamada eliminarCampoDeMetadatos para eliminar el campo «WrongData» de los metadatos de nuestro token. Añade el siguiente código a tu app.ts archivo:
async function removeMetadataField() {
const transaction = new Transaction().add(
createRemoveKeyInstruction({
programId: TOKEN_2022_PROGRAM_ID,
metadata: mint,
updateAuthority: authority.publicKey,
key: 'WrongData',
idempotent: true,
})
);
const signature = await sendAndConfirmTransaction(connection, transaction, [payer, authority]);
return signature;
}
El @solana/spl-token-metadata La biblioteca ofrece un crearEliminarClaveInstrucción función para crear una instrucción que elimine una clave de los metadatos del token. Simplemente creamos una nueva transacción y añadimos la instrucción para eliminar el campo «WrongData» de los metadatos del token. A continuación, enviamos y confirmamos la transacción y devolvemos la firma de la misma.
Paso 3: Eliminar la autorización
Dado que estamos creando un NFT, queremos limitar la oferta a 1 y evitar que se sigan acuñando monedas. Podemos hacerlo retirando la autoridad de acuñación. Crearemos una nueva función llamada eliminarTokenAutoridad para eliminar la autoridad de la cuenta de la casa de la moneda. Añade el siguiente código a tu app.ts archivo:
async function removeTokenAuthority(): Promise<string> {
const transaction = new Transaction().add(
createSetAuthorityInstruction(
mint,
authority.publicKey,
AuthorityType.MintTokens,
null,
[],
TOKEN_2022_PROGRAM_ID
)
);
return await sendAndConfirmTransaction(connection, transaction, [payer, authority]);
}
Estamos utilizando el createSetAuthorityInstruction función de la @solana/spl-token biblioteca para crear una instrucción que establezca la autoridad de acuñación de la cuenta de acuñación en una nueva autoridad (en este caso, pasamos null para eliminar la autorización). A continuación, enviamos y confirmamos la transacción y devolvemos la firma de la misma.
Paso 4: Puntos de incremento
Por último, aumentemos el campo «Puntos» en los metadatos de nuestro token. Crearemos una nueva función llamada puntos de incremento para incrementar el campo «Puntos» en una cantidad determinada. Añade el siguiente código a tu app.ts archivo:
async function incrementPoints(pointsToAdd: number = 1) {
// Retrieve mint information
const mintInfo = await getMint(
connection,
mint,
"confirmed",
TOKEN_2022_PROGRAM_ID,
);
const metadataPointer = getMetadataPointerState(mintInfo);
if (!metadataPointer || !metadataPointer.metadataAddress) {
throw new Error('No metadata pointer found');
}
const metadata = await getTokenMetadata(
connection,
metadataPointer?.metadataAddress,
);
if (!metadata) {
throw new Error('No metadata found');
}
if (metadata.mint.toBase58() !== mint.toBase58()) {
throw new Error('Metadata does not match mint');
}
const [key, currentPoints] = metadata.additionalMetadata.find(([key, _]) => key === 'Points') ?? [];
let pointsAsNumber = parseInt(currentPoints ?? '0');
pointsAsNumber += pointsToAdd;
const transaction = new Transaction().add(
createUpdateFieldInstruction({
programId: TOKEN_2022_PROGRAM_ID,
metadata: mint,
updateAuthority: authority.publicKey,
field: 'Points',
value: pointsAsNumber.toString(),
})
);
return await sendAndConfirmTransaction(connection, transaction, [payer, authority]);
}
En lugar de limitarse a utilizar el createUpdateFieldInstruction función para actualizar el campo «Puntos»; además, vamos a añadir aquí algunas comprobaciones para que te familiarices con algunas de las funciones de obtención de cuentas para tokens SPL con metadatos.
- En primer lugar, utilizamos
getMintpara recuperar la información de la cuenta de Mint - A continuación, utilizamos
getMetadataPointerStatepara obtener el estado del puntero de metadatos (esto incluye nuestra dirección de metadatos) - Utilizamos
getTokenMetadatapara recuperar los metadatos de nuestra cuenta de Mint - A continuación, comprobamos que los metadatos coincidan con la cuenta de Mint.
- Dado que esta función sirve para incrementar los puntos, también nos aseguramos de que el campo «Puntos» exista
- Por último, incrementamos los puntos, enviamos la transacción y la confirmamos.
Como puedes ver en nuestra gestión de los puntos, todos los campos de metadatos personalizados se almacenan como cadenas de caracteres. Esto significa que, si quieres almacenar un número, debes convertir entre los tipos «cadena» y «número» según sea necesario.
Ejecutar el código
Llama a tu principal función al final de tu app.ts archivo:
principal();
Por último, en otra ventana de terminal, ejecuta el siguiente comando para iniciar un clúster local de Solana (omite este paso si estás utilizando otro clúster):
solana-test-validator
Y en tu terminal principal, ejecuta tu script:
ts-node app.ts
Deberías ver un resultado similar al siguiente:
Token creado y acuñado:
https://solana.fm/tx/2HRXAjCt2zuyH3XPD4kKcKvyyqDZ6B98vk6CB8UFNSWhgydR8CNRNqAjNQLnorPhmabszLUgY6Uh5crhSUFYGvB9?cluster=localnet-solana
https://solana.fm/tx/37ngRFFSunK88gZKgTfsc866hqb5e3zXj4z3oeWxAdtsokPpcrwXb56sZwnJ7pS3X7HNbcBCtaaEgWKmDMmGgCks?cluster=localnet-solana
Campo de metadatos eliminado:
https://solana.fm/tx/4TWGd9fDCCTyv39o35BtZyP9kyoYruFCe3XASvmXUpSZpgFGM3So238oQiQAMS14uhxKTDe48V5wujBtzDQFxEuE?cluster=localnet-solana
Autoridad eliminada:
https://solana.fm/tx/Atbt1zwyRmBPijtC423yHTdWaM7ZZqiUoDuMpV5HHT9uHMDqyF4b4Mi4mQP6h9eEie75mew6RfmtbynGMWxyuXb?cluster=localnet-solana
Puntos sumados:
https://solana.fm/tx/4CbUcLk7TPvydRghXgNTR8WpGJU82zeMpJh4ughdYWhbj7BE2krXhhStEu7FzjJzW3GDRfigAWnuEBUjS5XESqBC?cluster=localnet-solana
Nuevo NFT:
https://solana.fm/address/FPyoBNZ24Xv3yu9p5uqsgHXxMXkPMPHGF9pLAWRLFmWF?cluster=localnet-solana
Deberías poder seguir el último enlace hasta la página de tu NFT en Solana FM. Deberías ver la imagen del NFT, los metadatos (sin el campo «WrongData»), que no hay autoridad de acuñación y un valor en puntos de «10»:


Nota: A veces, los datos del NFT no se muestran de inmediato. Si es así, espera unos instantes e intenta actualizar la página.
¡Buen trabajo!
Conclusión
Si lo deseas, puedes encontrar nuestro código completo en el repositorio de GitHub «Quicknode Examples».
Ahora dispones de otra herramienta más en tu arsenal para crear y gestionar NFT en Solana. Para seguir practicando, ponte a prueba añadiendo más funcionalidades a tu NFT. Podrías intentar:
- Cómo vincular el NFT al alma mediante el
No transferibleExtensión - Recaudar los derechos de autor mediante el
TransferHookExtensión
Nos encantaría saber más sobre lo que estás desarrollando y cómo piensas utilizar las extensiones de tokens en tus proyectos. ¡Escríbenos en Discord o síguenos en Twitter para estar al día de las últimas novedades!
¡Nos encanta recibir comentarios!
Si tienes algún comentario o sugerencia sobre nuevos temas, no dudes en hacérnoslo saber. Nos encantaría saber tu opinión.
