17 minutos de lectura
Resumen
Las «Token Extensions» (también conocidas como Token-2022) son una primitiva recién lanzada por el equipo de Solana Labs. Este innovador estándar aporta un mayor control sobre tu tokenómica. Aprovecha estas nuevas herramientas para disfrutar de una experiencia de desarrollo en Solana más dinámica. En esta guía, exploraremos las últimas funcionalidades relacionadas con el programa Token-2022 y aprenderemos a crear, acuñar y transferir tu primer token utilizando la nueva extensión «Transfer Fees».
Tus funciones
En los siguientes apartados, veremos paso a paso cómo crear un token, acuñarlo y transferirlo a otro monedero. También recaudaremos las comisiones de transferencia y las retiraremos a un monedero designado:
- Crea un token utilizando la ampliación del plazo para el pago de comisiones del programa Token-2022
- Transferir el token y cobrar las comisiones de transferencia
- Cobrar y retirar las tasas recaudadas
Lo que necesitarás
- Conocimientos básicos sobre los fundamentos de Solana
- Conocimientos básicos sobre carteras Web3
- Experiencia con los tokens SPL de Solana (Cómo acuñar tokens fungibles | Cómo obtener todos los tokens que contiene una cartera)
- Se ha instalado la última versión de la CLI de Solana
| Dependencia | Versión |
|---|---|
| node.js | 18.12.1 |
| tsc | 5.0.2 |
| ts-node | 10.9.1 |
| solana-cli | 1.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 && cd token-2022
npm init -y # o yarn init -y
npm install @solana/web3.js@1 @solana/spl-token # o bien yarn add @solana/web3.js@1 @solana/spl-token
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,
Cluster,
PublicKey,
TransactionSignature,
SignatureStatus,
TransactionConfirmationStatus
} from '@solana/web3.js';
import {
ExtensionType,
createInitializeMintInstruction,
mintTo,
createAccount,
getMintLen,
getTransferFeeAmount,
unpackAccount,
TOKEN_2022_PROGRAM_ID,
createInitializeTransferFeeConfigInstruction,
harvestWithheldTokensToMint,
transferCheckedWithFee,
withdrawWithheldTokensFromAccounts,
withdrawWithheldTokensFromMint,
getOrCreateAssociatedTokenAccount,
createAssociatedTokenAccountIdempotent
} from '@solana/spl-token';
// Initialize connection to local Solana node
const connection = new Connection('http://127.0.0.1:8899', 'confirmed');
// Generate keys for payer, mint authority, and mint
const payer = Keypair.generate();
const mintAuthority = Keypair.generate();
const mintKeypair = Keypair.generate();
const mint = mintKeypair.publicKey;
// Generate keys for transfer fee config authority and withdrawal authority
const transferFeeConfigAuthority = Keypair.generate();
const withdrawWithheldAuthority = Keypair.generate();
// Define the extensions to be used by the mint
const extensions = [
ExtensionType.TransferFeeConfig,
];
// Calculate the length of the mint
const mintLen = getMintLen(extensions);
// Set the decimals, fee basis points, and maximum fee
const decimals = 9;
const feeBasisPoints = 100; // 1%
const maxFee = BigInt(9 * Math.pow(10, decimals)); // 9 tokens
// Define the amount to be minted and the amount to be transferred, accounting for decimals
const mintAmount = BigInt(1_000_000 * Math.pow(10, decimals)); // Mint 1,000,000 tokens
const transferAmount = BigInt(1_000 * Math.pow(10, decimals)); // Transfer 1,000 tokens
// Calculate the fee for the transfer
const calcFee = (transferAmount * BigInt(feeBasisPoints)) / BigInt(10_000); // expect 10 fee
const fee = calcFee > maxFee ? maxFee : calcFee; // expect 9 fee
// Helper function to generate Explorer URL
function generateExplorerTxUrl(txId: string) {
return `https://explorer.solana.com/tx/${txId}?cluster=custom&customUrl=http%3A%2F%2Flocalhost%3A8899`;
}
Importamos las dependencias necesarias desde @solana/web3.js y @solana/spl-token. Los utilizaremos para crear, acuñar y transferir tokens Token-2022. Ten en cuenta que aquí estamos estableciendo una conexión con el clúster local. Si prefieres utilizar devnet o mainnet, solo tienes que cambiar la URL de conexión por tu punto final RPC de Quicknode. Además de crear las cuentas de autoridad necesarias y calcular el importe previsto de las comisiones, hemos añadido una función auxiliar para generar una URL de la transacción en Solana Explorer. Esto resultará útil para consultar los detalles de la transacción.
Hay un par de novedades en el programa Token-2022:
-
transferFeeConfigAuthorityyretirar la autorización retenidaSe han creado nuevas autorizaciones para controlar la configuración de las comisiones por transferencia y la retirada de las comisiones recaudadas. Las utilizaremos más adelante en el tutorial. -
El
extensionesEl array se utiliza para definir las extensiones que utilizará la casa de moneda. El programa Token-2022 admite 13 extensiones, pero solo utilizaremos laTransferFeeConfigextensión para este ejemplo. ElextensiónEl array calcula cuánto espacio se necesita para nuestras cuentas Token-2022 (las cuentas de acuñación ya no tienen todas 165 bytes, sino que varían en función de las extensiones utilizadas). -
También calculamos el importe previsto de la comisión (que utilizaremos más adelante en nuestra función de transferencia). Esto se debe a que debemos incluir el importe de la comisión en nuestra instrucción (importe previsto) para comprobar que coincide con el importe calculado cuando se ejecute la instrucción. Si no coincide, la transacción fallará. El objetivo es proteger a los usuarios para que no envíen por error una transacción sin conocer con exactitud el importe de la comisión.
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() {
// Step 1 - Airdrop to Payer
const airdropSignature = await connection.requestAirdrop(payer.publicKey, 2 * LAMPORTS_PER_SOL);
await confirmTransaction(connection, airdropSignature);
// Step 2 - Create a New Token
// Step 3 - Mint tokens to Owner
// Step 4 - Send Tokens from Owner to a New Account
// Step 5 - Fetch Fee Accounts
// Step 6 - Harvest Fees
}
// Execute the main function
main();
A continuación, describimos los pasos que seguiremos para crear, acuñar y transferir nuestros tokens Token-2022. Hemos añadido el primer paso: realizar un airdrop de SOL a nuestra cuenta de pago. Esto es necesario para pagar las comisiones de transacción.
Vamos a desarrollar los pasos restantes.
Paso 2: Crear un nuevo token
Empecemos por crear nuestro nuevo token.
// Step 2 - Create a New Token
const mintLamports = await connection.getMinimumBalanceForRentExemption(mintLen);
const mintTransaction = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: mint,
space: mintLen,
lamports: mintLamports,
programId: TOKEN_2022_PROGRAM_ID,
}),
createInitializeTransferFeeConfigInstruction(
mint,
transferFeeConfigAuthority.publicKey,
withdrawWithheldAuthority.publicKey,
feeBasisPoints,
maxFee,
TOKEN_2022_PROGRAM_ID
),
createInitializeMintInstruction(mint, decimals, mintAuthority.publicKey, null, TOKEN_2022_PROGRAM_ID)
);
const newTokenTx = await sendAndConfirmTransaction(connection, mintTransaction, [payer, mintKeypair], undefined);
console.log("New Token Created:", generateExplorerTxUrl(newTokenTx));
Este paso es prácticamente igual que la creación de un token mediante el antiguo proceso de SPL, con un par de diferencias importantes:
- En primer lugar, debemos calcular el saldo mínimo necesario para crear el nuevo token. Para ello, hay que pasar nuestro
mintLenen elobtenerSaldoMínimoParaExenciónDeAlquilerfunción. De este modo, nos aseguraremos de que nuestra cuenta de emisión de tokens disponga de espacio suficiente para las extensiones que utilizamos. - Incluimos una nueva instrucción en nuestra transacción para inicializar la configuración de la comisión de transferencia. Aquí definimos:
comisión en puntos básicos- Los puntos básicos de comisión (1 punto básico = 0,01 %) que se aplicarán a las transferencias de este token (un porcentaje del importe de la transacción, con un límite máximo demaxFee).maxFee- La comisión máxima que se cobrará por las transferencias de este token (un valor absoluto, no un porcentaje).transferFeeConfigAuthority- La autoridad que se utilizará para controlar la configuración de las tasas de transferencia.retirar la autorización retenida- La autoridad que se utilizará para retirar las tasas recaudadas.
- Por último, ten en cuenta que nuestras instrucciones de transacción utilizan el
TOKEN_2022_PROGRAM_IDcomo ID del programa. Este es el ID del programa «Token-2022». La transacción fallará si intentas ejecutar esta instrucción con el antiguo ID del programa del token SPL.
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:
// Step 3 - Mint tokens to Owner
const owner = Keypair.generate();
const sourceAccount = await createAssociatedTokenAccountIdempotent(connection, payer, mint, owner.publicKey, {}, TOKEN_2022_PROGRAM_ID);
const mintSig = await mintTo(connection,payer,mint,sourceAccount,mintAuthority,mintAmount,[],undefined,TOKEN_2022_PROGRAM_ID);
console.log("Tokens Minted:", generateExplorerTxUrl(mintSig));
En esta sección, vamos a crear un nuevo par de claves para el propietario del token y a configurar una cuenta de origen para nuestra emisión. El createAssociatedTokenAccountIdempotent Esta función se utiliza para inicializar una nueva cuenta de tokens asociada en la que se almacenarán los tokens de nuestra nueva emisión (con una cantidad inicial de nuestros menta, propietario, y el ID del programa Token-2022). Utilizaremos esto cuenta de origen para transferir tokens más adelante.
A continuación, utilizando el mintTo En esta función, acuñamos tokens en nuestra cuenta de origen. Registramos la firma devuelta y una URL para consultar la transacción en Solana Explorer.
Una vez más, ten en cuenta que utilizamos TOKEN_2022_PROGRAM_ID como identificador de programa para nuestras transacciones ATA y MINT.
Paso 4: Enviar tokens desde la cuenta del titular a una nueva cuenta
Ahora que ya tenemos tokens en nuestra cuenta de origen, vamos a enviarlos a una nueva cuenta. Añade lo siguiente a tu main() función:
// Step 4 - Send Tokens from Owner to a New Account
const destinationOwner = Keypair.generate();
const destinationAccount = await createAssociatedTokenAccountIdempotent(connection, payer, mint, destinationOwner.publicKey, {}, TOKEN_2022_PROGRAM_ID);
const transferSig = await transferCheckedWithFee(
connection,
payer,
sourceAccount,
mint,
destinationAccount,
owner,
transferAmount,
decimals,
fee,
[]
);
console.log("Tokens Transfered:", generateExplorerTxUrl(transferSig));
Al igual que en el paso anterior, creamos una nueva cartera, propietario del destino, y una nueva cuenta de tokens asociada, cuenta de destino, para definir a dónde se destinarán nuestros fondos. A continuación, llamamos a una función nueva de Token-2022, transferenciaComprobadaConComisión, para transferir tokens desde nuestro cuenta de origen a nuestro cuenta de destino. Esta función es similar al antiguo token SPL transferChecked (lo cual requiere un decimales parámetro a comprobar (que el cliente envía el número previsto de tokens). La diferencia es que ahora también comprobamos el importe previsto de la comisión. Esto significa que debemos calcular el importe previsto de la comisión en el lado del cliente y pasarlo como parámetro. En el backend, el parámetro de la comisión se compara con el comisión en puntos básicos y maxFee los valores que hemos establecido al inicializar la configuración de la comisión de transferencia. Si la comisión no es la esperada, la transacción fallará. A continuación se muestra un desglose de todos los parámetros que estamos pasando:
conexión- La conexión al clúster de Solana.pagador- La cuenta del pagador.fuente- La cuenta de tokens asociada a la fuente.menta- La casa de acuñación del token que se va a transferir.destino- La cuenta de tokens asociada al destino.propietario- El titular de la cuenta asociada al token de origen.cantidad- El número de tokens que se van a transferir (como recordarás, es el número de tokens que queremos enviar multiplicado por 10^decimales).decimales- El número de decimales del token que estamos transfiriendo.cuota- El importe previsto de la comisión por la transferencia (si recuerdas, cuando lo calculamos, comprobamos que el valor era menor o igual quemaxFee, de lo contrario, utilizamosmaxFee).- No utilizamos ninguna firma múltiple para esta transacción.
Nota: dado que esta transacción es propia de Token-2022 y no está disponible en el programa SPL Token, TOKEN_2022_PROGRAM_ID es el valor por defecto y no es necesario indicarlo.
Aún nos queda algo de trabajo por hacer, pero si ejecutas tu programa ahora, podrás comprobar que la transacción se realiza con éxito y que los tokens se transfieren a la nueva cuenta (una vez deducidas las comisiones).
En otra ventana de terminal, puedes iniciar un validador local con el siguiente comando:
solana-test-validator
Y en tu terminal principal, ejecuta tu script:
ts-node app.ts
Deberías ver un resultado similar al siguiente:
Nuevo token creado: https://explorer.solana.com/tx/TXID_1?cluster=custom&customUrl=http%3A%2F%2Flocalhost%3A8899
Token acuñados: https://explorer.solana.com/tx/TXID_2?cluster=custom&customUrl=http%3A%2F%2Flocalhost%3A8899
Tokens transferidos: https://explorer.solana.com/tx/TXID_3?cluster=custom&customUrl=http%3A%2F%2Flocalhost%3A8899
Si abres la URL del explorador correspondiente a «Tokens transferidos», deberías ver que se transfirieron 1.000 tokens de la cuenta de origen a la cuenta de destino, pero solo se recibieron 991 tokens. Los 9 tokens restantes se retuvieron en concepto de comisiones:

¡Vamos a buscar esas fichas!
Paso 5: Recuperar cuentas de comisiones
Para retirar las comisiones cobradas por las transferencias, debemos localizar las cuentas en las que se encuentran. Se trata de un proceso de dos pasos:
- Recupera todas las cuentas de tokens asociadas a nuestra emisión. Esto se parece mucho a Recuperar todos los tokens que contiene una cartera, salvo que estamos utilizando
TOKEN_2022_PROGRAM_IDen lugar deTOKEN_PROGRAM_ID. Añade lo siguientegetProgramAccountsconsulta a tumain()función:
// Step 5 - Fetch Fee Accounts
// Note: getProgramAccounts defaults to binary encoding, which is generally discouraged and should only
// be used when you need raw account data for custom deserialization (as we do later with unpackAccount).
// In most cases, prefer getParsedProgramAccounts (web3.js) which returns already-parsed account data.
const allAccounts = await connection.getProgramAccounts(TOKEN_2022_PROGRAM_ID, {
commitment: 'confirmed',
filters: [
{
memcmp: {
offset: 0,
bytes: mint.toString(),
},
},
],
});
Esta solicitud busca cuentas pertenecientes al programa Token-2022 y las filtra según la acuñación que nos interesa (la acuñación se encuentra en la posición 0 de los datos de la cuenta).
- Analiza cada cuenta para comprobar si hay comisiones por retirada de fondos. Añade el siguiente código después de tu
getProgramAccountsconsulta:
const accountsToWithdrawFrom: PublicKey[] = [];
for (const accountInfo of allAccounts) {
const account = unpackAccount(accountInfo.pubkey, accountInfo.account, TOKEN_2022_PROGRAM_ID);
const transferFeeAmount = getTransferFeeAmount(account);
if (transferFeeAmount !== null && transferFeeAmount.withheldAmount > BigInt(0)) {
accountsToWithdrawFrom.push(accountInfo.pubkey);
}
}
Esto es lo que estamos haciendo:
- Creamos un array vacío,
cuentasDeLasQueSeVaARretirar, para mantener las cuentas en las que se aplican comisiones por retirada de fondos. - Recorremos una por una cada una de las cuentas devueltas por nuestra
getProgramAccountsconsulta. - Descomprimimos los datos de la cuenta utilizando el
descomprimirCuentafunción para deserializar los datos en unTokenAccountobjeto. - Comprobamos si la cuenta tiene alguna comisión por retirada de fondos utilizando el
getTransferFeeAmountfunción. Si la cuenta tiene comisiones por retirada de fondos, las añadimos a nuestrocuentasDeLasQueSeVaARretirarmatriz.
¡Buen trabajo! ¡Ahora vamos a cobrar esos honorarios!
Paso 6: Comisiones por cosecha
Un aspecto importante que hay que tener en cuenta sobre las comisiones es que se acumulan en la cuenta del destinatario, en lugar de en una cuenta central de comisiones. El objetivo es maximizar la paralelización de las transacciones; de lo contrario, una única cuenta de comisiones tendría que estar bloqueada para escritura entre transferencias paralelas, lo que reduciría el rendimiento del protocolo. Esto también significa que una cuenta de tokens solo puede cerrarse una vez que se hayan retirado todas las comisiones. Por este motivo, hay dos formas de retirar las comisiones: una activada por el retirar la autorización retenida (utilizando el retirar los tokens retenidos de las cuentas método) y el otro activado por el titular de la cuenta (mediante el recogerTokensRetidosParaAcuñar método).
Analicemos ambos enfoques:
- Retirada por orden de las autoridades
- Retirada por parte del propietario
// Step 6 Withdraw Fees by Authority
const feeVault = Keypair.generate();
const feeVaultAccount = await createAssociatedTokenAccountIdempotent(connection, payer, mint, feeVault.publicKey, {}, TOKEN_2022_PROGRAM_ID);
const withdrawSig1 = await withdrawWithheldTokensFromAccounts(
connection,
payer,
mint,
feeVaultAccount,
withdrawWithheldAuthority,
[],
accountsToWithdrawFrom
);
console.log("Withdraw from Accounts:", generateExplorerTxUrl(withdrawSig1));
// Step 6 - Withdraw Fees by Owner
const feeVault = Keypair.generate();
const feeVaultAccount = await createAssociatedTokenAccountIdempotent(connection, payer, mint, feeVault.publicKey, {}, TOKEN_2022_PROGRAM_ID);
const harvestSig = await harvestWithheldTokensToMint(connection, payer, mint, [destinationAccount]);
console.log("Harvest by Owner:", generateExplorerTxUrl(harvestSig));
// Tokens that have been harvested to Mint can then be claimed by the authority
const withdrawSig2 = await withdrawWithheldTokensFromMint(
connection,
payer,
mint,
feeVaultAccount,
withdrawWithheldAuthority,
[]
);
console.log("Withdraw from Mint:", generateExplorerTxUrl(withdrawSig2));
En ambos casos, creamos un nuevo monedero, feeVault (y la cuenta de tokens asociada, feeVaultAccount), para retener las comisiones que estamos cobrando.
Comisiones por retirada según la entidad
Utilizamos el retirar los tokens retenidos de las cuentas método para retirar las comisiones de las cuentas que hemos encontrado en el paso 5. Este método requiere la autorización para firmar la transacción, por lo que pasamos el retirar la autorización retenida monedero.
Comisiones por retirada de fondos por parte del titular
- En primer lugar, utilizamos el
recogerTokensRetidosParaAcuñarmétodo para cobrar las comisiones de la cuenta del destinatario y transferirlas a la casa de acuñación. Este método requiere que el titular firme la transacción, por lo que pasamos elcuenta de destinomonedero. La razón más probable para hacerlo en una aplicación práctica es que el usuario (o el programa) vaya a cerrar la cuenta y necesite retirar todas las comisiones antes de cerrarla. - Una vez que las comisiones se hayan transferido a la cuenta de la casa de la moneda, la autoridad podrá retirarlas mediante el
retirar los tokens retenidos de la casa de monedamétodo. Este método requiere que la autoridad firme la transacción, por lo que le pasamos elretirar la autorización retenidamonedero. Esto difiere ligeramente del primer caso, en el que la autoridad retiraba las comisiones de la cuenta del destinatario; en este caso, las retirábamos de la cuenta de la casa de moneda.
Te recomendamos que pruebes ambos ejemplos y que explores las transacciones en el Solana Explorer utilizando los enlaces que aparecen en la consola.
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:
Nuevo token creado: https://explorer.solana.com/tx/TXID_1?cluster=custom&customUrl=http%3A%2F%2Flocalhost%3A8899
Token acuñados: https://explorer.solana.com/tx/TXID_2?cluster=custom&customUrl=http%3A%2F%2Flocalhost%3A8899
Tokens transferidos: https://explorer.solana.com/tx/TXID_3?cluster=custom&customUrl=http%3A%2F%2Flocalhost%3A8899
Retirar fondos de las cuentas: https://explorer.solana.com/tx/TXID_4?cluster=custom&customUrl=http%3A%2F%2Flocalhost%3A8899
Una vez más, revisa las transacciones en Solana Explorer utilizando los enlaces que aparecen en la consola; esto te ayudará a comprender el flujo de comisiones en el programa Token-2022.
¡Buen trabajo!
Conclusión
Acabas de crear un nuevo token, acuñar algunos tokens, transferir tokens y retirar comisiones de la cuenta del destinatario. ¡Ahora puedes utilizar este código como punto de partida para tus propios proyectos! Si hay algo que debes recordar al iniciar tu andadura con Token-2022, es asegurarte de que tu programa utilice el ID de programa correcto.
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.
