Depurando Transações que Falharam
Transações Solana que falham deixam pistas em logs de simulação, códigos de erro RPC e output de programa em explorers. Um fluxo disciplinado de decode separa erros de usuário, bugs de client, problemas de RPC e defeitos de programa on-chain antes de você fazer retry ou acionar on-call.
Receita
Card de receita de referência rápida - pronto para copiar e colar.
import { createSolanaRpc } from "@solana/kit";
const rpc = createSolanaRpc(process.env.RPC_URL!);
const sim = await rpc
.simulateTransaction(base64Tx, {
sigVerify: false,
replaceRecentBlockhash: true,
commitment: "confirmed",
})
.send();
if (sim.value.err) {
console.error("err:", sim.value.err);
console.error("logs:", sim.value.logs?.join("\n"));
console.error("units:", sim.value.unitsConsumed);
}Quando usar:
- Um usuário reporta "transação falhou" com apenas uma signature
- Preflight rejeita antes da tx chegar à rede
- Programa retorna
Custom: Ne você precisa do mapeamento do enum - Erros de compute ou tamanho de conta disparam após deploy
- Triagem de incidente precisa de classe de erro antes de acionar donos do programa
Exemplo Funcional
import {
createSolanaRpc,
createTransactionMessage,
setTransactionMessageFeePayer,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstruction,
signTransactionMessageWithSigners,
getBase64EncodedWireTransaction,
pipe,
} from "@solana/kit";
import { getTransferSolInstruction } from "@solana-program/system";
const rpc = createSolanaRpc("https://api.devnet.solana.com");
async function debugTransfer(signer: Awaited<ReturnType<typeof import("@solana/kit").createKeyPairSignerFromBytes>>) {
const { value: blockhash } = await rpc.getLatestBlockhash().send();
const message = pipe(
createTransactionMessage({ version: 0 }),
(m) => setTransactionMessageFeePayer(signer.address, m),
(m) => setTransactionMessageLifetimeUsingBlockhash(blockhash, m),
(m) =>
appendTransactionMessageInstruction(
getTransferSolInstruction({
source: signer,
destination: signer.address,
amount: 1n,
}),
m,
),
);
const signed = await signTransactionMessageWithSigners(message);
const wire = getBase64EncodedWireTransaction(signed);
const sim = await rpc
.simulateTransaction(wire, {
sigVerify: true,
replaceRecentBlockhash: false,
commitment: "confirmed",
})
.send();
return {
err: sim.value.err,
logs: sim.value.logs ?? [],
unitsConsumed: sim.value.unitsConsumed,
accounts: sim.value.accounts,
};
}O que isso demonstra:
- Construir transação real e simular com
replaceRecentBlockhashdesligado ao testar validade de assinatura - Ler
err,logseunitsConsumedcomo a tríade principal de debug - Usar tipos RPC @solana/kit 7.0.0 para respostas de simulação tipadas
- Capturar state de conta pós-simulação ao depurar rent ou mismatch de owner
Aprofundamento
Como Funciona
- Simulação preflight executa a transação contra o state atual do ledger antes do broadcast (quando habilitada em
sendTransaction). - Logs de programa emitem via
sol_log/msg!/anchor_lang::prelude::*e aparecem em simulação e metadata de transação confirmada. - Erros customizados de programa codificam como
InstructionError::Custom(u32)- mapeie para seu IDL ou enum#[error_code]. - Compute budget padrão é 200.000 CUs por instruction a menos que você prependa instructions
ComputeBudget. - Mismatch de meta de conta falha antes da execução: flag signer errada, flag writable errada ou conta faltando.
Classes de Erro em Resumo
| Sintoma | Causa típica | Primeiro check |
|---|---|---|
BlockhashNotFound | Blockhash expirado ou cluster errado | getLatestBlockhash, URL do cluster |
AccountNotFound | ATA faltando, conta fechada, PDA errado | Aba de conta no explorer, derivação de seed |
InsufficientFundsForFee | Fee payer subfinanciado | Saldo em lamports vs fee + rent |
ComputationalBudgetExceeded | Hot path ou loop ilimitado | Profile de CU, ix ComputeBudget |
Custom: N | require! / ErrorCode do programa | Tabela de erros IDL, CLI anchor error |
Notas TypeScript
function parseCustomError(logs: string[]): number | null {
const line = logs.find((l) => l.includes("custom program error:"));
if (!line) return null;
const m = line.match(/custom program error: (0x[0-9a-f]+|\d+)/i);
if (!m) return null;
return m[1].startsWith("0x") ? parseInt(m[1], 16) : Number(m[1]);
}Notas Rust / Anchor
// Anchor 0.32.1 - erros aparecem como códigos Custom nos logs
#[error_code]
pub enum MyError {
#[msg("Vault is paused")]
VaultPaused,
}
// Map: anchor error --program-id <PID> mostra código -> nomeErros Comuns
- Retry sem ler logs - Usuários martelam resubmit; você queima priority fees na mesma falha determinística. Correção: Simule uma vez, classifique erro, corrija causa raiz.
- RPC de cluster errado - Tx devnet simulada contra RPC mainnet retorna
AccountNotFound. Correção: FixeRPC_URL, rede da wallet e explorer?cluster=juntos. - Assumir que preflight está sempre ligado - Alguns caminhos de servidor desligam preflight por latência. Correção:
simulateTransactionexplícito em tooling de suporte. - Decimais vs lamports - Client envia
1.5SOL como float; programa espera lamports inteiros. Correção:bigintponta a ponta; converta apenas na borda da UI. - Mapa de erros IDL obsoleto - Programa v2 implantado, client ainda decodifica códigos v1. Correção: Versione IDL em CI; regenere clients a cada release de programa.
- Ignorar
unitsConsumed- Programas perto do limite falham só sob carga. Correção: Rastreie CU em métricas; alerte quando p95 exceder 80% do budget.
Alternativas
| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
| Triagem só no explorer | Suporte rápido one-off | Você precisa de automação reproduzível |
CLI solana confirm -v | Bastion SSH com CLI 3.0.10 | Servidores de app em produção sem keys |
| Replay local Surfpool / LiteSVM | Reproduzir sets de conta mainnet | Você precisa de comportamento real de congestionamento |
| Logs Geyser / indexer | Análise de falhas em alto volume | Reporte de tx única de usuário |
FAQs
Devo desabilitar preflight em produção?
Não para envios voltados ao usuário. Mantenha preflight ligado a menos que tenha motivo de latência medido e simulação server-side como rede de segurança.
Onde aparecem logs `msg!` do Anchor?
Em logs de simulação e na metadata de transação confirmada via RPC getTransaction.
Como decodificar `Custom: 6003`?
Execute anchor error 6003 contra seu IDL de programa, ou leia a ordem do enum #[error_code] (códigos começam em 6000 no Anchor).
E se simulação passar mas send falhar?
Provavelmente expiração de blockhash, queda de rede ou skip de leader. Atualize blockhash, verifique status da signature e inspecione se a tx pousou em slot posterior.
`sigVerify: false` esconde bugs de assinatura?
Sim. Use sigVerify: true ao depurar signer e fee payer; use false apenas ao testar lógica de programa com fixtures não assinadas.
Como capturar logs em rota API Next.js?
Simule server-side com @solana/kit, retorne { err, logs, unitsConsumed } estruturado ao client - nunca exponha signing keys.
Qual o limite padrão de compute units?
200.000 CUs por instruction a menos que você aumente com ComputeBudgetInstruction::set_compute_unit_limit.
Uma instruction falhando pode reverter a transação inteira?
Sim. Transações Solana são atômicas a menos que seu programa capture erros de CPI internamente.
Como correlacionar erros entre provedores RPC?
Logue signature, slot, block time, nome do provedor e JSON de erro. Compare getTransaction em provedor secundário.
Quando acionar time de programa vs time de client?
Custom: N do seu program ID -> programa. AccountNotFound em ATA do usuário -> client. RPC 429/503 -> infra.
Relacionado
- Transações que Não Pousam - quando simulação passa mas inclusão falha
- Outages de RPC e Infraestrutura - falhas do lado do provedor
- Checklists de Prevenção - monitores e alertas após incidentes
Versões do stack: Esta página foi escrita para Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, anchor-lang 0.32.1, Rust 1.91.1, @solana/kit 7.0.0, Surfpool 0.12.0 e LiteSVM 0.6.x.