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}`);
Expand
[6 more lines]

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(),
    };
  });
}
Expand
[3 more lines]

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;
}
Expand
[71 more lines]

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;
}
Expand
[18 more lines]

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
);
Expand
[13 more lines]

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
  }
}
Expand
[9 more lines]

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
Expand
[6 more lines]

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.");
  }
}
Expand
[6 more lines]

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: 3c44267