Tokens na Solana
Tudo na Solana é uma conta. Tokens seguem o mesmo modelo, mas com estrutura extra. Entender este modelo é essencial antes de escrever código de tokens mobile.
Um token tem três contas principais:
Mint Account: Define o token (decimais, fornecimento, autoridades)
Token Account: Mantém o saldo de um usuário daquele token
Metadata Account: Armazena nome, símbolo, imagem (via programa Token Metadata)
Quando um usuário quer manter USDC, ele precisa de uma Token Account que vincule sua carteira à Mint Account de USDC. Isso é chamado de Associated Token Account (ATA).
Wallet → Token Account → Mint Account
(owner) (balance) (definição do token)Configurando o SPL Token
Instale a biblioteca SPL Token:
yarn add @solana/spl-tokenA biblioteca fornece instruções para todas as operações de token. No mobile, você tipicamente:
Constrói a instrução
Cria uma transação
Assina com MWA
Envia e confirma
import {
getAssociatedTokenAddress,
createTransferInstruction,
TOKEN_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import {
Connection,
PublicKey,
Transaction,
TransactionMessage,
VersionedTransaction,
} from "@solana/web3.js";Verificando Saldos de Tokens
Antes de qualquer transferência, verifique o saldo do usuário:
async function getTokenBalance(
connection: Connection,
walletAddress: PublicKey,
mintAddress: PublicKey
): Promise<number> {
// Deriva o endereço da Associated Token Account
const ata = await getAssociatedTokenAddress(
mintAddress,
walletAddress
);
try {
const accountInfo = await connection.getTokenAccountBalance(ata);
return parseFloat(accountInfo.value.uiAmountString || "0");
} catch (error) {
// Conta não existe = saldo zero
return 0;
}
}
// Uso
const USDC_MINT = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
const balance = await getTokenBalance(connection, userWallet, USDC_MINT);
console.log(`Saldo USDC: ${balance}`);Buscando Todos os Saldos de Tokens
Para uma visão de portfólio, busque todos os tokens de uma vez:
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(),
};
});
}Construindo Transações de Transferência
Veja como construir uma transferência de token que você assinará com 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. Deriva contas de token
const fromAta = await getAssociatedTokenAddress(mintAddress, fromWallet);
const toAta = await getAssociatedTokenAddress(mintAddress, toWallet);
// 2. Verifica se o destinatário tem uma conta de token
const toAtaInfo = await connection.getAccountInfo(toAta);
// 3. Constrói instruções
const instructions = [];
// Cria a ATA do destinatário se não existir
if (!toAtaInfo) {
instructions.push(
createAssociatedTokenAccountInstruction(
fromWallet, // pagador
toAta, // endereço da ATA
toWallet, // owner
mintAddress // mint
)
);
}
// Adiciona instrução de transferência
const rawAmount = Math.floor(amount * Math.pow(10, decimals));
instructions.push(
createTransferInstruction(
fromAta, // source
toAta, // destination
fromWallet, // owner/signer
rawAmount // quantidade em menor unidade
)
);
// 4. Constrói a transação
const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash("confirmed");
const messageV0 = new TransactionMessage({
payerKey: fromWallet,
recentBlockhash: blockhash,
instructions,
}).compileToV0Message();
const transaction = new VersionedTransaction(messageV0);
// 5. Assina com 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. Envia e confirma
const signature = await connection.sendTransaction(signedTx);
await connection.confirmTransaction({
signature,
blockhash,
lastValidBlockHeight,
}, "confirmed");
return signature;
}Criando Contas de Token
Às vezes você precisa criar explicitamente uma conta de token antes de receber:
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) {
// Já existe
return ata;
}
// Precisa criar
const instruction = createAssociatedTokenAccountInstruction(
payer, // Quem paga pela criação da conta
ata, // O endereço da ATA
owner, // Quem será dono desta conta de token
mint // A mint do token
);
// Constrói e assina a transação...
// (similar ao fluxo de transferência)
return ata;
}Considerações de Custo
Criar uma ATA custa ~0.002 SOL (isenção de rent). Em uma transferência para um novo destinatário:
Você pode ter que pagar para criar a conta de token deles
Isso é frequentemente esperado nos apps, boa UX
Considere mostrar aos usuários este custo antes de confirmar
const RENT_EXEMPT_MINIMUM = 0.00203928; // SOL para uma conta de token
// Mostra ao usuário o custo total
const transferCost = recipientNeedsAta
? RENT_EXEMPT_MINIMUM + transactionFee
: transactionFee;Token-2022 (Token Extensions)
A Solana possui um programa de token mais recente com funcionalidades adicionais. Verifique qual programa uma mint utiliza:
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 não encontrada");
}
// Verifica qual programa é dono da mint
if (mintInfo.owner.equals(TOKEN_2022_PROGRAM_ID)) {
return TOKEN_2022_PROGRAM_ID;
}
return TOKEN_PROGRAM_ID;
}
// Usa o programa correto nas suas instruções
const tokenProgram = await getTokenProgram(connection, mintAddress);
const transferIx = createTransferInstruction(
sourceAta,
destAta,
owner,
amount,
[],
tokenProgram // Passa o programa correto
);Padrões de UX Mobile
Estados de Carregamento
Operações com tokens levam tempo. Mostre progresso significativo:
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");
// Trata erros específicos
}
}Entrada de Valor
Sempre trate precisão decimal com cuidado:
function parseTokenAmount(
input: string,
decimals: number
): bigint | null {
// Remove qualquer coisa exceto dígitos e ponto decimal
const cleaned = input.replace(/[^\d.]/g, "");
// Valida formato
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;
}
}
// Uso
const rawAmount = parseTokenAmount("10.5", 6); // USDC tem 6 decimais
// Resultado: 10500000nBotão Max
Permita que usuários enviem facilmente seu saldo completo:
function getMaxTransferAmount(
balance: number,
decimals: number,
estimatedFee: number = 0.00005 // Taxa de transação conservadora
): number {
// Deixa um pequeno buffer para taxas
// Nota: transferências de tokens não custam tokens, mas o usuário precisa de SOL para gas
return Math.max(0, balance);
}Tratamento de Erros
Operações com tokens têm modos de falha específicos:
try {
await transferToken(/* ... */);
} catch (error) {
const message = error.message || "";
if (message.includes("insufficient funds")) {
// Tokens insuficientes
showError("Saldo de token insuficiente");
} else if (message.includes("insufficient lamports")) {
// SOL insuficiente para taxas
showError("SOL insuficiente para taxas de transação");
} else if (message.includes("Account not found")) {
// Conta de token não existe
showError("Conta de token não encontrada");
} else if (message.includes("owner does not match")) {
// Owner incorreto para a conta de token
showError("Incompatibilidade de propriedade da conta de token");
} else {
showError("Transferência falhou. Por favor, tente novamente.");
}
}Pontos-Chave
Modelo de Token Account: Usuários precisam de ATAs para manter tokens. Verifique se existem antes de transferências.
Decimais importam: USDC tem 6 decimais, SOL tem 9. Sempre converta corretamente.
Criação de ATA custa SOL: Incorpore isso na UX ao enviar para novos destinatários.
Verifique o programa de token: Mints Token-2022 precisam do ID de programa correto.
Mostre progresso: Operações com tokens têm múltiplos passos; mantenha os usuários informados.
A seguir, construiremos sobre esses padrões para lidar com NFTs: tokens com fornecimento de 1 e metadata rica.