Tokens Não Transferíveis

O Que São Tokens Não Transferíveis?

A extensão de mint NonTransferable do Token Extensions Program torna cada token account desse mint não transferível. Após os tokens serem mintados, os detentores de tokens não podem movê-los para outra token account com Transfer ou TransferChecked.

Este padrão é útil para ativos que devem permanecer vinculados a uma carteira.

Tokens não transferíveis ainda podem ser:

  • Mintados pela autoridade do mint
  • Queimados pelo proprietário da token account ou por um delegado autorizado
  • Token accounts podem ser encerradas após o saldo atingir zero

Extensões de Token Account

Quando uma token account é inicializada para um mint com NonTransferable, a token account é inicializada com NonTransferableAccount e ImmutableOwner. Quando a token account é criada através do Associated Token Program, o tamanho necessário da conta é calculado e a token account é criada com o tamanho necessário e lamports isentos de rent.

Como Criar um Mint Não Transferível

Para criar um mint não transferível:

  1. Calcule o tamanho da mint account e o rent necessário para o mint e a extensão NonTransferable.
  2. Crie a mint account com CreateAccount, inicialize NonTransferable, e inicialize o mint com InitializeMint.
  3. Crie token accounts para o mint. NonTransferableAccount e ImmutableOwner são automaticamente habilitadas para token accounts.
  4. Transfer e TransferChecked falham com TokenError::NonTransferable.

Calcular tamanho da conta

Calcule o tamanho da mint account para o mint base mais a extensão NonTransferable. Este é o tamanho usado em CreateAccount.

Example
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

Calcule o rent usando o tamanho necessário para o mint mais a extensão NonTransferable.

Example
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();

Criar a mint account

Crie a mint account com o espaço e lamports calculados.

Example
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

Inicialize a extensão NonTransferable no mint.

Example
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 o mint

Inicialize o mint com InitializeMint na mesma transação.

Example
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
})
]);

Calcular tamanho da conta

Calcule o tamanho da mint account para o mint base mais a extensão NonTransferable. Este é o tamanho usado em CreateAccount.

Calcular rent

Calcule o rent usando o tamanho necessário para o mint mais a extensão NonTransferable.

Criar a mint account

Crie a mint account com o espaço e lamports calculados.

Inicializar NonTransferable

Inicialize a extensão NonTransferable no mint.

Inicializar o mint

Inicialize o mint com InitializeMint na mesma transação.

Example
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]));

Ordem das Instruções

InitializeNonTransferableMint deve vir antes de InitializeMint. CreateAccount, InitializeNonTransferableMint e InitializeMint devem ser incluídos na mesma transação.

Referência da Fonte

ItemDescriçãoFonte
NonTransferableExtensão de mint que marca tokens do mint como não transferíveis.Fonte
NonTransferableAccountExtensão de token account adicionada às token accounts para mints não transferíveis.Fonte
ImmutableOwnerExtensão de token account que impede mudanças de propriedade e é necessária para token accounts não transferíveis.Fonte
InitializeNonTransferableMintInstrução que inicializa a extensão não transferível no nível do mint antes de InitializeMint.Fonte
process_initialize_non_transferable_mintLógica do processador que inicializa a extensão de mint NonTransferable em um mint não inicializado.Fonte
get_required_init_account_extensionsUsado para adicionar automaticamente extensões de token account quando as token accounts são inicializadas com base nas extensões habilitadas no mint. Para mints NonTransferable, adiciona NonTransferableAccount e ImmutableOwner.Fonte

Typescript

O exemplo Kit abaixo usa as instruções geradas diretamente. Exemplos legados usando @solana/web3.js estão incluídos para referência.

Kit

Instructions
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);
Console
Click to execute the code.

Web3.js

Instructions
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());
Console
Click to execute the code.

Rust

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(())
}
Console
Click to execute the code.

Is this page helpful?

Índice

Editar Página

Gerenciado por

© 2026 Fundação Solana.
Todos os direitos reservados.
Conecte-se
  • Blog