Ir al contenido principal

Cómo exigir notas en las transferencias entrantes mediante las extensiones de tokens de Solana

Actualizado el
26 de noviembre de 2025

20 minutos de lectura

Resumen

Las extensiones de tokens (también conocidas como el programa Token-2022) son una nueva primitiva habilitada por Solana Labs que ofrece a los desarrolladores una mayor flexibilidad y control, lo que permite crear estructuras tokenómicas complejas e implementar tokens web3. En esta guía, analizaremos en profundidad una de las nuevas funciones de las cuentas asociadas al programa Token-2022: los memos obligatorios para las transacciones entrantes (similares a las transferencias bancarias).

Tus funciones

En esta guía, realizarás pruebas de envío de tokens utilizando la extensión «Required Memo» de Token-2022:

  1. Crea un token mediante el programa Token-2022
  2. Prueba diferentes situaciones en las que se envían tokens con y sin instrucciones en el campo «memo».
  3. Activar y desactivar el requisito de nota en una cuenta

Lo que necesitarás

DependenciaVersión
node.js18.12.1
tsc5.0.2
ts-node10.9.1
solana-cli1.14.16

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 token-2022-memo && cd token-2022-memo
npm init -y # o yarn init -y
npm install @solana/web3.js@1 @solana/spl-token @solana/spl-memo # or yarn add @solana/web3.js@1 @solana/spl-token @solana/spl-memo
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 {
sendAndConfirmTransaction,
Connection,
Keypair,
SystemProgram,
Transaction,
LAMPORTS_PER_SOL,
PublicKey,
SendTransactionError,
TransactionSignature,
SignatureStatus,
TransactionConfirmationStatus
} from '@solana/web3.js';
import {
createMint,
createEnableRequiredMemoTransfersInstruction,
createInitializeAccountInstruction,
disableRequiredMemoTransfers,
enableRequiredMemoTransfers,
getAccountLen,
ExtensionType,
TOKEN_2022_PROGRAM_ID,
mintTo,
createAssociatedTokenAccountIdempotent,
createTransferCheckedInstruction,
unpackAccount,
getMemoTransfer
} from '@solana/spl-token';
import { createMemoInstruction } from '@solana/spl-memo';

Importamos las dependencias necesarias desde @solana/web3.js, @solana/spl-token, y @solana/spl-memo. Los utilizaremos para crear, acuñar y transferir tokens Token-2022. Hay un par de aspectos que son específicos de las transferencias con memo: createEnableRequiredMemoTransfersInstruction, desactivar las transferencias de notas obligatorias, habilitar las transferencias de notas obligatorias, getAccountLen, y getMemoTransfer. Trataremos estos temas con más detalle más adelante en la guía.

Definamos una función auxiliar para confirmar las transacciones. Añade lo siguiente a tu app.ts archivo:

    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`);
}

Por último, crea una función asíncrona llamada principal y añade el siguiente código:

async function main() {

// Initialize connection to local Solana node
const connection = new Connection('http://127.0.0.1:8899', 'confirmed');

// Amount of Tokens to transfer
const decimals = 9;
const transferAmount = BigInt(1_000 * Math.pow(10, decimals)); // Transfer 1,000 tokens

// Define Keypair - payer and owner of the source account
const payer = Keypair.generate();

// Define Keypair - mint authority
const mintAuthority = Keypair.generate();

// Define Keypair - destination account (owner of the destination account)
const owner = Keypair.generate();

// Define destination account (Token Account subject to memo requirement)
const destinationKeypair = Keypair.generate();
const destination = destinationKeypair.publicKey;

// 1 - Request an airdrop for payer
const airdropSignature = await connection.requestAirdrop(payer.publicKey, 2 * LAMPORTS_PER_SOL);
await confirmTransaction(connection, airdropSignature);

// 2 - Create a mint


// 3 - Create a destination account with memo requirement enabled


// 4 - Mint tokens to source account (owned by the payer)


// 5 - Create a transfer instruction


// 6 - Try to send a transaction without a memo (should fail)


// 7 - Try to send a transaction with a memo (should succeed)


// 8 - Disable required memo transfers


// 9 - Try to send a transaction without a memo (should succeed)


// 10 - Verify the memo requirement toggle

}

// Call the main function
main().then(() => {
console.log("🎉 - Demo complete.");
}).catch((err) => {
console.error("⚠️ - Demo failed: ", err);
});

A continuación, describimos los pasos que seguiremos para crear, acuñar y realizar algunas pruebas con nuestros tokens Token-2022. Hemos añadido los primeros pasos: definir varias constantes importantes y realizar un airdrop de SOL a nuestros pagador cuenta. Esto es necesario para pagar las comisiones de la transacción. A continuación, te ofrecemos un resumen de nuestras declaraciones:

  • conexión - la conexión a un clúster local de Solana (si prefieres utilizar devnet o mainnet, solo tienes que cambiar la URL de conexión por la tuya Punto final RPC de Quicknode)
  • decimales - el número de decimales de nuestro token
  • importeDeLaTransferencia - el número de tokens que transferiremos como BigInt
  • pagador - la cuenta que pagará las comisiones de la transacción y será la titular de la cuenta de origen del token
  • mintAuthority - la cuenta que tendrá autoridad para acuñar monedas
  • propietario - la cuenta que será titular de la cuenta de destino
  • par de claves de destino - el par de claves de la cuenta de destino
  • destino - la clave pública de la cuenta del token de destino

Vamos a desarrollar los pasos restantes.

Paso 2: Crear un nuevo token

Empecemos por crear nuestro nuevo token.

    // 2 - Crear una moneda
const moneda = await createMint(
conexión,
pagador,
autoridad de emisión.clavePública,
autoridad de emisión.publicKey,
decimales,
indefinido,
indefinido,
TOKEN_2022_PROGRAM_ID
);

Utilizamos el habitual createMint método y asegúrate de pasarle el TOKEN_2022_PROGRAM_ID. Si no utilizas el TOKEN_2022_PROGRAM_ID, no podrás utilizar la funcionalidad de requisito de memo (el valor por defecto es la versión anterior del programa de tokens). Si has completado otra de nuestras guías sobre Token-2022, quizá te preguntes: ¿por qué no utilizamos las extensiones de Token-2022 al inicializar nuestra emisión? La razón es que el requisito de memo es un Cuenta-requisito de nivel, no un MentaRequisito de nivel. Esto significa que el requisito de nota se establece al inicializar la cuenta de un nuevo titular de tokens. Hagámoslo a continuación.

Paso 3: Acuñar tokens a nombre del propietario

Ahora que ya tenemos una máquina de acuñar tokens, ¡vamos a acuñar algunos! Añade lo siguiente a tu main() función:

    // 3 - Create a destination account with memo requirement enabled
const accountLen = getAccountLen([ExtensionType.MemoTransfer]);
const lamports = await connection.getMinimumBalanceForRentExemption(accountLen);
const transaction = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: destination,
space: accountLen,
lamports,
programId: TOKEN_2022_PROGRAM_ID,
}),
createInitializeAccountInstruction(destination, mint, owner.publicKey, TOKEN_2022_PROGRAM_ID),
createEnableRequiredMemoTransfersInstruction(destination, owner.publicKey)
);
await sendAndConfirmTransaction(connection, transaction, [payer, owner, destinationKeypair], undefined);

En esta sección, utilizamos el getAccountLen función (similar a getMintLen pero dirigido a Cuentas (utilizando las extensiones de Token-2022) para determinar el tamaño de la cuenta que vamos a crear. A continuación, utilizamos el obtenerSaldoMínimoParaExenciónDeAlquiler función para determinar el saldo mínimo de Lamport necesario para que la cuenta mantenga la exención del pago del alquiler.

Utilizamos el SystemProgram.createAccount y createInitializeAccountInstruction para inicializar la nueva cuenta de tokens. Asegúrate de pasar el TOKEN_2022_PROGRAM_ID como identificador de programa para ambas instrucciones; de lo contrario, no podrás utilizar la funcionalidad de requisitos de nota. Ten en cuenta que no podemos utilizar el createAssociatedTokenAccount función porque necesitamos utilizar un tamaño de cuenta personalizado y «lamports» debido al uso de la extensión Token 2022.

Por último, utilizamos una nueva función, createEnableRequiredMemoTransfersInstruction, para habilitar el requisito de nota para la nueva cuenta de token. Esta función admite los siguientes parámetros: la cuenta y la autoridad. Envía la transacción al clúster y confírmala utilizando enviarYConfirmarTransacción antes de pasar al siguiente paso.

Paso 4: Emisión de tokens a la cuenta de origen

Ahora que tenemos un destino cuenta, tenemos que acuñar algunos tokens en una cuenta de origen. Añade lo siguiente a tu main() función:

    // 4 - Mint tokens to source account (owned by the payer)
const sourceAccount = await createAssociatedTokenAccountIdempotent(connection, payer, mint, payer.publicKey, {}, TOKEN_2022_PROGRAM_ID);

await mintTo(
connection,
payer,
mint,
sourceAccount,
mintAuthority,
Number(transferAmount) * 10,
[],
undefined,
TOKEN_2022_PROGRAM_ID
);

Utilizamos dos funciones habituales para acuñar tokens en nuestra cuenta de origen:

  • createAssociatedTokenAccountIdempotent - crea una cuenta de token asociada para el pagador cuenta. A diferencia de nuestra destino, podemos utilizar el createAssociatedTokenAccountIdempotent función, ya que no estamos utilizando ninguna extensión de Token-2022 para esta cuenta.
  • mintTo - emite tokens a la cuenta de origen utilizando el mintAuthority cuenta. Ten en cuenta que tenemos que convertir nuestro importeDeLaTransferencia de un BigInt a un Número para esta función.

En ambas instrucciones, nos aseguramos de pasar el TOKEN_2022_PROGRAM_ID como identificador del programa para garantizar la compatibilidad con nuestra versión de Mint.

Paso 5: Crear una orden de transferencia de tokens

Ahora que ya tenemos configuradas nuestras cuentas y el token, vamos a crear algunos escenarios para probar el requisito del memo. Empezaremos creando una instrucción de transferencia de tokens que podamos utilizar en todas nuestras pruebas. Añade lo siguiente a tu main() función:

    // 5 - Crear una orden de transferencia
const ix = createTransferCheckedInstruction(
cuentaDeOrigen,
emisión,
destino,
pagador.publicKey,
importeDeLaTransferencia,
decimales,
indefinido,
TOKEN_2022_PROGRAM_ID
)

Esta es una sencilla instrucción de transferencia que utiliza el createTransferCheckedInstruction función. Esta función admite los siguientes parámetros:

  • cuenta de origen - la cuenta del token de origen de la transferencia
  • menta - la casa de la moneda para la ficha
  • destino - la cuenta de tokens de destino de la transferencia
  • propietario (clave pública del pagador) - el titular de la cuenta del token de origen
  • importe (importeDeLaTransferencia) - el número de tokens que se van a transferir
  • decimales: el número de decimales del importe de la transferencia (las instrucciones «marcadas» requieren este parámetro para garantizar que el importe transferido refleje correctamente el número de decimales que tiene el token)
  • firmantes múltiples: no aplicable en esta demostración
  • programId (TOKEN_2022_PROGRAM_ID) — el identificador del programa correspondiente al programa de tokens

Paso 6: Enviar una transacción sin nota

Ahora que ya tenemos nuestra instrucción de transferencia de tokens, intentemos enviar una transacción sin nota. Esta transacción debería fallar, ya que nuestra destino La cuenta tiene activado el requisito de nota. Añade lo siguiente a tu main() función:

    // 6 - Try to send a transaction without a memo (should fail)
try {
const failedTx = new Transaction().add(ix);
const failedTxSig = await sendAndConfirmTransaction(connection, failedTx, [payer], undefined);
console.log("❌ - This should have failed, but didn't. Tx: ", failedTxSig);
} catch (e) {
if (e instanceof SendTransactionError && e.logs) {
const errorMessage = e.logs.join('\n');
if (errorMessage.includes("No memo in previous instruction")) {
// https://github.com/solana-labs/solana-program-library/blob/d755eae17e0a2220f31bfc69548a78be832643af/token/program-2022/src/error.rs#L143
console.log("✅ - Transaction failed without memo (memo is required).");
} else {
console.error(`❌ - Unexpected error: ${errorMessage}`);
}
} else {
console.error(`❌ - Unknown error: ${e}`);
}
}

Aquí estamos creando un nuevo Transacción, failedTx (que recibe este nombre porque esperamos que falle), y le añadimos nuestra instrucción de transferencia de tokens. A continuación, enviamos la transacción al clúster y la confirmamos utilizando enviarYConfirmarTransacción. Si la transacción falla, detectamos el error y consultamos los registros para ver cuál es el mensaje de error. Mostramos un mensaje de éxito si el mensaje de error coincide con el esperado («No hay nota en la instrucción anterior»). De lo contrario, mostramos un mensaje de error.

Paso 7: Enviar una transacción con un memo

Ahora que hemos creado una prueba para comprobar que nuestro requisito de nota bloquea las transacciones sin nota, probemos a realizar una transacción con nota. Añade lo siguiente a tu main() función:

    // 7 - Try to send a transaction with a memo (should succeed)
try {
const memo = createMemoInstruction("Quicknode demo.");
const memoTx = new Transaction().add(memo, ix);
await sendAndConfirmTransaction(connection, memoTx, [payer], undefined);
console.log("✅ - Successful transaction with memo (memo is required).");
} catch (e) {
console.error("❌ - Something went wrong. Tx failed unexpectedly: ", e);
}

Al igual que en el paso anterior, creamos un nuevo Transacción, memoTx. A diferencia del paso anterior, creamos y añadimos una instrucción de nota a la transacción utilizando el createMemoInstruction función. Es importante señalar que la instrucción «memo» debe añadirse antes de la instrucción de transferencia de tokens (.add(memo, ix) funciona, pero .add(ix, memo) (no lo hace). A continuación, enviamos la transacción al clúster y la confirmamos utilizando enviarYConfirmarTransacción. Si la transacción falla, detectamos el error y mostramos un mensaje de error. En caso contrario, mostramos un mensaje de éxito.

Así pues, ya hemos comprobado el requisito del memo para nuestra cuenta de tokens (asegurándonos tanto de que las transacciones sin memo fallen como de que las transacciones con memo se realicen correctamente). ¿Y si quisiéramos desactivar ese requisito? Intentémoslo a continuación.

Paso 8: Desactivar el requisito de nota

Solana ha incorporado una función muy fácil de usar, desactivar las transferencias de notas obligatorias a la biblioteca de tokens SPL. Añádela a tu main() función:

    // 8 - Desactivar las transferencias de notas obligatorias
await desactivarTransferenciasDeNotasObligatorias(conexión, pagador, destino, propietario);

Esta función requiere que introduzcas una conexión, un pagador de la tarifa, la cuenta que deseas actualizar y el titular o la persona autorizada de la cuenta. A continuación, enviará una transacción al clúster para desactivar el requisito del memo.

Comprobemos si ha funcionado como se esperaba.

Paso 9: Comprueba que la opción «Requisito de nota» esté desactivada

Ahora que se ha desactivado el requisito del memo, deberíamos poder enviar una transacción sin memo. Añade lo siguiente a tu main() función:

    // 9 - Try to send a transaction without a memo (should succeed)
try {
const noMemoTx = new Transaction().add(ix);
await sendAndConfirmTransaction(connection, noMemoTx, [payer], undefined);
console.log("✅ - Successful transaction without memo (memo is NOT required).");
} catch (e) {
console.error("❌ - Something went wrong. Tx failed unexpectedly: ", e);
}

Esto es muy similar a nuestra primera prueba, pero esperamos que esta vez la transacción se realice con éxito. Creamos un nuevo Transacción, noMemoTx, añadimos nuestra instrucción de transferencia de tokens y la enviamos al clúster. Si la transacción falla, detectamos el error y mostramos un mensaje de error. De lo contrario, mostramos un mensaje de éxito.

Aunque esto funciona para una prueba sencilla como esta, ¿qué pasaría si tuviéramos que comprobar si una cuenta de tokens tiene el requisito del memo activado o desactivado? Probemos eso a continuación.

Paso 10: Comprueba si el requisito de nota está activado

En las pruebas anteriores, conocíamos (o teníamos una idea bastante clara) del estado del requisito de nota de la cuenta (lo activamos según lo requerido y posteriormente lo desactivamos). ¿Y si no lo supiéramos? Necesitamos una forma de consultarlo. Creemos una función para comprobarlo. A continuación, tu main() función, añade la siguiente función:

async function verifyMemoRequirement(tokenAccount: PublicKey, connection: Connection): Promise<boolean> {
const accountInfo = await connection.getAccountInfo(tokenAccount);
const account = unpackAccount(tokenAccount, accountInfo, TOKEN_2022_PROGRAM_ID);
const memoDetails = getMemoTransfer(account);
if (!memoDetails) {
throw new Error("Memo details not found.");
}
return memoDetails.requireIncomingTransferMemos;
}

Nuestra función asíncrona acepta una dirección de cuenta de tokens y un objeto `Connection`, y devuelve una promesa con un valor booleano (que indica si se requiere o no un «memo» para enviar tokens a la cuenta). Así es como funciona:

  1. Obtenemos la información de la cuenta del token mediante getAccountInfo().
  2. Analizamos el relato utilizando unpackAccount(). Esto devolverá un Cuenta objeto con los datos de la cuenta.
  3. Obtenemos los detalles del memorándum mediante getMemoTransfer(). Esto devolverá un objeto con un valor booleano, requireIncomingTransferMemos, que devolvemos.

Ahora vamos a llamarlo así en nuestro main() función para comprobar que el requisito del memo está desactivado. Añade lo siguiente a tu main() función:

    // 10 - Verify the memo requirement toggle
let isMemoRequired = await verifyMemoRequirement(destination, connection);
if (isMemoRequired) {
console.log("❌ - Something's wrong. Expected memo requirement to be disabled.");
} else {
console.log("✅ - Memo requirement disabled.");
}

await enableRequiredMemoTransfers(connection, payer, destination, owner);

isMemoRequired = await verifyMemoRequirement(destination, connection);
if (isMemoRequired) {
console.log("✅ - Memo requirement enabled.");
} else {
console.log("❌ - Something's wrong. Expected memo to be required.");
}

A nuestro verifyMemoRequirement() función pasando el destino cuenta y Solana conexión. A continuación, comprobamos el valor devuelto y mostramos un mensaje de éxito o de error.

A continuación, ejecutamos habilitar las transferencias de notas obligatorias (lo cual es justo lo contrario de desactivar las transferencias de notas obligatorias) para volver a activar el requisito del memorándum. A continuación, llamamos a nuestro verifyMemoRequirement() Vuelve a ejecutar la función y comprueba que ha funcionado como se esperaba.

Ejecutar el código

Por último, en otra ventana de terminal, ejecuta el siguiente comando para iniciar un clúster local de Solana:

solana-test-validator

Y en tu terminal principal, ejecuta tu script:

ts-node app.ts

Deberías ver un resultado similar al siguiente:

% ts-node memo
✅ - Transaction failed without memo (memo is required).
✅ - Successful transaction with memo (memo is required).
✅ - Successful transaction without memo (memo is NOT required).
✅ - Memo requirement disabled.
✅ - Memo requirement enabled.
🎉 - Demo complete.

¡Buen trabajo!


Extra: aplicar un requisito de nota explicativa a una cuenta ya existente

En nuestra guía, hemos creado una nueva cuenta de tokens que hemos inicializado con el requisito de memo. ¿Qué pasaría si quisiéramos añadir el requisito de memo a una cuenta ya existente?

Podemos hacerlo utilizando el createReallocateInstruction() función de la biblioteca de tokens SPL. Esta función nos permite añadir lamports adicionales a una cuenta en función de las extensiones que se le pasen como parámetros. Podemos pasar el MemoTransfer Tipo de extensión para garantizar una reasignación correcta. A continuación, podemos utilizar el createEnableRequiredMemoTransfersInstruction() función para activar el requisito de la nota.

No dudes en añadir el n.º 11 que aparece a continuación en tu main() función para probarlo (asegúrate de actualizar tus importaciones).

    // Additional imports required
import { createAccount, createReallocateInstruction } from '@solana/spl-token';

// ...

// 11 - Bonus - add memo requirement to an existing account
try {
// Create a new token account without a memo requirement
const newOwner = Keypair.generate();
const bonusAccount = await createAccount(
connection,
payer,
mint,
newOwner.publicKey,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID
);

const extensions = [ExtensionType.MemoTransfer];
const addExtensionTx = new Transaction().add(
// Create a reallocate instruction to add lamports for the the memo requirement
createReallocateInstruction(
bonusAccount,
payer.publicKey,
extensions,
newOwner.publicKey
),
// Create an instruction to enable the memo requirement
createEnableRequiredMemoTransfersInstruction(bonusAccount, newOwner.publicKey)
);
await sendAndConfirmTransaction(connection, addExtensionTx, [payer, newOwner]);
console.log("✅ - Memo requirement added to existing account.");
} catch (e) {
console.error("❌ - Something went wrong. Tx failed unexpectedly: ", e);
}

Conclusión

Recapitulemos lo que hemos hecho hasta ahora:

  • Hemos creado y emitido un nuevo token Token-2022.
  • Hemos creado una nueva cuenta de tokens que requiere un campo de nota.
  • Intentamos enviar tokens a la cuenta sin nota (no funcionó).
  • Intentamos enviar tokens a la cuenta con un memo (se realizó correctamente).
  • Hemos eliminado el requisito de presentar una nota.
  • Intentamos enviar tokens a la cuenta sin nota (se realizó correctamente).
  • Comprobamos que el requisito del memorándum estuviera desactivado, lo volvimos a activar y comprobamos que estuviera activado.

Esperamos que esta guía te haya ayudado a comprender cómo funciona la función de requisitos de notas del Programa Token-2022.

Nos encantaría saber más sobre lo que estás desarrollando y cómo piensas utilizar Token-2022 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.

Comparte esta guía