양도 불가능한 토큰

양도 불가능한 토큰이란?

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를 생성하려면:

  1. mint 및 NonTransferable 확장 기능에 필요한 mint account 크기와 rent를 계산합니다.
  2. *rsCreateAccount*로 mint account를 생성하고, *rsNonTransferable*를 초기화한 다음, *rsInitializeMint*로 mint를 초기화합니다.
  3. mint에 대한 token account를 생성합니다. token account에 대해 NonTransferableAccount 및 *rsImmutableOwner*가 자동으로 활성화됩니다.
  4. Transfer 및 *rsTransferChecked*는 TokenError::NonTransferable 오류로 실패합니다.

계정 크기 계산

기본 mint와 NonTransferable 확장 기능을 포함한 mint account 크기를 계산합니다. 이것이 *rsCreateAccount*에서 사용되는 크기입니다.

렌트 계산

민트에 필요한 크기와 NonTransferable 확장을 사용하여 렌트를 계산합니다.

mint account 생성

계산된 공간과 lamport로 mint account를 생성합니다.

NonTransferable 초기화

민트에서 NonTransferable 확장을 초기화합니다.

민트 초기화

동일한 트랜잭션에서 *rsInitializeMint*를 사용하여 민트를 초기화합니다.

계정 크기 계산

기본 mint와 NonTransferable 확장 기능을 포함한 mint account 크기를 계산합니다. 이것이 *rsCreateAccount*에서 사용되는 크기입니다.

렌트 계산

민트에 필요한 크기와 NonTransferable 확장을 사용하여 렌트를 계산합니다.

mint account 생성

계산된 공간과 lamport로 mint account를 생성합니다.

NonTransferable 초기화

민트에서 NonTransferable 확장을 초기화합니다.

민트 초기화

동일한 트랜잭션에서 *rsInitializeMint*를 사용하여 민트를 초기화합니다.

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

명령어 순서

_rsInitializeNonTransferableMint_는 _rsInitializeMint_보다 먼저 실행되어야 합니다. CreateAccount, InitializeNonTransferableMint, 그리고 _rsInitializeMint_는 동일한 트랜잭션에 포함되어야 합니다.

소스 참조

항목설명소스
NonTransferable민트의 토큰을 양도 불가능으로 표시하는 민트 확장입니다.소스
NonTransferableAccount양도 불가능 민트를 위한 token account에 추가되는 token account 확장입니다.소스
ImmutableOwner소유권 변경을 방지하며 양도 불가능 token account에 필수적인 token account 확장입니다.소스
InitializeNonTransferableMintInitializeMint 이전에 민트 수준의 양도 불가능 확장을 초기화하는 명령어입니다.소스
process_initialize_non_transferable_mint초기화되지 않은 민트에서 NonTransferable 민트 확장을 초기화하는 프로세서 로직입니다.소스
get_required_init_account_extensions민트에서 활성화된 확장을 기반으로 token account가 초기화될 때 token account 확장을 자동으로 추가하는 데 사용됩니다. NonTransferable 민트의 경우 *rsNonTransferableAccount*와 *rsImmutableOwner*를 추가합니다.소스

Typescript

아래의 Kit 예제는 생성된 명령어를 직접 사용합니다. @solana/web3.js를 사용하는 레거시 예제는 참고용으로 포함되어 있습니다.

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?

목차

페이지 편집

관리자

© 2026 솔라나 재단.
모든 권리 보유.
연결하기