En el tutorial anterior, aprendimos cómo funcionan los tokens SPL. En este tutorial, implementaremos un ciclo de vida completo de un token SPL: crear, acuñar (mint), transferir y consultar tokens utilizando dos enfoques:
- On-chain con Anchor: crearemos un programa de Solana con Anchor que acuña tokens SPL hasta alcanzar un límite de suministro predefinido.
- Del lado del cliente con TypeScript: también mostraremos cómo interactuar directamente con el Token Program desde un cliente TypeScript para crear SPL mints, ATAs, acuñar tokens, transferir y leer balances.
¿Por qué dos enfoques?
Saber cómo hacer ambas cosas es crucial porque:
- Con Anchor, podemos construir lógica on-chain personalizada sobre un token SPL (por ejemplo, calendarios de consolidación o vesting, acuñación condicional) o crear un token SPL controlado por nuestro programa en lugar de una wallet.
- Con TypeScript, podemos interactuar directamente con el programa SPL para actividades sencillas como transferir tokens SPL o autorizar/revocar un delegado.
Ahora comencemos con el enfoque de Anchor.
Creando tokens SPL en Anchor
Recuerde del tutorial anterior de tokens SPL que cada token utiliza el mismo programa on-chain (el SPL Token Program en la dirección TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA) para crear cuentas mint y realizar la acuñación de tokens, transferencias, aprobaciones, etc.
En esta sección, construiremos un programa en Anchor que crea y acuña tokens SPL a través de Cross-Program Invocations (CPI) al Token Program.
Nuestro programa tendrá solo dos funciones:
- Una función
create_and_mint_tokenpara crear la cuenta mint y acuñar un suministro inicial en una associated token account (ATA) especificada, a través de una CPI al Token Program. - Una función
transfer_tokenspara mover tokens desde una ATA de origen hacia una ATA de destino a través de una CPI al Token Program.
Ahora, cree un nuevo proyecto en Anchor con anchor init spl_token. Abra el proyecto y reemplace el código en programs/spl_token/src/lib.rs con lo siguiente:
En este código:
- Importamos nuestras dependencias:
anchor_spl::associated_token::AssociatedTokenpara crear Associated Token Accounts (ATAs).anchor_spl::token::{Mint, MintTo, Token, TokenAccount, Transfer}para trabajar con el SPL Token Program (estas son las instrucciones y los tipos de cuenta que necesitamos para la acuñación y las transferencias).
- Definimos una función
create_and_mint_tokenque:- Utiliza la cuenta mint proporcionada y la ATA de destino (donde se depositarán los tokens acuñados).
- Construye un contexto CPI que apunta al Token Program.
- Llama a la instrucción
mint_todel Token Program para acuñar 100 tokens (con 9 decimales) en la ATA. - Devuelve éxito una vez que los tokens han sido acuñados.
use anchor_lang::prelude::*;
use anchor_spl::associated_token::AssociatedToken; // Needed for ATA creation
use anchor_spl::token::{self, Mint, MintTo, Token, TokenAccount, Transfer}; // Needed for mint account creation/handling
declare_id!("6zndm8QQsPxbjTRC8yh5mxqfjmUchTaJyu2yKbP7ZT2x");
#[program]
pub mod spl_token {
use super::*;
// This function deploys a new SPL token with decimal of 9 and mints 100 units of the token
pub fn create_and_mint_token(ctx: Context<CreateMint>) -> Result<()> {
let mint_amount = 100_000_000_000; // 100 tokens with 9 decimals
let mint = ctx.accounts.new_mint.clone();
let destination_ata = &ctx.accounts.new_ata;
let authority = ctx.accounts.signer.clone();
let token_program = ctx.accounts.token_program.clone();
let mint_to_instruction = MintTo {
mint: mint.to_account_info(),
to: destination_ata.to_account_info(),
authority: authority.to_account_info(),
};
let cpi_ctx = CpiContext::new(token_program.to_account_info(), mint_to_instruction);
token::mint_to(cpi_ctx, mint_amount)?;
Ok(())
}
}
Agregue el struct de cuenta CreateMint. Contiene las siguientes cuentas:
signer: La cuenta que paga las tarifas de la transacción y también sirve como el mint authoritynew_mint: Una cuenta PDA mint que se inicializa con 9 posiciones decimales y utiliza elsignertanto como mint authority como freeze authoritynew_ata: Una associated token account que se creará para el nuevo mint y utiliza elsignercomo su autoridad (en efecto, la cuenta que mantiene el balance delsigner)- Finalmente, pasamos el Token Program, el Associated Token Program y el System Program. Estos son los programas nativos con los que interactuamos a través de CPI.
#[derive(Accounts)]
pub struct CreateMint<'info> {
#[account(mut)]
pub signer: Signer<'info>,
#[account(
init,
payer = signer,
mint::decimals = 9,
mint::authority = signer,
// Commenting out or removing this line permanently disables the freeze authority.
mint::freeze_authority = signer,
// When a token is created without a freeze authority, Solana prevents any future updates to it.
// This makes the token more decentralized, as no authority can freeze a user's ATA.
seeds = [b"my_mint", signer.key().as_ref()],
bump
)]
pub new_mint: Account<'info, Mint>,
#[account(
init,
payer = signer,
associated_token::mint = new_mint,
associated_token::authority = signer,
)]
pub new_ata: Account<'info, TokenAccount>,
// This represents the SPL Token Program (TokenkegQfeZ…)
// The same program we introduced in the previous article that owns and manages all mint and associated token account.
pub token_program: Program<'info, Token>,
// This represents the ATA program (ATokenGPvbdGV...)
// As mentioned in the previous tutorial, it is only in charge of creating the ATA.
pub associated_token_program: Program<'info, AssociatedToken>,
pub system_program: Program<'info, System>,
}
Ahora ejecute anchor keys sync para sincronizar su Program ID.
A continuación, actualice el archivo programs/spl_token/Cargo.toml para agregar el crate anchor-spl a nuestro proyecto como dependencia
[package]
name = "spl_token"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
name = "spl_token"
[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 = "0.31.0" # added this
Este anchor-spl nos da acceso al SPL Token Program, al programa ATA y a sus instrucciones.
Ahora, examinemos qué está sucediendo en el código del programa.
Comenzamos con el struct CreateMint.

signer
Primero, declaramos el signer que paga por la transacción de despliegue del token, como se resalta en morado a continuación.

mint
A continuación, declaramos una cuenta new_mint, que representa el token SPL que queremos crear (resaltado en rojo a continuación). Su tipo de cuenta es Mint (resaltado en amarillo a continuación). Este tipo de cuenta representa una cuenta mint en Solana.

Como puede ver en la imagen anterior, inicializamos esta nueva cuenta mint como una Program Derived Address (PDA) y establecemos sus parámetros: decimales del token, autoridades de acuñación (mint) y congelamiento (freeze), y las semillas (seeds) de la PDA. En lugar de utilizar una cuenta keypair, derivamos el mint como una PDA a partir de semillas fijas y el ID del programa, por lo que no hay necesidad de generar o gestionar una clave privada como en una cuenta keypair. Estamos utilizando una PDA mint principalmente por conveniencia.
Si es nuevo en el funcionamiento de las PDAs o en cómo se diferencian de las cuentas keypair, consulte nuestro artículo “PDA (Program Derived Address) vs Keypair Account in Solana.”
Finalmente, la restricción init le indica a Anchor que cree e inicialice la cuenta mint automáticamente cuando se ejecute create_and_mint_token (explicaremos esa función a continuación).
Debido a esta restricción init, Anchor realizará una CPI (cross-program invocation) a la instrucción InitializeMint del Token Program entre bastidores. Esta instrucción establece los decimales del mint en 9 y asigna tanto el mint authority como el freeze authority al signer.
Associated Token Account
El siguiente paso es la associated token account (ATA) en la que acuñaremos este token (resaltado en amarillo a continuación).
Nota: una cuenta mint no necesita que exista una ATA. Solo estamos creando una aquí porque queremos acuñar algunos tokens para el signer.

La ATA es de tipo TokenAccount, lo que representa una ATA en Solana. Al igual que la cuenta mint, establecemos sus parámetros: el mint de la ATA se establece en el nuevo token que estamos creando, y el signer se convierte en su autoridad. Esto significa que solo el signer puede autorizar instrucciones que modifiquen el estado de la ATA. Anchor realiza internamente una CPI a la instrucción InitializeAccount del Token Program para aplicar estas configuraciones.
Nota: podemos utilizar init aquí de forma segura únicamente porque la cuenta mint (new_mint) también se está creando en la misma instrucción. Si el mint ya existiera, el uso de init en una ATA podría fallar si alguien ya hubiera creado esa ATA, causando una denegación de servicio. En los casos en que el mint ya pueda existir, es más seguro utilizar init_if_needed en su lugar. De lo contrario, alguien podría adelantarse (hacer frontrun) a la instrucción y crear una ATA en nombre del signer, y provocar que esta transacción falle.
Cuentas de programas nativos
Finalmente, declaramos los programas nativos de Solana necesarios para crear el mint y la associated token account (resaltado en verde a continuación). Estos son los programas on-chain con los que interactúa nuestro programa de Anchor: el Token Program para crear el mint y acuñar tokens, el Associated Token Account Program para crear la ATA del usuario, y el System Program para asignar espacio a las cuentas y gestionar la renta (rent).

Puede que haya notado que la ATA (cuenta new_ata) no tiene un seed ni un bump como la cuenta mint (new_mint), esto se debe a que la instrucción InitializeAccount utiliza el proceso estándar de derivación de las associated token accounts, es decir, user_wallet_address + token_mint_address => associated_token_account_address. Así que no tenemos que pasar un seed ni un bump. Si intenta pasar un seed y un bump, Anchor arrojará este error.

Tampoco especificamos el space (espacio) para la cuenta mint y la ATA porque Anchor también añade el espacio por nosotros entre bastidores. Conoce esta información porque especificamos que el programa es AssociatedToken. Se producirá un error si intentamos especificar el space para cualquiera de los dos.

El tamaño real para el mint y la associated token account es de 82 y 165 bytes respectivamente.
Ahora que hemos declarado todas las cuentas que necesitamos, examinemos la función create_and_mint_token para acuñar tokens SPL.
Acuñando el token SPL
Usamos esta función para acuñar 100 (con 9 decimales) de los tokens que acabamos de crear en la ATA recién creada del signer.

Construimos una instrucción MintTo en el código anterior. Estos tres campos definen el comportamiento de MintTo:
mint: qué token estamos acuñando, según lo especificado por la cuenta mint.to: la ATA que recibirá los tokens acuñados.authority: la cuenta permitida para acuñar tokens para este mint. En nuestro programa, establecemos el mint authority al firmante de la transacción (signer), por lo que elsignerdebe firmar y coincidir con la autoridad del mint para que la acuñación tenga éxito.
Luego hacemos una CPI al Token Program con esta instrucción (como se resalta en verde), la cual acuña 100 unidades de nuestro token a la associated token account.
Además, como se discutió en el tutorial anterior, tanto la cuenta mint como la ATA deben existir antes de que se llame a la instrucción MintTo (esto también aplica a Transfer). Es por eso que utilizamos la restricción #[account(init…)]; esta garantiza que estas cuentas sean creadas justo antes de que se ejecute la instrucción.

Nota: Para crear un NFT en Solana, se inicializa el mint con mint::decimals = 0, se acuña exactamente 1 token para un destinatario y luego se revoca el mint authority estableciéndolo en None. Esto asegura que nunca se puedan acuñar más tokens y hace que el token sea único y no fungible porque no es fraccionario, debido a que tiene cero decimales.
Probando la función createAndMintToken
Ahora, probaremos la función createAndMintToken.
Reemplace el código de prueba en tests/spl_token.ts con el siguiente código. La prueba está estructurada de esta manera.
- Derivamos la dirección de la cuenta mint del token off-chain usando
findProgramAddressSyncde la biblioteca@coral-xyz/anchor, con las mismas semillas (seeds) utilizadas en nuestro programa de Anchor. Este paso no despliega la cuenta mint, de eso ya nos encargamos dentro del programa Anchor, como se explicó anteriormente. - A continuación calculamos la dirección de la ATA del
signerusando la funcióngetAssociatedTokenAddressSync. De nuevo, esto no despliega la cuenta. - Llamamos a la función del programa Anchor con las cuentas apropiadas (
signer, mint, ATA, Token Program, ATA Program y el System Program) e imprimimos el hash de la transacción, la dirección del token y la dirección de la ATA delsigner. - Por último, recuperamos la información del mint y de la ATA utilizando las funciones
getMintygetAccountde la biblioteca@solana/spl-token, y afirmamos (assert) que sus contenidos coinciden con lo que habíamos establecido previamente en nuestro Programa Anchor. Verificamos los decimales del token, autoridades, suministro de tokens, balance de la ATA del token, etc.
import * as anchor from "@coral-xyz/anchor";
import { Program, web3 } from "@coral-xyz/anchor";
import * as splToken from "@solana/spl-token";
import { PublicKey } from "@solana/web3.js";
import { assert } from 'chai';
import { SplToken } from "../target/types/spl_token";
describe("spl_token", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.splToken as Program<SplToken>;
const provider = anchor.AnchorProvider.env();
const signerKp = provider.wallet.payer;
const toKp = new web3.Keypair();
it("Creates a new mint and associated token account using CPI", async () => {
// Derive the mint address using the same seeds ("my_mint" + signer public key) we used when the mint was created in our Anchor program
const [mint] = PublicKey.findProgramAddressSync(
[Buffer.from("my_mint"), signerKp.publicKey.toBuffer()],
program.programId
);
// Get the associated token account address
// The boolean value here indicates whether the authority of the ATA is an "off-curve" address (i.e., a PDA).
// A value of false means the owner is a normal wallet address.
// `signerKp` is the owner here and it is a normal wallet address, so we use false.
const ata = splToken.getAssociatedTokenAddressSync(mint, signerKp.publicKey, false)
// Call the create_mint instruction
const tx = await program.methods
.createAndMintToken()
.accounts({
signer: signerKp.publicKey,
newMint: mint,
newAta: ata,
tokenProgram: splToken.TOKEN_PROGRAM_ID,
associatedTokenProgram: splToken.ASSOCIATED_TOKEN_PROGRAM_ID,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
console.log("Transaction signature:", tx);
console.log("Token (Mint Account) Address:", mint.toString());
console.log("Associated Token Account:", ata.toString());
/// Verify the token details
const mintInfo = await splToken.getMint(provider.connection, mint);
assert.equal(mintInfo.decimals, 9, "Mint decimals should be 9");
assert.equal(mintInfo.mintAuthority?.toString(), signerKp.publicKey.toString(), "Mint authority should be the signer");
assert.equal(mintInfo.freezeAuthority?.toString(), signerKp.publicKey.toString(), "Freeze authority should be the signer");
assert.equal(mintInfo.supply.toString(), "100000000000", "Supply should be 100 tokens (with 9 decimals)");
// Verify the ATA details
const tokenAccount = await splToken.getAccount(provider.connection, ata);
assert.equal(tokenAccount.mint.toString(), mint.toString(), "Token account mint should match the mint PDA");
assert.equal(tokenAccount.owner.toString(), signerKp.publicKey.toString(), "Token account owner should be the signer");
assert.equal(tokenAccount.amount.toString(), "100000000000", "Token balance should be 100 tokens (with 9 decimals)");
assert.equal(tokenAccount.delegate, null, "Token account should not have a delegate");
});
});
Ejecute npm install @solana/spl-token para instalar la biblioteca de tokens SPL.
Ahora ejecute anchor test y verá que tanto el token como la ATA se desplegaron con éxito.

Transfiriendo tokens SPL
Para transferir tokens, construimos una instrucción Transfer y hacemos una CPI al Token Program. Esta transferencia funciona moviendo la cantidad especificada de unidades del token desde una associated token account de origen hacia una associated token account de destino. El firmante de esta transacción debe ser la autoridad de la ATA de origen.
Ahora agregue la siguiente función a su programa. Hace lo siguiente:
- Carga la associated token account de origen (
from_ata) de donde se tomarán los tokens. - Carga la associated token account de destino (
to_ata) a donde se enviarán los tokens (esta ATA será creada en nuestro código de prueba). - Carga la cuenta de autoridad (
from) que debe firmar y aprobar la transferencia. - Carga la cuenta del Token Program que procesará la transferencia.
- Construye la instrucción
Transfercon las cuentas de origen, destino y autoridad. - Crea un contexto CPI (Cross-Program Invocation) que envuelve el Token Program y la instrucción de transferencia.
- Llama a la función
token::transfercon el contexto CPI y la cantidad, lo cual mueve los tokens de la ATA de origen a la ATA de destino.
pub fn transfer_tokens(ctx: Context<TransferSpl>, amount: u64) -> Result<()> {
let source_ata = &ctx.accounts.from_ata;
let destination_ata = &ctx.accounts.to_ata;
let authority = &ctx.accounts.from;
let token_program = &ctx.accounts.token_program;
// Transfer tokens from from_ata to to_ata
let cpi_accounts = Transfer { // Transfer instruction
from: source_ata.to_account_info().clone(),
to: destination_ata.to_account_info().clone(),
authority: authority.to_account_info().clone(),
};
let cpi_ctx = CpiContext::new(token_program.to_account_info(), cpi_accounts); // Create a CPI context
token::transfer(cpi_ctx, amount)?;
Ok(())
}
En ERC-20, transfer asume a msg.sender como el propietario del token, y transferFrom permite a un tercero (un delegado) mover tokens en nombre de otro si está aprobado. El SPL Token Program combina ambos en una única instrucción transfer, pero requiere pasar la autoridad de transferencia de forma explícita como una cuenta (AccountInfo en nuestro código Anchor) — esto se mapea al campo Transfer.authority. Esta autoridad es el firmante permitido para mover tokens; puede ser el propietario del token o un delegado aprobado.
Por lo tanto, en la instrucción transfer:
from: es la ATA del remitente del tokento: es la ATA del receptor del tokenauthority: es el firmante que tiene permiso para mover tokens desdefrom(puede ser el propietario o un delegado con aprobación)
Ahora agregue el struct de cuenta TransferSpl a continuación, el cual define las cuentas necesarias para realizar la transferencia del token.
#[derive(Accounts)]
pub struct TransferSpl<'info> {
pub from: Signer<'info>,
#[account(mut)]
pub from_ata: Account<'info, TokenAccount>,
#[account(mut)]
pub to_ata: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>, // We are interacting with the Token Program
}
Pasamos el firmante de la transacción, las ATAs de origen y destino, y finalmente el Token Program con el que interactuaremos.
Agregue esta prueba a nuestro archivo de pruebas.
Hacemos lo siguiente en la prueba.
- Primero, derivamos la dirección del token (cuenta mint) que queremos transferir con la función
findProgramAddressSync. - A continuación, calculamos la dirección de la ATA tanto de origen (billetera del remitente) como de destino (billetera del receptor) con la función
getAssociatedTokenAddressSyncde@solana/spl-token, que toma la dirección mint, la respectiva dirección de cuenta, y un valor booleano que indica si el firmante de la ATA (signerKp) es una PDA o no. Lo cual no es, en este caso. - Creamos la ATA para la cuenta de destino con la función
createAssociatedTokenAccount. No creamos la ATA para elsignerporque ya se hizo en el caso de prueba anterior. Dado que todos los casos de prueba se ejecutan juntos, la cuenta persiste. - Finalmente, transferimos 10 tokens a la ATA de destino utilizando la función
transfer_tokensde nuestro programa. Luego, recuperamos el balance de tokens de la ATA de destino con la funcióngetTokenAccountBalancey afirmamos (assert) que es de 10 (la cantidad que enviamos).
it("Transfers tokens using CPI", async () => {
// Derive the PDA for the mint
const [mint] = PublicKey.findProgramAddressSync(
[Buffer.from("my_mint"), signerKp.publicKey.toBuffer()],
program.programId
);
// Get the ATAs
const fromAta = splToken.getAssociatedTokenAddressSync(mint, signerKp.publicKey, false);
const toAta = splToken.getAssociatedTokenAddressSync(mint, toKp.publicKey, false);
// Create to_ata as it doesn't exist yet
try {
await splToken.createAssociatedTokenAccount(
provider.connection,
signerKp,
mint,
toKp.publicKey
);
} catch (error) {
throw new Error(error)
}
const transferAmount = new anchor.BN(10_000_000_000); // 10 tokens with 9 decimals
// Transfer tokens
const tx = await program.methods
.transferTokens(transferAmount)
.accounts({
from: signerKp.publicKey,
fromAta: fromAta,
toAta: toAta,
tokenProgram: splToken.TOKEN_PROGRAM_ID,
})
.rpc();
console.log("Transfer Transaction signature:", tx);
// Verify the transfer
const toBalance = await provider.connection.getTokenAccountBalance(toAta);
assert.equal(
toBalance.value.amount,
transferAmount.toString(),
"Recipient balance should match transfer amount"
);
});
Ahora ejecute la prueba

Recuperando balances de tokens
Agregue esta función al programa para recuperar los balances de tokens de la ATA
pub fn get_balance(ctx: Context<GetBalance>) -> Result<()> {
// Get the token account address, its owner & balance
let ata_pubkey = ctx.accounts.token_account.key();
let owner = ctx.accounts.token_account.owner; // the `owner` is a field in the ATA
let balance = ctx.accounts.token_account.amount; // the `amount` is a field in the ATA
// Print the balance information
msg!("Token Account Address: {}", ata_pubkey);
msg!("Token Account Owner: {}", owner);
msg!("Token Account Balance: {}", balance);
Ok(())
}
El campo amount en la ATA contiene el balance de tokens. En esta función, accedemos a él directamente desde ctx.accounts.token_account para imprimir el balance.
Agregue el struct de contexto correspondiente:
#[derive(Accounts)]
pub struct GetBalance<'info> {
#[account(mut)]
pub token_account: Account<'info, TokenAccount>,
}
Actualice la prueba
it("Reads token balance using CPI", async () => {
// Derive the PDA for the mint
const [mint] = PublicKey.findProgramAddressSync(
[Buffer.from("my_mint"), signerKp.publicKey.toBuffer()],
program.programId
);
// Get the associated token account address
const ata = splToken.getAssociatedTokenAddressSync(mint, signerKp.publicKey, false);
// Call the get_balance instruction
const tx = await program.methods
.getBalance()
.accounts({
tokenAccount: ata,
})
.rpc();
console.log("Get Balance Transaction signature:", tx);
// Verify balance through direct query
const balance = await provider.connection.getTokenAccountBalance(ata);
assert.isTrue(balance.value.uiAmount > 0, "Token balance should be greater than 0");
});
Si ejecutamos el validador y revisamos los logs, deberíamos ver que el balance de la ATA del signer se ha reducido en 10 tokens (de 100 a 90). Esta es la cantidad que transferimos en el caso de prueba anterior.

Crear y transferir tokens directamente con el cliente TypeScript
También es posible crear e interactuar con un token SPL sin un programa de Solana simplemente utilizando el cliente TypeScript de web3.js.
Esto es útil cuando no necesita un programa on-chain con lógica personalizada. Si solo está acuñando tokens, transfiriéndolos o leyendo balances, hacerlo desde el cliente es más rápido y más barato. No hay necesidad de escribir o desplegar un programa.
Vamos a crear nuevos tokens y ATAs, y transferirlos directamente desde TypeScript.
Creando un Mint y ATA en TypeScript
Cree un nuevo proyecto de Anchor spl_token_ts y reemplace la prueba con el bloque de código TypeScript que se muestra más adelante en esta sección.
Esta suite de pruebas en TypeScript demuestra cómo interactuar directamente con el programa SPL Token utilizando la biblioteca @solana/spl-token.
Hace lo siguiente:
- Primero, llama a
splToken.createMint. Esta función envía una instrucciónInitializeMintal Token Program para crear una nueva cuenta mint de un token SPL. Proporcionamos la conexión, el pagador (signerKp, nuestrosignerlocal por defecto), el mint authority y el freeze authority, y la cantidad deseada de decimales (6 en este caso). Retorna la llave pública (public key) del mint recién creado. - Luego, utiliza
splToken.createAssociatedTokenAccountpara crear la ATA para elsignerKpen el mint recién creado. Este es un helper del SDK de TypeScript de@solana/spl-token. De manera interna, deriva la dirección de la ATA y envía la instrucción de creación al Associated Token Account Program. - Después, se llama a
splToken.mintTopara emitir nuevas unidades de token. Requiere la conexión, el pagador de la transacción (usamossignerKp), la llave pública del mint, la dirección de la ATA de destino, la llave pública del mint authority (signerKp.PublicKey), y la cantidad de tokens a acuñar (tenemos en cuenta los decimales). - Finalmente, verifica la configuración.
splToken.getMintobtiene los datos on-chain para la cuenta mint, y afirmamos que los decimales y autoridades coinciden con lo que especificamos.splToken.getAccountobtiene los datos de la ATA, y afirmamos que su balance de tokens coincide con la cantidad que acabamos de acuñar.
import * as anchor from "@coral-xyz/anchor";
import * as splToken from "@solana/spl-token";
import * as web3 from "@solana/web3.js";
import { assert } from 'chai';
describe("TypeScript SPL Token Tests", () => {
const provider = anchor.AnchorProvider.env();
const signerKp = provider.wallet.payer;
const toKp = new web3.Keypair();
// Define mint parameters
const mintDecimals = 6;
const mintAuthority = provider.wallet.publicKey;
const freezeAuthority = provider.wallet.publicKey;
it("Creates a mint account and ATA using TypeScript", async () => {
// Create the Mint
const mintPublicKey = await splToken.createMint(
provider.connection,
signerKp,
mintAuthority,
freezeAuthority,
mintDecimals
);
console.log("Created Mint:", mintPublicKey.toString());
// Create ATA for the signer
const ataAddress = await splToken.createAssociatedTokenAccount(
provider.connection,
signerKp,
mintPublicKey,
signerKp.publicKey
);
console.log("Created ATA:", ataAddress.toString());
// Mint some tokens
const mintAmount = BigInt(1000 * (10 ** mintDecimals)); // 1000 tokens
await splToken.mintTo(
provider.connection,
signerKp,
mintPublicKey,
ataAddress,
mintAuthority,
mintAmount
);
// Verify the mint
const mintInfo = await splToken.getMint(provider.connection, mintPublicKey);
assert.equal(mintInfo.decimals, mintDecimals, "Mint decimals should match");
assert.equal(mintInfo.mintAuthority?.toString(), mintAuthority.toString(), "Mint authority should match");
assert.equal(mintInfo.freezeAuthority?.toString(), freezeAuthority.toString(), "Freeze authority should match");
// Verify the balance
const accountInfo = await splToken.getAccount(provider.connection, ataAddress);
assert.equal(accountInfo.amount.toString(), mintAmount.toString(), "Balance should match minted amount");
});
});
Obteniendo balances de tokens en TypeScript
Ahora, agregue el siguiente bloque de prueba it para leer el balance de tokens.
Este bloque de prueba es similar a la primera prueba:
- Crea un nuevo mint y su correspondiente ATA para el
signerKpy acuña una cantidad inicial de tokens (1000 en este caso) hacia esta ATA (ataAddress). - El punto principal aquí es demostrar la recuperación del balance. Mostramos dos maneras de hacer esto:
splToken.getAccount: Obtiene el estado completo de la cuenta del token, desde el cual podemos acceder a la propiedad.amount.provider.connection.getTokenAccountBalance: Esta es una llamada RPC más directa específicamente para obtener el balance de una cuenta de token. Retorna un objeto que contiene la cantidad.
- Por motivos ilustrativos, se utilizan ambos métodos, y afirmamos que el balance recuperado coincide con la cantidad acuñada.
it("Reads token balance using TypeScript", async () => {
// Create a new mint for this test
const mintPublicKey = await splToken.createMint(
provider.connection,
signerKp,
mintAuthority,
freezeAuthority,
mintDecimals
);
// Create ATA
const ataAddress = await splToken.createAssociatedTokenAccount(
provider.connection,
signerKp,
mintPublicKey,
signerKp.publicKey
);
// Mint tokens
const mintAmount = BigInt(1000 * (10 ** mintDecimals)); // 1000 tokens
await splToken.mintTo(
provider.connection,
signerKp,
mintPublicKey,
ataAddress,
mintAuthority,
mintAmount
);
// Read balance using getAccount
const accountInfo = await splToken.getAccount(provider.connection, ataAddress);
console.log("Token Balance:", accountInfo.amount.toString());
assert.equal(accountInfo.amount.toString(), mintAmount.toString(), "Balance should match minted amount");
// Alternative: Read balance using getTokenAccountBalance
const balance = await provider.connection.getTokenAccountBalance(ataAddress);
assert.equal(balance.value.amount, mintAmount.toString(), "Balance should match minted amount");
});
Probando el movimiento de tokens entre cuentas
Finalmente, agregue el último bloque de prueba it para transferir tokens.
Este bloque de prueba:
- Crea un nuevo mint.
- Luego crea dos ATAs para este mint: una para la ATA de origen (
signerKp) y otra para la ATA de destino (toKp). Note quetoKpes un keypair recién generado, que representa a otro usuario. - 1000 unidades del token son acuñadas en la ATA de origen (la ATA del
signerKp). - El núcleo de esta prueba es la función
splToken.transfer. Esta función construye y envía la transacción para mover tokens entre las ATAs. Necesita la conexión, el pagador/firmante (signerKp), la ATA de origen, la ATA de destino, la autoridad de la ATA de origen (que essignerKp.publicKey), y la cantidad a transferir (500 tokens). - Después de la transferencia, verifica el resultado obteniendo los balances tanto de la ATA de origen como de la de destino utilizando
provider.connection.getTokenAccountBalance. Finalmente, afirmamos que el balance de origen ha disminuido en la cantidad de la transferencia y el balance de destino ahora es igual a la cantidad de la transferencia.
it("Transfers tokens using TypeScript", async () => {
// Create a new mint
const mintPublicKey = await splToken.createMint(
provider.connection,
signerKp,
mintAuthority,
freezeAuthority,
mintDecimals
);
// Create source ATA
const sourceAta = await splToken.createAssociatedTokenAccount(
provider.connection,
signerKp,
mintPublicKey,
signerKp.publicKey
);
// Create destination ATA
const destinationAta = await splToken.createAssociatedTokenAccount(
provider.connection,
signerKp,
mintPublicKey,
toKp.publicKey
);
// Mint tokens to source
const mintAmount = BigInt(1000 * (10 ** mintDecimals)); // 1000 tokens
await splToken.mintTo(
provider.connection,
signerKp,
mintPublicKey,
sourceAta,
mintAuthority,
mintAmount
);
// Read balance before transfer
const sourceBalanceBefore = await provider.connection.getTokenAccountBalance(sourceAta);
const destinationBalanceBefore = await provider.connection.getTokenAccountBalance(destinationAta);
console.log("Source Balance before transfer:", sourceBalanceBefore.value.amount);
console.log("Destination Balance before transfer:", destinationBalanceBefore.value.amount);
// Transfer tokens
const transferAmount = BigInt(500 * (10 ** mintDecimals)); // 500 tokens
await splToken.transfer(
provider.connection,
signerKp,
sourceAta,
destinationAta,
signerKp.publicKey,
transferAmount
);
// Read balance after transfer
const sourceBalanceAfter = await provider.connection.getTokenAccountBalance(sourceAta);
const destinationBalanceAfter = await provider.connection.getTokenAccountBalance(destinationAta);
console.log("Source Balance after transfer:", sourceBalanceAfter.value.amount);
console.log("Destination Balance after transfer:", destinationBalanceAfter.value.amount);
assert.equal(sourceBalanceAfter.value.amount, (mintAmount - transferAmount).toString(), "Source should have 500 tokens left");
assert.equal(destinationBalanceAfter.value.amount, transferAmount.toString(), "Destination should have received 500 tokens");
});
Ejecutamos la prueba completa para ver si todo funciona como se esperaba.

Ejercicio: Escriba una función disable_mint_authority que establezca el mint authority a None a través de la instrucción set_authority. Establezca el tipo de autoridad a AuthorityType::MintTokens. Después de eso, escriba una prueba para llamar a la función y luego intente acuñar más tokens; debería fallar con un error “supply is fixed”. Compruebe también que el mint authority ahora es null.
Debería obtener un resultado similar al de abajo.

Este artículo forma parte de una serie de tutoriales sobre Solana.