Token Không Thể Chuyển Nhượng Là Gì?
Tiện ích mở rộng mint NonTransferable của Token Extensions Program làm cho
mọi token account cho mint đó trở thành không thể chuyển nhượng. Sau khi token
được mint, chủ sở hữu token không thể chuyển chúng sang token account khác bằng
Transfer hoặc TransferChecked.
Mô hình này hữu ích cho các tài sản cần gắn liền với một ví.
Token không thể chuyển nhượng vẫn có thể:
- Được mint bởi mint authority
- Được burn bởi chủ sở hữu token account hoặc người được ủy quyền
- Token account có thể được đóng sau khi số dư về không
Tiện Ích Mở Rộng Token Account
Khi một token account được khởi tạo cho mint có NonTransferable, token
account được khởi tạo với NonTransferableAccount và
ImmutableOwner. Khi token account được tạo thông qua Associated Token
Program,
kích thước tài khoản cần thiết được tính toán và token account được tạo với
kích thước và lamport miễn phí rent cần thiết.
Cách Tạo Mint Không Thể Chuyển Nhượng
Để tạo mint không thể chuyển nhượng:
- Tính toán kích thước mint account và rent cần thiết cho mint và tiện ích mở
rộng
NonTransferable. - Tạo mint account với
CreateAccount, khởi tạoNonTransferable, và khởi tạo mint vớiInitializeMint. - Tạo token account cho mint.
NonTransferableAccountvàImmutableOwnerđược tự động kích hoạt cho token account. TransfervàTransferCheckedsẽ thất bại vớiTokenError::NonTransferable.
Tính toán kích thước tài khoản
Tính toán kích thước mint account cho base mint cộng với tiện ích mở rộng
NonTransferable. Đây là kích thước được sử dụng trong CreateAccount.
Tính toán rent
Tính toán rent bằng cách sử dụng kích thước cần thiết cho mint cộng với phần mở
rộng NonTransferable.
Tạo mint account
Tạo mint account với dung lượng và lamport đã tính toán.
Khởi tạo NonTransferable
Khởi tạo phần mở rộng NonTransferable trên mint.
Khởi tạo mint
Khởi tạo mint với InitializeMint trong cùng một giao dịch.
Thứ tự chỉ thị
InitializeNonTransferableMint phải đứng trước InitializeMint.
CreateAccount, InitializeNonTransferableMint và
InitializeMint phải được bao gồm trong cùng một giao dịch.
Tài liệu tham khảo nguồn
| Mục | Mô tả | Nguồn |
|---|---|---|
NonTransferable | Phần mở rộng mint đánh dấu token từ mint là không thể chuyển nhượng. | Nguồn |
NonTransferableAccount | Phần mở rộng token account được thêm vào token account cho các mint không thể chuyển nhượng. | Nguồn |
ImmutableOwner | Phần mở rộng token account ngăn chặn thay đổi quyền sở hữu và được yêu cầu cho các token account không thể chuyển nhượng. | Nguồn |
InitializeNonTransferableMint | Chỉ thị khởi tạo phần mở rộng không thể chuyển nhượng ở cấp mint trước InitializeMint. | Nguồn |
process_initialize_non_transferable_mint | Logic xử lý khởi tạo phần mở rộng mint NonTransferable trên một mint chưa được khởi tạo. | Nguồn |
get_required_init_account_extensions | Được sử dụng để tự động thêm các phần mở rộng token account khi token account được khởi tạo dựa trên các phần mở rộng được kích hoạt trên mint. Đối với mint NonTransferable, nó thêm NonTransferableAccount và ImmutableOwner. | Nguồn |
Typescript
Ví dụ Kit bên dưới sử dụng trực tiếp các chỉ thị được tạo. Các ví dụ cũ sử
dụng @solana/web3.js được bao gồm để tham khảo.
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?