¿Qué Son los Tokens No Transferibles?
La extensión de mint NonTransferable del Token Extensions Program hace que
cada token account para ese mint sea no transferible. Después de que los tokens
son acuñados, los titulares de tokens no pueden moverlos a otra token account
con Transfer o TransferChecked.
Este patrón es útil para activos que deben permanecer asociados a una sola billetera.
Los tokens no transferibles aún pueden ser:
- Acuñados por la autoridad del mint
- Quemados por el propietario de la token account o un delegado autorizado
- Las token accounts pueden cerrarse después de que el saldo llegue a cero
Extensiones de Token Account
Cuando se inicializa una token account para un mint con NonTransferable,
la token account se inicializa con NonTransferableAccount y
ImmutableOwner. Cuando la token account se crea a través del Associated
Token
Program,
se calcula el tamaño de cuenta requerido y la token account se crea con el
tamaño necesario y los lamports exentos de rent.
Cómo Crear un Mint No Transferible
Para crear un mint no transferible:
- Calcula el tamaño de la mint account y el rent necesario para el mint y la
extensión
NonTransferable. - Crea la mint account con
CreateAccount, inicializaNonTransferablee inicializa el mint conInitializeMint. - Crea token accounts para el mint.
NonTransferableAccountyImmutableOwnerse habilitan automáticamente para las token accounts. TransferyTransferCheckedfallan conTokenError::NonTransferable.
Calcular el tamaño de la cuenta
Calcula el tamaño de la mint account para el mint base más la extensión
NonTransferable. Este es el tamaño utilizado en CreateAccount.
const client = await createClient().use(generatedPayer()).use(solanaRpc({rpcUrl: "http://localhost:8899",rpcSubscriptionsUrl: "ws://localhost:8900"})).use(rpcAirdrop()).use(airdropPayer(lamports(1_000_000_000n)));const mint = await generateKeyPairSigner();const recipient = await generateKeyPairSigner();const nonTransferableExtension = extension("NonTransferable", {});const mintSpace = BigInt(getMintSize([nonTransferableExtension]));
Calcular rent
Calcula el rent utilizando el tamaño necesario para el mint más la extensión
NonTransferable.
const client = await createClient().use(generatedPayer()).use(solanaRpc({rpcUrl: "http://localhost:8899",rpcSubscriptionsUrl: "ws://localhost:8900"})).use(rpcAirdrop()).use(airdropPayer(lamports(1_000_000_000n)));const mint = await generateKeyPairSigner();const recipient = await generateKeyPairSigner();const nonTransferableExtension = extension("NonTransferable", {});const mintSpace = BigInt(getMintSize([nonTransferableExtension]));const mintRent = await client.rpc.getMinimumBalanceForRentExemption(mintSpace).send();
Crear la mint account
Crea la mint account con el espacio y lamports calculados.
const client = await createClient().use(generatedPayer()).use(solanaRpc({rpcUrl: "http://localhost:8899",rpcSubscriptionsUrl: "ws://localhost:8900"})).use(rpcAirdrop()).use(airdropPayer(lamports(1_000_000_000n)));const mint = await generateKeyPairSigner();const recipient = await generateKeyPairSigner();const nonTransferableExtension = extension("NonTransferable", {});const mintSpace = BigInt(getMintSize([nonTransferableExtension]));const mintRent = await client.rpc.getMinimumBalanceForRentExemption(mintSpace).send();await client.sendTransaction([getCreateAccountInstruction({payer: client.payer,newAccount: mint,lamports: mintRent,space: mintSpace,programAddress: TOKEN_2022_PROGRAM_ADDRESS})]);
Inicializar NonTransferable
Inicializa la extensión NonTransferable en el mint.
const client = await createClient().use(generatedPayer()).use(solanaRpc({rpcUrl: "http://localhost:8899",rpcSubscriptionsUrl: "ws://localhost:8900"})).use(rpcAirdrop()).use(airdropPayer(lamports(1_000_000_000n)));const mint = await generateKeyPairSigner();const recipient = await generateKeyPairSigner();const nonTransferableExtension = extension("NonTransferable", {});const mintSpace = BigInt(getMintSize([nonTransferableExtension]));const mintRent = await client.rpc.getMinimumBalanceForRentExemption(mintSpace).send();await client.sendTransaction([getCreateAccountInstruction({payer: client.payer,newAccount: mint,lamports: mintRent,space: mintSpace,programAddress: TOKEN_2022_PROGRAM_ADDRESS}),getInitializeNonTransferableMintInstruction({mint: mint.address})]);
Inicializar el mint
Inicializa el mint con InitializeMint en la misma transacción.
const client = await createClient().use(generatedPayer()).use(solanaRpc({rpcUrl: "http://localhost:8899",rpcSubscriptionsUrl: "ws://localhost:8900"})).use(rpcAirdrop()).use(airdropPayer(lamports(1_000_000_000n)));const mint = await generateKeyPairSigner();const recipient = await generateKeyPairSigner();const nonTransferableExtension = extension("NonTransferable", {});const mintSpace = BigInt(getMintSize([nonTransferableExtension]));const mintRent = await client.rpc.getMinimumBalanceForRentExemption(mintSpace).send();await client.sendTransaction([getCreateAccountInstruction({payer: client.payer,newAccount: mint,lamports: mintRent,space: mintSpace,programAddress: TOKEN_2022_PROGRAM_ADDRESS}),getInitializeNonTransferableMintInstruction({mint: mint.address}),getInitializeMintInstruction({mint: mint.address,decimals: 0,mintAuthority: client.payer.address,freezeAuthority: client.payer.address})]);
Orden de Instrucciones
InitializeNonTransferableMint debe ejecutarse antes de
InitializeMint. CreateAccount, InitializeNonTransferableMint
y InitializeMint deben incluirse en la misma transacción.
Referencia del Código Fuente
| Elemento | Descripción | Fuente |
|---|---|---|
NonTransferable | Extensión de mint que marca los tokens del mint como no transferibles. | Fuente |
NonTransferableAccount | Extensión de token account agregada a las token accounts para mints no transferibles. | Fuente |
ImmutableOwner | Extensión de token account que previene cambios de propiedad y es requerida para token accounts no transferibles. | Fuente |
InitializeNonTransferableMint | Instrucción que inicializa la extensión no transferible a nivel de mint antes de InitializeMint. | Fuente |
process_initialize_non_transferable_mint | Lógica del procesador que inicializa la extensión de mint NonTransferable en un mint no inicializado. | Fuente |
get_required_init_account_extensions | Se utiliza para agregar automáticamente extensiones de token account cuando se inicializan las token accounts basándose en las extensiones habilitadas en el mint. Para mints NonTransferable, agrega NonTransferableAccount e ImmutableOwner. | Fuente |
Typescript
El siguiente ejemplo Kit utiliza las instrucciones generadas directamente. Se
incluyen ejemplos heredados que usan @solana/web3.js como referencia.
Kit
import { lamports, createClient, generateKeyPairSigner } from "@solana/kit";import { solanaRpc, rpcAirdrop } from "@solana/kit-plugin-rpc";import { generatedPayer, airdropPayer } from "@solana/kit-plugin-signer";import { getCreateAccountInstruction } from "@solana-program/system";import {extension,fetchMint,fetchToken,findAssociatedTokenPda,getCreateAssociatedTokenInstructionAsync,getInitializeMintInstruction,getInitializeNonTransferableMintInstruction,getMintSize,getMintToCheckedInstruction,getTransferCheckedInstruction,TOKEN_2022_PROGRAM_ADDRESS} from "@solana-program/token-2022";const client = await createClient().use(generatedPayer()).use(solanaRpc({rpcUrl: "http://localhost:8899",rpcSubscriptionsUrl: "ws://localhost:8900"})).use(rpcAirdrop()).use(airdropPayer(lamports(1_000_000_000n)));const mint = await generateKeyPairSigner();const recipient = await generateKeyPairSigner();const nonTransferableExtension = extension("NonTransferable", {});const mintSpace = BigInt(getMintSize([nonTransferableExtension]));const mintRent = await client.rpc.getMinimumBalanceForRentExemption(mintSpace).send();await client.sendTransaction([getCreateAccountInstruction({payer: client.payer,newAccount: mint,lamports: mintRent,space: mintSpace,programAddress: TOKEN_2022_PROGRAM_ADDRESS}),getInitializeNonTransferableMintInstruction({mint: mint.address // Mint account that stores the NonTransferable extension.}),getInitializeMintInstruction({mint: mint.address,decimals: 0,mintAuthority: client.payer.address,freezeAuthority: client.payer.address})]);const [sourceToken] = await findAssociatedTokenPda({mint: mint.address,owner: client.payer.address,tokenProgram: TOKEN_2022_PROGRAM_ADDRESS});const [destinationToken] = await findAssociatedTokenPda({mint: mint.address,owner: recipient.address,tokenProgram: TOKEN_2022_PROGRAM_ADDRESS});await client.sendTransaction([await getCreateAssociatedTokenInstructionAsync({payer: client.payer,mint: mint.address,owner: client.payer.address}),await getCreateAssociatedTokenInstructionAsync({payer: client.payer,mint: mint.address,owner: recipient.address}),getMintToCheckedInstruction({mint: mint.address,token: sourceToken,mintAuthority: client.payer,amount: 1n,decimals: 0})]);try {await client.sendTransaction([getTransferCheckedInstruction({source: sourceToken, // Token account sending the transfer.mint: mint.address, // Mint with the non-transferable configuration.destination: destinationToken, // Token account receiving the transfer.authority: client.payer, // Signer approving the transfer.amount: 1n, // Token amount in base units.decimals: 0 // Decimals defined on the mint.})]);} catch (error) {console.error("Transfer failed as expected:", error);}const mintAccount = await fetchMint(client.rpc, mint.address);const sourceTokenAccount = await fetchToken(client.rpc, sourceToken);console.log("Mint Address:", mint.address);console.log("Mint Extensions:", mintAccount.data.extensions);console.log("\nSource ATA:", sourceToken);console.log("Source Token Extensions:", sourceTokenAccount.data.extensions);console.log("Destination ATA:", destinationToken);
Web3.js
import {Connection,Keypair,LAMPORTS_PER_SOL,sendAndConfirmTransaction,SystemProgram,Transaction} from "@solana/web3.js";import {ASSOCIATED_TOKEN_PROGRAM_ID,createAssociatedTokenAccountInstruction,createInitializeMintInstruction,createInitializeNonTransferableMintInstruction,createMintToCheckedInstruction,createTransferCheckedInstruction,ExtensionType,getAccount,getAssociatedTokenAddressSync,getMint,getMintLen,getNonTransferable,TOKEN_2022_PROGRAM_ID} from "@solana/spl-token";const connection = new Connection("http://localhost:8899", "confirmed");const latestBlockhash = await connection.getLatestBlockhash();const feePayer = Keypair.generate();const recipient = Keypair.generate();const airdropSignature = await connection.requestAirdrop(feePayer.publicKey,LAMPORTS_PER_SOL);await connection.confirmTransaction({blockhash: latestBlockhash.blockhash,lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,signature: airdropSignature});const mint = Keypair.generate();const mintSpace = getMintLen([ExtensionType.NonTransferable]);const mintRent = await connection.getMinimumBalanceForRentExemption(mintSpace);const sourceToken = getAssociatedTokenAddressSync(mint.publicKey,feePayer.publicKey,false,TOKEN_2022_PROGRAM_ID,ASSOCIATED_TOKEN_PROGRAM_ID);const destinationToken = getAssociatedTokenAddressSync(mint.publicKey,recipient.publicKey,false,TOKEN_2022_PROGRAM_ID,ASSOCIATED_TOKEN_PROGRAM_ID);await sendAndConfirmTransaction(connection,new Transaction({feePayer: feePayer.publicKey,blockhash: latestBlockhash.blockhash,lastValidBlockHeight: latestBlockhash.lastValidBlockHeight}).add(SystemProgram.createAccount({fromPubkey: feePayer.publicKey,newAccountPubkey: mint.publicKey,lamports: mintRent,space: mintSpace,programId: TOKEN_2022_PROGRAM_ID}),createInitializeNonTransferableMintInstruction(mint.publicKey, // Mint account that stores the NonTransferable extension.TOKEN_2022_PROGRAM_ID // Token program that owns the mint.),createInitializeMintInstruction(mint.publicKey,0,feePayer.publicKey,feePayer.publicKey,TOKEN_2022_PROGRAM_ID)),[feePayer, mint],{ commitment: "confirmed" });await sendAndConfirmTransaction(connection,new Transaction().add(createAssociatedTokenAccountInstruction(feePayer.publicKey,sourceToken,feePayer.publicKey,mint.publicKey,TOKEN_2022_PROGRAM_ID,ASSOCIATED_TOKEN_PROGRAM_ID),createAssociatedTokenAccountInstruction(feePayer.publicKey,destinationToken,recipient.publicKey,mint.publicKey,TOKEN_2022_PROGRAM_ID,ASSOCIATED_TOKEN_PROGRAM_ID),createMintToCheckedInstruction(mint.publicKey,sourceToken,feePayer.publicKey,1,0,[],TOKEN_2022_PROGRAM_ID)),[feePayer],{ commitment: "confirmed" });try {await sendAndConfirmTransaction(connection,new Transaction().add(createTransferCheckedInstruction(sourceToken, // Token account sending the transfer.mint.publicKey, // Mint with the non-transferable configuration.destinationToken, // Token account receiving the transfer.feePayer.publicKey, // Signer approving the transfer.1, // Token amount in base units.0, // Decimals defined on the mint.[], // Additional multisig signers.TOKEN_2022_PROGRAM_ID // Token program that processes the transfer.)),[feePayer],{ commitment: "confirmed" });} catch (error) {console.error("Transfer failed as expected:", error);}const mintAccount = await getMint(connection,mint.publicKey,"confirmed",TOKEN_2022_PROGRAM_ID);const sourceTokenAccount = await getAccount(connection,sourceToken,"confirmed",TOKEN_2022_PROGRAM_ID);console.log("Mint Address:", mint.publicKey.toBase58());console.log("Has NonTransferable:", getNonTransferable(mintAccount) !== null);console.log("\nSource ATA:", sourceToken.toBase58());console.log("Source Token Account:", sourceTokenAccount);console.log("Destination ATA:", destinationToken.toBase58());
Rust
use anyhow::Result;use solana_client::nonblocking::rpc_client::RpcClient;use solana_commitment_config::CommitmentConfig;use solana_sdk::{signature::{Keypair, Signer},transaction::Transaction,};use solana_system_interface::instruction::create_account;use spl_associated_token_account_interface::{address::get_associated_token_address_with_program_id,instruction::create_associated_token_account,};use spl_token_2022_interface::{extension::{non_transferable::{NonTransferable, NonTransferableAccount},BaseStateWithExtensions, ExtensionType, StateWithExtensions,},instruction::{initialize_mint, initialize_non_transferable_mint, mint_to_checked, transfer_checked,},state::{Account, Mint},ID as TOKEN_2022_PROGRAM_ID,};#[tokio::main]async fn main() -> Result<()> {let client = RpcClient::new_with_commitment(String::from("http://localhost:8899"),CommitmentConfig::confirmed(),);let recipient = Keypair::new();let fee_payer = Keypair::new();let decimals = 0;let airdrop_signature = client.request_airdrop(&fee_payer.pubkey(), 1_000_000_000).await?;loop {let confirmed = client.confirm_transaction(&airdrop_signature).await?;if confirmed {break;}}let mint = Keypair::new();let mint_space =ExtensionType::try_calculate_account_len::<Mint>(&[ExtensionType::NonTransferable])?;let mint_rent = client.get_minimum_balance_for_rent_exemption(mint_space).await?;let mint_blockhash = client.get_latest_blockhash().await?;let mint_transaction = Transaction::new_signed_with_payer(&[create_account(&fee_payer.pubkey(),&mint.pubkey(),mint_rent,mint_space as u64,&TOKEN_2022_PROGRAM_ID,),initialize_non_transferable_mint(&TOKEN_2022_PROGRAM_ID, &mint.pubkey())?,initialize_mint(&TOKEN_2022_PROGRAM_ID,&mint.pubkey(),&fee_payer.pubkey(),Some(&fee_payer.pubkey()),decimals,)?,],Some(&fee_payer.pubkey()),&[&fee_payer, &mint],mint_blockhash,);client.send_and_confirm_transaction(&mint_transaction).await?;let source_token_address = get_associated_token_address_with_program_id(&fee_payer.pubkey(),&mint.pubkey(),&TOKEN_2022_PROGRAM_ID,);let destination_token_address = get_associated_token_address_with_program_id(&recipient.pubkey(),&mint.pubkey(),&TOKEN_2022_PROGRAM_ID,);let setup_blockhash = client.get_latest_blockhash().await?;let setup_transaction = Transaction::new_signed_with_payer(&[create_associated_token_account(&fee_payer.pubkey(),&fee_payer.pubkey(),&mint.pubkey(),&TOKEN_2022_PROGRAM_ID,),create_associated_token_account(&fee_payer.pubkey(),&recipient.pubkey(),&mint.pubkey(),&TOKEN_2022_PROGRAM_ID,),mint_to_checked(&TOKEN_2022_PROGRAM_ID,&mint.pubkey(),&source_token_address,&fee_payer.pubkey(),&[],1,decimals,)?,],Some(&fee_payer.pubkey()),&[&fee_payer],setup_blockhash,);client.send_and_confirm_transaction(&setup_transaction).await?;let transfer_blockhash = client.get_latest_blockhash().await?;let transfer_transaction = Transaction::new_signed_with_payer(&[transfer_checked(&TOKEN_2022_PROGRAM_ID, // Token program to invoke.&source_token_address, // Token account sending the tokens.&mint.pubkey(), // Mint for the token being transferred.&destination_token_address, // Token account receiving the tokens.&fee_payer.pubkey(), // Owner or delegate approving the transfer.&[], // Additional multisig signers.1, // Token amount in base units.decimals, // Decimals defined on the mint account.)?,],Some(&fee_payer.pubkey()),&[&fee_payer],transfer_blockhash,);match client.send_and_confirm_transaction(&transfer_transaction).await {Ok(signature) => println!("Transfer unexpectedly succeeded: {}", signature),Err(error) => println!("Transfer failed as expected: {error:#?}"),}let mint_account = client.get_account(&mint.pubkey()).await?;let mint_state = StateWithExtensions::<Mint>::unpack(&mint_account.data)?;let source_token_account = client.get_account(&source_token_address).await?;let source_token_state = StateWithExtensions::<Account>::unpack(&source_token_account.data)?;println!("Mint Address: {}", mint.pubkey());println!("Mint Extensions: {:?}", mint_state.get_extension_types()?);println!("Has NonTransferable: {}",mint_state.get_extension::<NonTransferable>().is_ok());println!("\nSource ATA: {}", source_token_address);println!("Source Token Extensions: {:?}",source_token_state.get_extension_types()?);println!("Has NonTransferableAccount: {}",source_token_state.get_extension::<NonTransferableAccount>().is_ok());println!("Destination ATA: {}", destination_token_address);Ok(())}
Is this page helpful?