양도 불가능한 토큰이란?
Token Extensions Program의 NonTransferable mint 확장 기능은 해당 mint의
모든 token account를 양도 불가능하게 만듭니다. 토큰이 발행된 후 토큰 보유자는
Transfer 또는 *rsTransferChecked*를 사용하여 다른 token account로 이동할
수 없습니다.
이 패턴은 하나의 지갑에 고정되어야 하는 자산에 유용합니다.
양도 불가능한 토큰은 여전히 다음이 가능합니다:
- mint 권한에 의한 발행
- token account 소유자 또는 승인된 위임자에 의한 소각
- 잔액이 0이 된 후 token account 폐쇄
Token Account 확장 기능
_rsNonTransferable_가 있는 mint에 대해 token account가 초기화되면, token
account는 NonTransferableAccount 및 _rsImmutableOwner_와 함께
초기화됩니다. Associated Token
Program을
통해 token account가 생성되면 필요한 계정 크기가 계산되고 token account는
필요한 크기와 rent 면제 lamport로 생성됩니다.
양도 불가능한 Mint를 생성하는 방법
양도 불가능한 mint를 생성하려면:
- mint 및
NonTransferable확장 기능에 필요한 mint account 크기와 rent를 계산합니다. - *rs
CreateAccount*로 mint account를 생성하고, *rsNonTransferable*를 초기화한 다음, *rsInitializeMint*로 mint를 초기화합니다. - mint에 대한 token account를 생성합니다. token account에 대해
NonTransferableAccount및 *rsImmutableOwner*가 자동으로 활성화됩니다. Transfer및 *rsTransferChecked*는TokenError::NonTransferable오류로 실패합니다.
계정 크기 계산
기본 mint와 NonTransferable 확장 기능을 포함한 mint account 크기를
계산합니다. 이것이 *rsCreateAccount*에서 사용되는 크기입니다.
렌트 계산
민트에 필요한 크기와 NonTransferable 확장을 사용하여 렌트를 계산합니다.
mint account 생성
계산된 공간과 lamport로 mint account를 생성합니다.
NonTransferable 초기화
민트에서 NonTransferable 확장을 초기화합니다.
민트 초기화
동일한 트랜잭션에서 *rsInitializeMint*를 사용하여 민트를 초기화합니다.
명령어 순서
_rsInitializeNonTransferableMint_는 _rsInitializeMint_보다 먼저 실행되어야
합니다. CreateAccount, InitializeNonTransferableMint, 그리고
_rsInitializeMint_는 동일한 트랜잭션에 포함되어야 합니다.
소스 참조
| 항목 | 설명 | 소스 |
|---|---|---|
NonTransferable | 민트의 토큰을 양도 불가능으로 표시하는 민트 확장입니다. | 소스 |
NonTransferableAccount | 양도 불가능 민트를 위한 token account에 추가되는 token account 확장입니다. | 소스 |
ImmutableOwner | 소유권 변경을 방지하며 양도 불가능 token account에 필수적인 token account 확장입니다. | 소스 |
InitializeNonTransferableMint | InitializeMint 이전에 민트 수준의 양도 불가능 확장을 초기화하는 명령어입니다. | 소스 |
process_initialize_non_transferable_mint | 초기화되지 않은 민트에서 NonTransferable 민트 확장을 초기화하는 프로세서 로직입니다. | 소스 |
get_required_init_account_extensions | 민트에서 활성화된 확장을 기반으로 token account가 초기화될 때 token account 확장을 자동으로 추가하는 데 사용됩니다. NonTransferable 민트의 경우 *rsNonTransferableAccount*와 *rsImmutableOwner*를 추가합니다. | 소스 |
Kit
이 예제들은 생성된 Token-2022 인스트럭션을 직접 사용합니다.
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})]);let transferFailure: string | undefined;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) {transferFailure = error instanceof Error ? error.message : String(error);}if (!transferFailure) {throw new Error("Expected the non-transferable transfer to fail");}console.log("Transfer failed as expected:", transferFailure);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 v3
이 예제들은 @solana-program 인스트럭션 빌더와 함께 클래스 기반 @solana/web3.js v3 API를 사용합니다.
import {Connection,Keypair,LAMPORTS_PER_SOL,PublicKey,sendAndConfirmTransaction,SystemProgram,Transaction} from "@solana/web3.js";import { unwrapOption } from "@solana/kit";import {findAssociatedTokenPda,getCreateAssociatedTokenIdempotentInstruction,getInitializeMint2Instruction,getInitializeNonTransferableMintInstruction,getMintDecoder,getMintSize,getMintToCheckedInstruction,getTokenDecoder,getTransferCheckedInstruction,TOKEN_2022_PROGRAM_ADDRESS} from "@solana-program/token-2022";const connection = new Connection("http://localhost:8899", "confirmed");const latestBlockhash = await connection.getLatestBlockhash();const feePayer = await Keypair.generate();const recipient = await 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 = await Keypair.generate();const mintSpace = getMintSize([{ __kind: "NonTransferable" }]);const mintRent = await connection.getMinimumBalanceForRentExemption(mintSpace);const [sourceToken] = await findAssociatedTokenPda({mint: mint.publicKey.toBase58(),owner: feePayer.publicKey.toBase58(),tokenProgram: TOKEN_2022_PROGRAM_ADDRESS});const [destinationToken] = await findAssociatedTokenPda({mint: mint.publicKey.toBase58(),owner: recipient.publicKey.toBase58(),tokenProgram: TOKEN_2022_PROGRAM_ADDRESS});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: new PublicKey(TOKEN_2022_PROGRAM_ADDRESS)}),getInitializeNonTransferableMintInstruction({mint: mint.publicKey // Mint account that stores the NonTransferable extension.}),getInitializeMint2Instruction({mint: mint.publicKey,decimals: 0,mintAuthority: feePayer.publicKey.toBase58(),freezeAuthority: feePayer.publicKey.toBase58()})),[feePayer, mint],{ commitment: "confirmed" });await sendAndConfirmTransaction(connection,new Transaction().add(getCreateAssociatedTokenIdempotentInstruction({payer: feePayer,ata: sourceToken,owner: feePayer.publicKey,mint: mint.publicKey}),getCreateAssociatedTokenIdempotentInstruction({payer: feePayer,ata: destinationToken,owner: recipient.publicKey,mint: mint.publicKey}),getMintToCheckedInstruction({mint: mint.publicKey,token: sourceToken,mintAuthority: feePayer,amount: 1,decimals: 0})),[feePayer],{ commitment: "confirmed" });let transferFailure: string | undefined;try {await sendAndConfirmTransaction(connection,new Transaction().add(getTransferCheckedInstruction({source: sourceToken, // Token account sending the transfer.mint: mint.publicKey, // Mint with the non-transferable configuration.destination: destinationToken, // Token account receiving the transfer.authority: feePayer, // Signer approving the transfer.amount: 1, // Token amount in base units.decimals: 0 // Decimals defined on the mint.})),[feePayer],{ commitment: "confirmed" });} catch (error) {transferFailure = error instanceof Error ? error.message : String(error);}if (!transferFailure) {throw new Error("Expected the non-transferable transfer to fail");}console.log("Transfer failed as expected:", transferFailure);const mintAccountInfo = await connection.getAccountInfo(mint.publicKey);const mintAccount = getMintDecoder().decode(mintAccountInfo!.data);const sourceAccountInfo = await connection.getAccountInfo(new PublicKey(sourceToken));const sourceTokenAccount = getTokenDecoder().decode(sourceAccountInfo!.data);const extensions = unwrapOption(mintAccount.extensions) ?? [];console.log("Mint Address:", mint.publicKey.toBase58());console.log("Has NonTransferable:",extensions.some((extension) => extension.__kind === "NonTransferable"));console.log("\nSource ATA:", sourceToken);console.log("Source Token Account:", sourceTokenAccount);console.log("Destination ATA:", destinationToken);
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) => anyhow::bail!("Expected the non-transferable transfer to fail, but it 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(())}
Web3.js (레거시)
이 예제들은 레거시 @solana/web3.js와 @solana/spl-token을 사용합니다.
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" });let transferFailure: string | undefined;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) {transferFailure = error instanceof Error ? error.message : String(error);}if (!transferFailure) {throw new Error("Expected the non-transferable transfer to fail");}console.log("Transfer failed as expected:", transferFailure);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());
Is this page helpful?