Создание токен-аккаунта

Что такое токен-аккаунт?

Токен-аккаунт — это тип аккаунта в Token Program Solana, который хранит информацию о владении конкретным токеном (mint) пользователем. Каждый токен-аккаунт связан с одним mint и отслеживает такие данные, как баланс токенов и владелец.

Token Account Type
/// Account data.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Account {
/// The mint associated with this account
pub mint: Pubkey,
/// The owner of this account.
pub owner: Pubkey,
/// The amount of tokens this account holds.
pub amount: u64,
/// If `delegate` is `Some` then `delegated_amount` represents
/// the amount authorized by the delegate
pub delegate: COption<Pubkey>,
/// The account's state
pub state: AccountState,
/// If `is_native.is_some`, this is a native token, and the value logs the
/// rent-exempt reserve. An Account is required to be rent-exempt, so
/// the value is used by the Processor to ensure that wrapped SOL
/// accounts do not drop below this threshold.
pub is_native: COption<u64>,
/// The amount delegated
pub delegated_amount: u64,
/// Optional authority to close the account.
pub close_authority: COption<Pubkey>,
}

Обратите внимание, что в исходном коде разработчики называют токен-аккаунт типом Account. И Token Program, и Token Extension Program используют одну и ту же базовую реализацию для токен-аккаунта.

Чтобы хранить токены для конкретного mint, пользователи должны сначала создать токен-аккаунт. Каждый токен-аккаунт отслеживает:

  1. Конкретный mint (тип токена, который хранится в токен-аккаунте)
  2. Владельца (авторитет, который может переводить токены с аккаунта)

Вот пример с использованием USD Coin (USDC) на Solana:

  • Адрес mint для USD Coin: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
  • Circle (эмитент USD Coin) имеет токен-аккаунт по адресу 3emsAVdmGKERbHjmGfQ6oZ1e35dkf5iYcS6U4CPKFVaa
  • Этот токен-аккаунт хранит только единицы токена USD Coin (mint)
  • Circle обладает полномочиями владельца по адресу 7VHUFJHWu2CuExkJcJrzhQPJ2oygupTWkL2A2For4BmE и может переводить эти токены

Вы можете просмотреть детали этого токен-аккаунта на Solana Explorer.

Термин "владелец" используется в двух разных контекстах:

  1. "Владелец" токен-аккаунта — это адрес, хранящийся в данных токен-аккаунта в поле "owner" типа Account, определенного в Token Program. Владелец может переводить, сжигать или делегировать токены с аккаунта. Этот адрес иногда называют "авторитетом" токен-аккаунта, чтобы отличить его от владельца программы.

  2. Программа "владелец" - это программа, которая владеет данными аккаунта на Solana. Для токен-аккаунтов это всегда либо Token Program, либо Token Extensions Program, как указано в поле "owner" базового типа Solana Account.

При работе с токен-аккаунтами "владелец" обычно относится к полномочию, которое может распоряжаться токенами, а не к программе, которая владеет аккаунтом.

Что такое связанный токен-аккаунт?

Связанный токен-аккаунт — это токен-аккаунт с адресом, который является Program Derived Address (PDA), созданным Associated Token Program. Вы можете рассматривать связанный токен-аккаунт как основной токен-аккаунт для пользователя, чтобы хранить единицы определённого токена (mint).

Термин "связанные токен-аккаунты" применяется только к токен-аккаунтам, которые создаёт Associated Token Program.

Связанные токен-аккаунты предоставляют детерминированный способ найти токен-аккаунт пользователя для любого заданного mint. Вы можете ознакомиться с реализацией этого процесса здесь.

Associated Token Account Address Derivation
pub fn get_associated_token_address_and_bump_seed_internal(
wallet_address: &Pubkey,
token_mint_address: &Pubkey,
program_id: &Pubkey,
token_program_id: &Pubkey,
) -> (Pubkey, u8) {
Pubkey::find_program_address(
&[
&wallet_address.to_bytes(), // Owner's public key
&token_program_id.to_bytes(), // Token Program or Token Extension Program
&token_mint_address.to_bytes(), // Token mint address
],
program_id, // Associated Token Program ID
)
}

Этот детерминированный процесс гарантирует, что для любой комбинации адреса кошелька и mint токена существует ровно один адрес связанного токен-аккаунта. Такой подход упрощает поиск токен-аккаунта пользователя для любого заданного mint токена, устраняя необходимость отдельно отслеживать адреса токен-аккаунтов.

Associated Token Program функционирует как вспомогательная программа, которая создаёт токен-аккаунты с детерминированными адресами (PDA). При создании связанного токен-аккаунта Associated Token Program выполняет CPI (Cross-Program Invocation) либо в Token Program, либо в Token Extensions Program. Token Program владеет созданным аккаунтом, который имеет ту же Account структуру типа, что и определено в Token Program. Associated Token Program не сохраняет состояние - он просто предлагает стандартизированный способ создания токен-аккаунтов по детерминированному адресу, который является PDA.

Как создать токен-аккаунт

Чтобы создать токен-аккаунт, вызовите инструкцию InitializeAccount. Реализации этой инструкции можно найти здесь.

Транзакция для создания токен-аккаунта требует выполнения двух инструкций:

  1. Вызовите System Program для создания и выделения пространства для токен-аккаунта и передачи прав собственности Token Program.
  2. Вызовите Token Program для инициализации данных токен-аккаунта.

Typescript

import {
airdropFactory,
appendTransactionMessageInstructions,
createSolanaRpc,
createSolanaRpcSubscriptions,
createTransactionMessage,
generateKeyPairSigner,
getSignatureFromTransaction,
lamports,
pipe,
sendAndConfirmTransactionFactory,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
signTransactionMessageWithSigners
} from "@solana/kit";
import { getCreateAccountInstruction } from "@solana-program/system";
import {
getInitializeAccount2Instruction,
getInitializeMintInstruction,
getMintSize,
getTokenSize,
TOKEN_2022_PROGRAM_ADDRESS
} from "@solana-program/token-2022";
// Create Connection, local validator in this example
const rpc = createSolanaRpc("http://127.0.0.1:8899");
const rpcSubscriptions = createSolanaRpcSubscriptions("ws://localhost:8900");
// Get latest blockhash to include in transaction
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
// Generate keypairs for fee payer
const feePayer = await generateKeyPairSigner();
// Fund fee payer
await airdropFactory({ rpc, rpcSubscriptions })({
recipientAddress: feePayer.address,
lamports: lamports(1_000_000_000n),
commitment: "confirmed"
});
// Generate keypair to use as address of mint
const mint = await generateKeyPairSigner();
// Get default mint account size (in bytes), no extensions enabled
const space = BigInt(getMintSize());
// Get minimum balance for rent exemption
const rent = await rpc.getMinimumBalanceForRentExemption(space).send();
// Instruction to create new account for mint (token 2022 program)
// Invokes the system program
const createAccountInstruction = getCreateAccountInstruction({
payer: feePayer,
newAccount: mint,
lamports: rent,
space,
programAddress: TOKEN_2022_PROGRAM_ADDRESS
});
// Instruction to initialize mint account data
// Invokes the token 2022 program
const initializeMintInstruction = getInitializeMintInstruction({
mint: mint.address,
decimals: 2,
mintAuthority: feePayer.address
});
const instructions = [createAccountInstruction, initializeMintInstruction];
// Create transaction message
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }), // Create transaction message
(tx) => setTransactionMessageFeePayerSigner(feePayer, tx), // Set fee payer
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), // Set transaction blockhash
(tx) => appendTransactionMessageInstructions(instructions, tx) // Append instructions
);
// Sign transaction message with required signers (fee payer and mint keypair)
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
// Send and confirm transaction
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(
signedTransaction,
{ commitment: "confirmed" }
);
// Get transaction signature
const transactionSignature = getSignatureFromTransaction(signedTransaction);
console.log("Mint Address: ", mint.address);
console.log("Transaction Signature: ", transactionSignature);
// Generate keypair to use as address of token account
const tokenAccount = await generateKeyPairSigner();
// Get token account size (in bytes)
const tokenAccountSpace = BigInt(getTokenSize());
// Get minimum balance for rent exemption
const tokenAccountRent = await rpc
.getMinimumBalanceForRentExemption(tokenAccountSpace)
.send();
// Instruction to create new account for token account (token 2022 program)
// Invokes the system program
const createTokenAccountInstruction = getCreateAccountInstruction({
payer: feePayer,
newAccount: tokenAccount,
lamports: tokenAccountRent,
space: tokenAccountSpace,
programAddress: TOKEN_2022_PROGRAM_ADDRESS
});
// Instruction to initialize token account data
// Invokes the token 2022 program
const initializeTokenAccountInstruction = getInitializeAccount2Instruction({
account: tokenAccount.address,
mint: mint.address,
owner: feePayer.address
});
const instructions2 = [
createTokenAccountInstruction,
initializeTokenAccountInstruction
];
// Create transaction message for token account creation
const tokenAccountMessage = pipe(
createTransactionMessage({ version: 0 }), // Create transaction message
(tx) => setTransactionMessageFeePayerSigner(feePayer, tx), // Set fee payer
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), // Set transaction blockhash
(tx) => appendTransactionMessageInstructions(instructions2, tx) // Append instructions
);
// Sign transaction message with required signers (fee payer and token account keypair)
const signedTokenAccountTx =
await signTransactionMessageWithSigners(tokenAccountMessage);
// Send and confirm transaction
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(
signedTokenAccountTx,
{ commitment: "confirmed" }
);
// Get transaction signature
const tokenAccountTxSignature =
getSignatureFromTransaction(signedTokenAccountTx);
console.log("Token Account Address:", tokenAccount.address);
console.log("Transaction Signature:", tokenAccountTxSignature);
Console
Click to execute the code.

Rust

use anyhow::Result;
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
commitment_config::CommitmentConfig,
program_pack::Pack,
signature::{Keypair, Signer},
system_instruction::create_account,
transaction::Transaction,
};
use spl_token_2022::{
id as token_2022_program_id,
instruction::{initialize_account, initialize_mint},
state::{Account, Mint},
};
fn main() -> Result<()> {
// Create connection to local validator
let client = RpcClient::new_with_commitment(
String::from("http://127.0.0.1:8899"),
CommitmentConfig::confirmed(),
);
let recent_blockhash = client.get_latest_blockhash()?;
// Generate a new keypair for the fee payer
let fee_payer = Keypair::new();
// Airdrop 1 SOL to fee payer
let airdrop_signature = client.request_airdrop(&fee_payer.pubkey(), 1_000_000_000)?;
client.confirm_transaction(&airdrop_signature)?;
loop {
let confirmed = client.confirm_transaction(&airdrop_signature)?;
if confirmed {
break;
}
}
// Generate keypair to use as address of mint
let mint = Keypair::new();
// Get default mint account size (in bytes), no extensions enabled
let mint_space = Mint::LEN;
let mint_rent = client.get_minimum_balance_for_rent_exemption(mint_space)?;
// Instruction to create new account for mint (token 2022 program)
let create_account_instruction = create_account(
&fee_payer.pubkey(), // payer
&mint.pubkey(), // new account (mint)
mint_rent, // lamports
mint_space as u64, // space
&token_2022_program_id(), // program id
);
// Instruction to initialize mint account data
let initialize_mint_instruction = initialize_mint(
&token_2022_program_id(),
&mint.pubkey(), // mint
&fee_payer.pubkey(), // mint authority
Some(&fee_payer.pubkey()), // freeze authority
2, // decimals
)?;
// Create transaction and add instructions
let transaction = Transaction::new_signed_with_payer(
&[create_account_instruction, initialize_mint_instruction],
Some(&fee_payer.pubkey()),
&[&fee_payer, &mint],
recent_blockhash,
);
// Send and confirm transaction
let transaction_signature = client.send_and_confirm_transaction(&transaction)?;
println!("Mint Address: {}", mint.pubkey());
println!("Transaction Signature: {}", transaction_signature);
// Generate keypair to use as address of token account
let token_account = Keypair::new();
// Get token account size (in bytes)
let token_account_space = Account::LEN;
let token_account_rent = client.get_minimum_balance_for_rent_exemption(token_account_space)?;
// Instruction to create new account for token account (token 2022 program)
let create_token_account_instruction = create_account(
&fee_payer.pubkey(), // payer
&token_account.pubkey(), // new account (token account)
token_account_rent, // lamports
token_account_space as u64, // space
&token_2022_program_id(), // program id
);
// Instruction to initialize token account data
let initialize_token_account_instruction = initialize_account(
&token_2022_program_id(),
&token_account.pubkey(), // account
&mint.pubkey(), // mint
&fee_payer.pubkey(), // owner
)?;
// Create transaction and add instructions
let transaction = Transaction::new_signed_with_payer(
&[
create_token_account_instruction,
initialize_token_account_instruction,
],
Some(&fee_payer.pubkey()),
&[&fee_payer, &token_account],
recent_blockhash,
);
// Send and confirm transaction
let transaction_signature = client.send_and_confirm_transaction(&transaction)?;
println!("Token Account Address: {}", token_account.pubkey());
println!("Transaction Signature: {}", transaction_signature);
Ok(())
}
Console
Click to execute the code.

Как создать связанный токен-аккаунт

Чтобы создать связанный токен-аккаунт, вызовите инструкцию Create. Реализации этой инструкции можно найти здесь.

Инструкция для создания связанного токен-аккаунта автоматически вызывает System Program для создания токен-аккаунта и вызывает Token Program для инициализации данных токен-аккаунта. Это происходит через Cross Program Invocations (CPI).

Typescript

import {
airdropFactory,
appendTransactionMessageInstructions,
createSolanaRpc,
createSolanaRpcSubscriptions,
createTransactionMessage,
generateKeyPairSigner,
getSignatureFromTransaction,
lamports,
pipe,
sendAndConfirmTransactionFactory,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
signTransactionMessageWithSigners
} from "@solana/kit";
import { getCreateAccountInstruction } from "@solana-program/system";
import {
getCreateAssociatedTokenInstructionAsync,
getInitializeMintInstruction,
getMintSize,
TOKEN_2022_PROGRAM_ADDRESS,
findAssociatedTokenPda
} from "@solana-program/token-2022";
// Create Connection, local validator in this example
const rpc = createSolanaRpc("http://127.0.0.1:8899");
const rpcSubscriptions = createSolanaRpcSubscriptions("ws://localhost:8900");
// Generate keypairs for fee payer
const feePayer = await generateKeyPairSigner();
// Fund fee payer
await airdropFactory({ rpc, rpcSubscriptions })({
recipientAddress: feePayer.address,
lamports: lamports(1_000_000_000n),
commitment: "confirmed"
});
// Generate keypair to use as address of mint
const mint = await generateKeyPairSigner();
// Get default mint account size (in bytes), no extensions enabled
const space = BigInt(getMintSize());
// Get minimum balance for rent exemption
const rent = await rpc.getMinimumBalanceForRentExemption(space).send();
// Get latest blockhash to include in transaction
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
// Instruction to create new account for mint (token 2022 program)
// Invokes the system program
const createAccountInstruction = getCreateAccountInstruction({
payer: feePayer,
newAccount: mint,
lamports: rent,
space,
programAddress: TOKEN_2022_PROGRAM_ADDRESS
});
// Instruction to initialize mint account data
// Invokes the token 2022 program
const initializeMintInstruction = getInitializeMintInstruction({
mint: mint.address,
decimals: 2,
mintAuthority: feePayer.address
});
const instructions = [createAccountInstruction, initializeMintInstruction];
// Create transaction message
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(feePayer, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstructions(instructions, tx)
);
// Sign transaction message with all required signers
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
// Send and confirm transaction
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(
signedTransaction,
{ commitment: "confirmed" }
);
// Get transaction signature
const transactionSignature = getSignatureFromTransaction(signedTransaction);
console.log("Mint Address: ", mint.address.toString());
console.log("Transaction Signature: ", transactionSignature);
// Use findAssociatedTokenPda to derive the ATA address
const [associatedTokenAddress] = await findAssociatedTokenPda({
mint: mint.address,
owner: feePayer.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
console.log(
"Associated Token Account Address: ",
associatedTokenAddress.toString()
);
// Get a fresh blockhash for the second transaction
const { value: latestBlockhash2 } = await rpc.getLatestBlockhash().send();
// Create instruction to create the associated token account
const createAtaInstruction = await getCreateAssociatedTokenInstructionAsync({
payer: feePayer,
mint: mint.address,
owner: feePayer.address
});
// Create transaction message
const transactionMessage2 = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(feePayer, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash2, tx),
(tx) => appendTransactionMessageInstructions([createAtaInstruction], tx)
);
// Sign transaction message with all required signers
const signedTransaction2 =
await signTransactionMessageWithSigners(transactionMessage2);
// Send and confirm transaction
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(
signedTransaction2,
{ commitment: "confirmed" }
);
// Get transaction signature
const transactionSignature2 = getSignatureFromTransaction(signedTransaction2);
console.log("Transaction Signature: ", transactionSignature2);
Console
Click to execute the code.

Rust

use anyhow::Result;
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
commitment_config::CommitmentConfig,
program_pack::Pack,
signature::{Keypair, Signer},
system_instruction::create_account,
transaction::Transaction,
};
use spl_associated_token_account::{
get_associated_token_address_with_program_id, instruction::create_associated_token_account,
};
use spl_token_2022::{id as token_2022_program_id, instruction::initialize_mint, state::Mint};
fn main() -> Result<()> {
// Create connection to local validator
let client = RpcClient::new_with_commitment(
String::from("http://127.0.0.1:8899"),
CommitmentConfig::confirmed(),
);
let recent_blockhash = client.get_latest_blockhash()?;
// Generate a new keypair for the fee payer
let fee_payer = Keypair::new();
// Airdrop 1 SOL to fee payer
let airdrop_signature = client.request_airdrop(&fee_payer.pubkey(), 1_000_000_000)?;
client.confirm_transaction(&airdrop_signature)?;
loop {
let confirmed = client.confirm_transaction(&airdrop_signature)?;
if confirmed {
break;
}
}
// Generate keypair to use as address of mint
let mint = Keypair::new();
// Get default mint account size (in bytes), no extensions enabled
let mint_space = Mint::LEN;
let mint_rent = client.get_minimum_balance_for_rent_exemption(mint_space)?;
// Instruction to create new account for mint (token 2022 program)
let create_account_instruction = create_account(
&fee_payer.pubkey(), // payer
&mint.pubkey(), // new account (mint)
mint_rent, // lamports
mint_space as u64, // space
&token_2022_program_id(), // program id
);
// Instruction to initialize mint account data
let initialize_mint_instruction = initialize_mint(
&token_2022_program_id(),
&mint.pubkey(), // mint
&fee_payer.pubkey(), // mint authority
Some(&fee_payer.pubkey()), // freeze authority
2, // decimals
)?;
// Create transaction and add instructions
let transaction = Transaction::new_signed_with_payer(
&[create_account_instruction, initialize_mint_instruction],
Some(&fee_payer.pubkey()),
&[&fee_payer, &mint],
recent_blockhash,
);
// Send and confirm transaction
let transaction_signature = client.send_and_confirm_transaction(&transaction)?;
println!("Mint Address: {}", mint.pubkey());
println!("Transaction Signature: {}", transaction_signature);
// Get the latest blockhash for the next transaction
let recent_blockhash = client.get_latest_blockhash()?;
// Derive the associated token account address for fee_payer
let associated_token_account = get_associated_token_address_with_program_id(
&fee_payer.pubkey(), // owner
&mint.pubkey(), // mint
&token_2022_program_id(), // program_id
);
// Instruction to create associated token account
let create_ata_instruction = create_associated_token_account(
&fee_payer.pubkey(), // funding address
&fee_payer.pubkey(), // wallet address
&mint.pubkey(), // mint address
&token_2022_program_id(), // program id
);
// Create transaction and add instruction
let transaction = Transaction::new_signed_with_payer(
&[create_ata_instruction],
Some(&fee_payer.pubkey()),
&[&fee_payer],
recent_blockhash,
);
// Send and confirm transaction
let transaction_signature = client.send_and_confirm_transaction(&transaction)?;
println!(
"Associated Token Account Address: {}",
associated_token_account
);
println!("Transaction Signature: {}", transaction_signature);
Ok(())
}
Console
Click to execute the code.

Is this page helpful?

Содержание

Редактировать страницу