Mobile
Solana Mobile: RPC, Tokens, NFTs e Interação com Programas

Solana Mobile: RPC, Tokens, NFTs e Interação com Programas

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).

text
Wallet → Token Account → Mint Account
(owner)   (balance)      (definição do token)

Configurando o SPL Token

Instale a biblioteca SPL Token:

shellscript
yarn add @solana/spl-token

A biblioteca fornece instruções para todas as operações de token. No mobile, você tipicamente:

  1. Constrói a instrução

  2. Cria uma transação

  3. Assina com MWA

  4. Envia e confirma

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

  1. Você pode ter que pagar para criar a conta de token deles

  2. Isso é frequentemente esperado nos apps, boa UX

  3. Considere mostrar aos usuários este custo antes de confirmar

typescript
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:

typescript
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:

typescript
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:

typescript
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: 10500000n

Botão Max

Permita que usuários enviem facilmente seu saldo completo:

typescript
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:

typescript
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.

Blueshift © 2026Commit: 1b88646