Цей контент перекладається і буде доступний тут, коли буде готовий.
Tokens on Solana
Everything on Solana is an account. Tokens follow the same model, but with extra structure. Understanding this model is essential before you write mobile token code.
A token has three key accounts:
Mint Account: Defines the token (decimals, supply, authorities)
Token Account: Holds a user's balance of that token
Metadata Account: Stores name, symbol, image (via Token Metadata program)
When a user wants to hold USDC, they need a Token Account that links their wallet to the USDC Mint Account. This is called an Associated Token Account (ATA).
Wallet → Token Account → Mint Account
(owner) (balance) (token definition)Setting Up SPL Token
Install the SPL Token library:
yarn add @solana/spl-tokenThe library provides instructions for all token operations. On mobile, you'll typically:
Build the instruction
Create a transaction
Sign with MWA
Send and confirm
import {
getAssociatedTokenAddress,
createTransferInstruction,
TOKEN_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import {
Connection,
PublicKey,
Transaction,
TransactionMessage,
VersionedTransaction,
} from "@solana/web3.js";Checking Token Balances
Before any transfer, check the user's balance:
async function getTokenBalance(
connection: Connection,
walletAddress: PublicKey,
mintAddress: PublicKey
): Promise<number> {
// Derive the Associated Token Account address
const ata = await getAssociatedTokenAddress(
mintAddress,
walletAddress
);
try {
const accountInfo = await connection.getTokenAccountBalance(ata);
return parseFloat(accountInfo.value.uiAmountString || "0");
} catch (error) {
// Account doesn't exist = zero balance
return 0;
}
}
// Usage
const USDC_MINT = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
const balance = await getTokenBalance(connection, userWallet, USDC_MINT);
console.log(`USDC Balance: ${balance}`);Fetching All Token Balances
For a portfolio view, fetch all tokens at once:
async function getAllTokenBalances(
connection: Connection,
walletAddress: PublicKey
): Promise<TokenBalance[]> {
const tokenAccounts = await connection.getParsedTokenAccountsByOwner(
walletAddress,
{ programId: TOKEN_PROGRAM_ID }
);
return tokenAccounts.value.map((account) => {
const info = account.account.data.parsed.info;
return {
mint: info.mint,
balance: info.tokenAmount.uiAmount,
decimals: info.tokenAmount.decimals,
address: account.pubkey.toBase58(),
};
});
}Building Transfer Transactions
Here's how to build a token transfer that you'll sign with MWA:
import { transact } from "@solana-mobile/mobile-wallet-adapter-protocol-web3js";
async function transferToken(
connection: Connection,
fromWallet: PublicKey,
toWallet: PublicKey,
mintAddress: PublicKey,
amount: number,
decimals: number
): Promise<string> {
// 1. Derive token accounts
const fromAta = await getAssociatedTokenAddress(mintAddress, fromWallet);
const toAta = await getAssociatedTokenAddress(mintAddress, toWallet);
// 2. Check if recipient has a token account
const toAtaInfo = await connection.getAccountInfo(toAta);
// 3. Build instructions
const instructions = [];
// Create recipient's ATA if it doesn't exist
if (!toAtaInfo) {
instructions.push(
createAssociatedTokenAccountInstruction(
fromWallet, // payer
toAta, // ATA address
toWallet, // owner
mintAddress // mint
)
);
}
// Add transfer instruction
const rawAmount = Math.floor(amount * Math.pow(10, decimals));
instructions.push(
createTransferInstruction(
fromAta, // source
toAta, // destination
fromWallet, // owner/signer
rawAmount // amount in smallest units
)
);
// 4. Build transaction
const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash("confirmed");
const messageV0 = new TransactionMessage({
payerKey: fromWallet,
recentBlockhash: blockhash,
instructions,
}).compileToV0Message();
const transaction = new VersionedTransaction(messageV0);
// 5. Sign with MWA
const signedTx = await transact(async (wallet) => {
const authResult = await wallet.authorize({
cluster: "mainnet-beta",
identity: {
name: "My Token App",
uri: "https://myapp.com",
icon: "favicon.ico",
},
});
const [signedTransaction] = await wallet.signTransactions({
transactions: [transaction],
});
return signedTransaction;
});
// 6. Send and confirm
const signature = await connection.sendTransaction(signedTx);
await connection.confirmTransaction({
signature,
blockhash,
lastValidBlockHeight,
}, "confirmed");
return signature;
}Creating Token Accounts
Sometimes you need to explicitly create a token account before receiving:
import {
createAssociatedTokenAccountInstruction,
getAssociatedTokenAddress,
} from "@solana/spl-token";
async function createTokenAccountIfNeeded(
connection: Connection,
payer: PublicKey,
owner: PublicKey,
mint: PublicKey
): Promise<PublicKey> {
const ata = await getAssociatedTokenAddress(mint, owner);
const ataInfo = await connection.getAccountInfo(ata);
if (ataInfo) {
// Already exists
return ata;
}
// Need to create it
const instruction = createAssociatedTokenAccountInstruction(
payer, // Who pays for the account creation
ata, // The ATA address
owner, // Who will own this token account
mint // The token mint
);
// Build and sign transaction...
// (similar to transfer flow)
return ata;
}Cost Considerations
Creating an ATA costs ~0.002 SOL (rent exemption). In a transfer to a new recipient:
You might pay to create their token account
This is often expected in apps, good UX
Consider showing users this cost before confirming
const RENT_EXEMPT_MINIMUM = 0.00203928; // SOL for a token account
// Show user the total cost
const transferCost = recipientNeedsAta
? RENT_EXEMPT_MINIMUM + transactionFee
: transactionFee;Token-2022 (Token Extensions)
Solana has a newer token program with additional features. Check which program a mint uses:
import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from "@solana/spl-token";
async function getTokenProgram(
connection: Connection,
mintAddress: PublicKey
): Promise<PublicKey> {
const mintInfo = await connection.getAccountInfo(mintAddress);
if (!mintInfo) {
throw new Error("Mint not found");
}
// Check which program owns the mint
if (mintInfo.owner.equals(TOKEN_2022_PROGRAM_ID)) {
return TOKEN_2022_PROGRAM_ID;
}
return TOKEN_PROGRAM_ID;
}
// Use the correct program in your instructions
const tokenProgram = await getTokenProgram(connection, mintAddress);
const transferIx = createTransferInstruction(
sourceAta,
destAta,
owner,
amount,
[],
tokenProgram // Pass the correct program
);Mobile UX Patterns
Loading States
Token operations take time. Show meaningful progress:
const [status, setStatus] = useState<
"idle" | "building" | "signing" | "sending" | "confirming" | "done" | "error"
>("idle");
async function handleTransfer() {
try {
setStatus("building");
const tx = await buildTransferTransaction(/* ... */);
setStatus("signing");
const signedTx = await signWithMwa(tx);
setStatus("sending");
const signature = await connection.sendTransaction(signedTx);
setStatus("confirming");
await connection.confirmTransaction(signature, "confirmed");
setStatus("done");
} catch (error) {
setStatus("error");
// Handle specific errors
}
}Amount Input
Always handle decimal precision carefully:
function parseTokenAmount(
input: string,
decimals: number
): bigint | null {
// Remove anything except digits and decimal
const cleaned = input.replace(/[^\d.]/g, "");
// Validate format
const parts = cleaned.split(".");
if (parts.length > 2) return null;
const wholePart = parts[0] || "0";
const decimalPart = (parts[1] || "").padEnd(decimals, "0").slice(0, decimals);
try {
return BigInt(wholePart + decimalPart);
} catch {
return null;
}
}
// Usage
const rawAmount = parseTokenAmount("10.5", 6); // USDC has 6 decimals
// Result: 10500000nMax Button
Let users easily send their full balance:
function getMaxTransferAmount(
balance: number,
decimals: number,
estimatedFee: number = 0.00005 // Conservative transaction fee
): number {
// Leave a small buffer for fees
// Note: token transfers don't cost tokens, but the user needs SOL for gas
return Math.max(0, balance);
}Error Handling
Token operations have specific failure modes:
try {
await transferToken(/* ... */);
} catch (error) {
const message = error.message || "";
if (message.includes("insufficient funds")) {
// Not enough tokens
showError("Insufficient token balance");
} else if (message.includes("insufficient lamports")) {
// Not enough SOL for fees
showError("Not enough SOL for transaction fees");
} else if (message.includes("Account not found")) {
// Token account doesn't exist
showError("Token account not found");
} else if (message.includes("owner does not match")) {
// Wrong owner for the token account
showError("Token account ownership mismatch");
} else {
showError("Transfer failed. Please try again.");
}
}Key Takeaways
Token Account model: Users need ATAs to hold tokens. Check if they exist before transfers.
Decimals matter: USDC is 6 decimals, SOL is 9. Always convert properly.
ATA creation costs SOL: Factor this into UX when sending to new recipients.
Check the token program: Token-2022 mints need the right program ID.
Show progress: Token operations have multiple steps; keep users informed.
Next, we'll build on these patterns to handle NFTs: tokens with supply of 1 and rich metadata.