Introdujimos el estándar de metadatos de Metaplex en el tutorial anterior. En este, crearemos un token SPL y le adjuntaremos metadatos utilizando el estándar de Metaplex.
Construiremos un programa Anchor que crea tokens SPL con metadatos adjuntos utilizando el estándar de Metaplex. Esto nos permite añadir información a nuestros tokens, como nombres, símbolos, imágenes y otros atributos.
Antes de comenzar a construir, entendamos los estándares de Metaplex que rigen cómo deben estructurarse los metadatos de los tokens.
Estándares de tokens de Metaplex y formatos de URI
Cuando creamos metadatos para nuestros tokens, necesitamos seguir formatos JSON específicos definidos por Metaplex. La estructura depende del tipo de token que estemos creando (NFT, token fungible, etc.).
Existen tres estándares principales:
Fungible Standard (cuenta de metadatos token_standard = 2)
Este es tu token SPL regular con metadatos. Este es el ejemplo que crearemos más adelante en este artículo.
Su esquema JSON de metadatos se define de la siguiente manera:
{
"name": "Example Token",
"symbol": "EXT",
"description": "A basic fungible SPL token with minimal metadata.",
"image": "https://example.com/images/ext-logo.png"
}
Fungible Asset Standard (token_standard = 1)
Esto es análogo al ERC-1155 en Ethereum, utilizado para moneda o artículos dentro del juego. Se define como un token SPL fungible con un suministro mayor a 1 pero con cero decimales (es decir, sin unidades fraccionarias).
Su esquema JSON incluye algunos campos adicionales, como attributes:
{
"name": "Game Sword",
"description": "A rare in-game sword used in the battle arena.",
"image": "https://example.com/images/sword.png",
"animation_url": "https://example.com/animations/sword-spin.mp4",
"external_url": "https://game.example.com/item/1234",
"attributes": [
{ "trait_type": "Damage", "value": "12" },
{ "trait_type": "Durability", "value": "50" }
],
"properties": {
"files": [
{
"uri": "https://example.com/images/sword.png",
"type": "image/png"
}
],
"category": "image"
}
}
Non-Fungible Standard (token_standard = 0)
Esto es análogo al ERC-721 en Ethereum — representa un Token No Fungible (NFT). Sin embargo, en Solana, cada NFT es un mint separado con un suministro de 1 y 0 decimales, mientras que en Ethereum, ERC-721 utiliza IDs de token únicos dentro de un solo contrato.
El esquema JSON para el Non-Fungible Standard es idéntico al estándar Fungible Asset anterior. Ambos estándares utilizan exactamente la misma estructura de metadatos — la distinción es solo on-chain (suministro y decimales), no en el formato JSON.
{
"name": "Rare Art Piece",
"description": "A one-of-one digital artwork by Artist X.",
"image": "https://example.com/images/artwork.png",
"animation_url": "https://example.com/animations/artwork-loop.mp4",
"external_url": "https://artistx.example.com/rare-art-piece",
"attributes": [
{ "trait_type": "Artist", "value": "Artist X" },
{ "trait_type": "Year", "value": "2025" }
],
"properties": {
"files": [
{
"uri": "https://example.com/images/artwork.png",
"type": "image/png"
}
],
"category": "image"
}
}
Nota: En Ethereum, una colección de NFTs generalmente vive en un solo contrato que emite y gestiona muchos NFTs. En Solana, cada NFT es su propio mint, y las colecciones se forman mediante un enlace on-chain verificado en los metadatos de Metaplex (el campo collection que cubrimos en el tutorial anterior), no mediante un único contrato.
Ahora que entendemos estos estándares, construyamos nuestro programa para crear un token fungible con metadatos.
Implementando un token de Fungible Standard
Esto es lo que lograremos:
- Crearemos un programa Anchor con una función para adjuntar metadatos a tokens SPL
- Crear un token SPL (cuenta mint)
- Usar el Metaplex Token Metadata Program vía CPI para crear una cuenta de metadatos y vincularla al token
- Almacenar el URI del token y su contenido (como la imagen del token) en un almacenamiento permanente, al cual hará referencia la cuenta de metadatos del token
Configuración del proyecto
Primero, crea un nuevo proyecto de Anchor con anchor init spl_with_metadata.
Luego, actualiza el archivo Anchor.toml con lo siguiente para configurar nuestro proyecto correctamente. Estableceremos el clúster en ‘devnet’ ya que necesitamos interactuar con el Metaplex Token Metadata Program real, el cual no existe en nuestro entorno local. También añadiremos las dependencias necesarias para trabajar con tokens SPL y metadatos:
[toolchain]
package_manager = "yarn"
[features]
resolution = true
skip-lint = false
[programs.localnet]
spl_token_with_metadata = "ApCjqNHgvuvsiQYpX4kGCxXTipcWJUe7NmnNfq3UKrwD"
[registry]
url = "https://api.apr.dev"
[provider]
cluster = "devnet" # added this
wallet = "~/.config/solana/id.json"
[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
A continuación, actualiza el archivo programs/spl_token_with_metadata/Cargo.toml.
[package]
name = "spl_token_with_metadata"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
name = "spl_token_with_metadata"
[features]
default = []
cpi = ["no-entrypoint"]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] # added "anchor-spl/idl-build"
[dependencies]
anchor-lang = "0.31.0"
anchor-spl = { version = "0.31.0", features = ["token"] } # added this
mpl-token-metadata = "5.1.0" # added this
Configuramos nuestro proyecto para usar Anchor SPL y el crate de Metaplex Token Metadata.
Estamos añadiendo estas dependencias para propósitos específicos:
anchor-spl: Proporciona interfaces compatibles con Anchor para el programa de tokens SPL de Solanampl-token-metadata: Nos permite interactuar con el Metaplex Token Metadata Program para crear y gestionar metadatos para nuestros tokens SPL
Hemos añadido la característica idl-build = ["anchor-spl/idl-build"] a nuestro Cargo.toml para generar un archivo IDL que incluye los tipos de tokens SPL, permitiendo que nuestro cliente TypeScript interactúe correctamente con nuestro programa
Añadir el código del programa Anchor
Ahora actualiza el programa Anchor con el siguiente código.
Aquí hemos definido una función create_token_metadata para adjuntar metadatos a un token SPL proporcionado. Explicaremos el código en detalle a medida que avancemos.
// Import the necessary dependencies for out program: Anchor, Anchor SPL and Metaplex Token Metadata crate
use anchor_lang::prelude::*;
use anchor_spl::token::Mint;
use mpl_token_metadata::instructions::{
CreateMetadataAccountV3Cpi, CreateMetadataAccountV3CpiAccounts,
CreateMetadataAccountV3InstructionArgs,
};
use mpl_token_metadata::types::{Creator, DataV2};
use mpl_token_metadata::ID as METADATA_PROGRAM_ID;
declare_id!("2SZvgGtgotJFy1aKd4Rnm7UEZNxUdP4sdXbeLDgKDiGM"); // run Anchor sync to update your program ID
#[program]
pub mod spl_token_with_metadata {
use super::*;
pub fn create_token_metadata(
ctx: Context<CreateTokenMetadata>,
name: String,
symbol: String,
uri: String,
seller_fee_basis_points: u16,
is_mutable: bool,
) -> Result<()> {
// Create metadata instruction arguments using the Fungible Standard format
// This follows the token_standard = 2 format we discussed earlier
let data = DataV2 {
name,
symbol,
uri, // Points to JSON with name, symbol, description, and image
seller_fee_basis_points,
creators: Some(vec![Creator {
address: ctx.accounts.payer.key(),
verified: true,
share: 100,
}]),
collection: None,
uses: None,
};
// Find the metadata account address (PDA)
let mint_key = ctx.accounts.mint.key();
let seeds = &[
b"metadata".as_ref(),
METADATA_PROGRAM_ID.as_ref(),
mint_key.as_ref(),
];
let (metadata_pda, _) = Pubkey::find_program_address(seeds, &METADATA_PROGRAM_ID);
// Ensure the provided metadata account matches the PDA
require!(
metadata_pda == ctx.accounts.metadata.key(),
MetaplexError::InvalidMetadataAccount
);
// Create and execute the CPI to create metadata
let token_metadata_program_info = ctx.accounts.token_metadata_program.to_account_info();
let metadata_info = ctx.accounts.metadata.to_account_info();
let mint_info = ctx.accounts.mint.to_account_info();
let authority_info = ctx.accounts.authority.to_account_info();
let payer_info = ctx.accounts.payer.to_account_info();
let system_program_info = ctx.accounts.system_program.to_account_info();
let rent_info = ctx.accounts.rent.to_account_info();
let cpi = CreateMetadataAccountV3Cpi::new(
&token_metadata_program_info,
CreateMetadataAccountV3CpiAccounts {
metadata: &metadata_info,
mint: &mint_info,
mint_authority: &authority_info,
payer: &payer_info,
update_authority: (&authority_info, true),
system_program: &system_program_info,
rent: Some(&rent_info),
},
CreateMetadataAccountV3InstructionArgs {
data,
is_mutable,
collection_details: None,
},
);
cpi.invoke()?;
Ok(())
}
}
Importamos las dependencias necesarias para nuestro programa: las utilidades de tokens SPL del crate anchor_spl para operaciones de tokens y los componentes de metadatos de tokens de Metaplex del crate mpl_token_metadata para crear y estructurar cuentas de metadatos.
La función create_token_metadata adjunta metadatos a un mint de token SPL. Demostraremos esto llamándola con una dirección mint que crearemos más adelante durante las pruebas.
Explicando la función create_token_metadata
Repasemos la función create_token_metadata, comenzando con la parte más importante:
Definiendo la estructura de metadatos
Primero definimos el contenido de la cuenta de metadatos con el tipo de struct DataV2 que fue importado de mpl_token_metadata::types (crate de Metaplex Metadata).

Esta estructura contiene los datos para nuestra cuenta de metadatos y es utilizada por Metaplex al construir una cuenta de metadatos.
Validando la cuenta de metadatos
A continuación, hacemos una comprobación para asegurar que se pasa la cuenta de metadatos correcta para el token (mint).

Creando la cuenta de metadatos vía CPI
Finalmente, construimos y ejecutamos una instrucción CreateMetadataAccountV3 (discutimos esto en el artículo anterior) hacia el Metaplex Token Metadata Program a través de CPI.

De la imagen anterior, podemos ver que pasamos varias cuentas a CreateMetadataAccountV3Cpi. Estas son las cuentas requeridas para crear metadatos. Estas cuentas pasadas están definidas en el struct de contexto a continuación, donde explicamos el origen y propósito de cada cuenta.
Ahora, veamos el struct de contexto para la función create_token_metadata, el cual contiene las cuentas necesarias para la llamada CPI CreateMetadataAccountV3Cpi:
El struct de cuenta CreateTokenMetadata a continuación incluye:
metadata: El PDA de metadatos que será creado por el Metaplex Token Metadata Programmint: Una cuenta mint de token SPL existente a la que adjuntaremos metadatos. La crearemos y la pasaremos al probar nuestro programaauthority: La autoridad del mint para autorizar esta transacciónpayer: La cuenta de la wallet que paga las tarifas de transacción y el alquiler (renta)system_program: El System Program de Solana para crear nuevas cuentasrent: El sysvar Rent de Solana para calcular la exención de rentatoken_metadata_program: El Metaplex Token Metadata Program on-chain (con la dirección fija definida comoMETADATA_PROGRAM_IDen nuestro código)
También definimos un error personalizado MetaplexError para manejar fallos de validación. Añade este código al código del programa.
#[derive(Accounts)]
pub struct CreateTokenMetadata<'info> {
/// CHECK: metadata PDA (will be created by the Metaplex Token Metadata program via CPI in the create_token_metadata function)
#[account(mut)]
pub metadata: AccountInfo<'info>,
// The mint account of the token
#[account(mut)]
pub mint: Account<'info, Mint>,
// The mint authority of the token
pub authority: Signer<'info>,
// The account paying for the transaction
#[account(mut)]
pub payer: Signer<'info>,
// Onchain programs our code depends on
pub system_program: Program<'info, System>,
pub rent: Sysvar<'info, Rent>,
/// CHECK: This is the Metaplex Token Metadata program
#[account(address = METADATA_PROGRAM_ID)]
// constraint to ensure the right account is passed
pub token_metadata_program: AccountInfo<'info>,
}
#[error_code]
pub enum MetaplexError {
#[msg("The provided metadata account does not match the PDA for this mint")]
InvalidMetadataAccount,
}
Implementemos esta prueba para nuestro programa.
Probando nuestro programa
Entendiendo Irys
Irys (anteriormente Bundlr) es un servicio que facilita la subida de datos a Arweave, un almacenamiento descentralizado permanente. En nuestra prueba, utilizaremos Irys para subir tanto la imagen de nuestro token como el JSON de metadatos al almacenamiento permanente.
Puntos clave sobre Irys:
- Ya está incluido en el paquete
@metaplex-foundation/jsa través del móduloirysStorage, por lo que no se requiere la instalación de dependencias adicionales después de instalar@metaplex-foundation/js. Simplemente puedes importarlo. - No se requiere la creación de una cuenta separada, ya que utiliza nuestra wallet de Solana existente para los pagos
- En devnet, las subidas se pagan utilizando devnet SOL de tu wallet (solicitaremos devnet SOL a través de un airdrop más adelante)
Ahora actualiza la prueba del programa en tests/spl_token_with_metadata.ts con el código a continuación.
La prueba hace lo siguiente:
- Define el ID del Metaplex Token Metadata Program como una constante (necesario para la derivación de PDA y llamadas CPI)
- Crea un token SPL utilizando una cuenta keypair generada
- Configura Metaplex con Irys para subir una imagen almacenada localmente a la devnet de Arweave, lo cual devuelve un URI a la imagen subida
- Crea los metadatos JSON en la prueba (con nombre, símbolo, descripción y el URI de la imagen) y los sube a Arweave
- Deriva el PDA de metadatos usando el prefijo ‘metadata’, el ID del programa de metadatos y la dirección mint como semillas (seeds)
- Llama a
create_token_metadataen nuestro programa Anchor - Finalmente, verifica que la cuenta de metadatos exista y tenga el propietario correcto
Se han añadido comentarios en el código para mostrar cada paso.
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import {
Metaplex,
irysStorage,
keypairIdentity,
toMetaplexFile,
} from "@metaplex-foundation/js";
import { createMint } from "@solana/spl-token";
import { Keypair, PublicKey, SystemProgram } from "@solana/web3.js";
import { assert } from "chai";
import { readFileSync } from "fs";
import path from "path";
import { SplTokenWithMetadata } from "../target/types/spl_token_with_metadata";
describe("spl_token_with_metadata", () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace
.splTokenWithMetadata as Program<SplTokenWithMetadata>;
const wallet = provider.wallet as anchor.Wallet;
const TOKEN_METADATA_PROGRAM_ID = new PublicKey(
"metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
);
// configure Metaplex with Irys (formerly Bundlr)
const metaplex = Metaplex.make(provider.connection)
.use(keypairIdentity(wallet.payer))
.use(
irysStorage({
address: "https://devnet.irys.xyz", // Irys endpoint
providerUrl: provider.connection.rpcEndpoint,
timeout: 60_000,
})
);
it("creates token with metadata", async () => {
// Create the mint
const mintKeypair = Keypair.generate();
await createMint(
provider.connection,
wallet.payer,
wallet.publicKey,
wallet.publicKey,
9,
mintKeypair
);
const mintPubkey = mintKeypair.publicKey;
console.log("Mint Pubkey:", mintPubkey.toBase58());
// Read & convert our image into a MetaplexFile
const imageBuffer = readFileSync(
path.resolve(__dirname, "../assets/image/kitten.png")
);
const metaplexFile = toMetaplexFile(imageBuffer, "kitten.png");
// Upload image, get arweave URI string
const arweaveImageUri: string = await metaplex.storage().upload(metaplexFile);
const imageTxId = arweaveImageUri.split("/").pop()!;
const imageUri = `https://devnet.irys.xyz/${imageTxId}`;
console.log("Devnet Irys image URL:", imageUri); // using Irys devnet gateway because Arweave public gateway has no devnet
// Build our JSON metadata object following the Fungible Standard format
// This matches the token_standard = 2 format we explained earlier
const metadata = {
name: "Test Token",
symbol: "TEST",
description: "Test token with metadata example",
image: imageUri,
};
// Upload JSON, get arweave URI string
const arweaveMetadataUri: string = await metaplex
.storage()
.uploadJson(metadata);
const metadataTxId = arweaveMetadataUri.split("/").pop()!;
const metadataUri = `https://devnet.irys.xyz/${metadataTxId}`;
console.log("Devnet Irys metadata URL:", metadataUri); // using Irys devnet gateway because Arweave public gateway has no devnet
// Derive on-chain metadata PDA
const [metadataPda] = PublicKey.findProgramAddressSync(
[
Buffer.from("metadata"),
TOKEN_METADATA_PROGRAM_ID.toBuffer(),
mintPubkey.toBuffer(),
],
TOKEN_METADATA_PROGRAM_ID
);
console.log("Metadata PDA:", metadataPda.toBase58());
// Call the create_token_metadata function
const tx = await program.methods
.createTokenMetadata(
metadata.name,
metadata.symbol,
metadataUri,
100, // 1%
true // isMutable
)
.accounts({
metadata: metadataPda,
mint: mintPubkey,
authority: wallet.publicKey,
payer: wallet.publicKey,
systemProgram: SystemProgram.programId,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
})
.rpc();
console.log("Transaction signature:", tx);
// Assert the account exists & is owned by the Metadata program
const info = await provider.connection.getAccountInfo(metadataPda);
assert(info !== null, "Metadata account must exist");
assert(
info.owner.equals(TOKEN_METADATA_PROGRAM_ID),
"Wrong owner for metadata account"
);
});
});
Ahora que tenemos nuestra prueba lista, crea un directorio assets/image en nuestro espacio de trabajo para colocar la imagen que usaremos como imagen de nuestro token. Esta imagen será utilizada por Irys en nuestra prueba en:

Ya hemos subido esta imagen a la devnet de Irys por ti. Haz clic en el enlace a continuación para descargarla y colocarla en el directorio que acabas de crear: https://devnet.irys.xyz/8VY89xG1RiUjtz1Lwgip7eUxZvtsdkf1gViGYaDKmwx8
Ahora, ejecuta npm install @solana/spl-token @metaplex-foundation/js en la terminal para instalar las dependencias de la prueba.
A continuación, configura Solana para usar devnet.
Ejecuta este comando en la terminal: solana config set --url [https://api.devnet.solana.com](https://api.devnet.solana.com/)

Solicita un airdrop: solana airdrop 5. Necesitamos fondos para desplegar nuestro programa y todas las cuentas involucradas en devnet.

Ahora compila (build) el proyecto y ejecuta la prueba. Esto desplegará el programa, el token y la cuenta de metadatos en la devnet de Solana (nota: esto podría tomar algo de tiempo dependiendo de tu conexión a la red).

Podemos ver que el mint (token) ha sido creado con metadatos (y tiene una imagen), como se muestra a continuación.

También podemos ver sus metadatos.

Enlace de despliegue para este en particular: https://explorer.solana.com/tx/2c27FRN48fHzzLTA9kV2XXwCEUPEQcWXvfT3k31PhPoEyFNe3bepJ7XxvKwAXekzPaV5nQeCR8mfxAeKqG15QT4Q?cluster=devnet
Cuenta de metadatos: https://explorer.solana.com/address/5feQdhNd3PxPJ9apKUpCWfB47cQdLitNMrVP8Gnq3cad?cluster=devnet
Para ver el efecto de la cuenta de metadatos, observa la interfaz de usuario para el despliegue de un token SPL regular (el cual hicimos en tutoriales anteriores). Nota cómo no hay un nombre o imagen en la parte superior, ni una pestaña para Metadatos junto a las pestañas de History, Transfer e Instructions.

Hemos desplegado con éxito un token SPL y le adjuntamos metadatos utilizando el estándar de Metaplex, siguiendo el formato Fungible Standard que discutimos anteriormente con los campos básicos de nombre, símbolo, descripción e imagen.
Conclusión
En este tutorial, creamos un token SPL y le adjuntamos metadatos con la ayuda del estándar Metaplex Token Metadata.
En nuestro programa Anchor, usamos el struct DataV2 para definir los metadatos del token e invocamos la instrucción CreateMetadataAccountV3 (a través de CPI) desde el crate mpl-token-metadata para crear la cuenta de metadatos. Usamos Metaplex con Irys para subir la imagen del token y el JSON de metadatos a Arweave. Luego confirmamos que la cuenta de metadatos existe después de la creación y es propiedad del programa Metaplex. Por último, explicamos los estándares de tokens de Metaplex — Fungible (2), Fungible Asset (1) y Non-Fungible (0) — y describimos sus formatos URI de JSON.
Este artículo es parte de una serie de tutoriales sobre Solana.