Depuración de Transacciones Fallidas
Las transacciones fallidas de Solana dejan pistas en los registros de simulación, los códigos de error RPC y la salida del programa del explorador. Un flujo de trabajo de decodificación disciplinado separa los errores del usuario, los errores del cliente, los problemas de RPC y los defectos del programa en la cadena antes de que reintentes o contactes al personal de guardia.
Receta
Tarjeta de receta de referencia rápida, lista para copiar y pegar.
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);
}Cuándo usar esto:
- Un usuario informa "transacción fallida" solo con una firma
- Preflight rechaza antes de que la transacción llegue a la red
- El programa devuelve
Custom: Ny necesitas el mapeo del enum - Los errores de cómputo o tamaño de cuenta se disparan después de un despliegue
- La clasificación de incidentes necesita la clase de error antes de contactar a los propietarios del programa
Ejemplo de Trabajo
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,
};
}Lo que esto demuestra:
- Construir una transacción real y simular con
replaceRecentBlockhashdesactivado al probar la validez de la firma - Leer
err,logsyunitsConsumedcomo el triple de depuración principal - Usar los tipos RPC de
@solana/kit7.0.0 para respuestas de simulación tipificadas - Capturar el estado de la cuenta posterior a la simulación al depurar desajustes de alquiler o propietario
Análisis Detallado
Cómo Funciona
- Simulación de preflight ejecuta la transacción contra el estado actual del ledger antes de la transmisión (cuando está habilitada en
sendTransaction). - Los registros del programa se emiten a través de
sol_log/msg!/anchor_lang::prelude::*y aparecen en la simulación y en los metadatos de las transacciones confirmadas. - Los errores personalizados del programa se codifican como
InstructionError::Custom(u32)- mapea a tu IDL o enum#[error_code]. - El presupuesto de cómputo por defecto es 200,000 CUs por instrucción a menos que antepongas instrucciones de
ComputeBudget. - Los desajustes de metadatos de cuenta fallan antes de la ejecución: indicador de firma incorrecto, indicador de escritura incorrecto o cuenta faltante.
Clases de Error de un Vistazo
| Síntoma | Causa Típica | Primera Verificación |
|---|---|---|
BlockhashNotFound | Blockhash expirado o incorrecto del clúster | getLatestBlockhash, URL del clúster |
AccountNotFound | ATA faltante, cuenta cerrada, PDA incorrecto | Pestaña de cuenta del explorador, derivación de semilla |
InsufficientFundsForFee | Pagador de tarifa con fondos insuficientes | Saldo de lamports vs tarifa + alquiler |
ComputationalBudgetExceeded | Ruta activa o bucle ilimitado | Perfil de CU, instrucción ComputeBudget |
Custom: N | require! del programa / ErrorCode | Tabla de errores de IDL, CLI anchor error |
Notas de 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 de Rust / Anchor
// Anchor 0.32.1 - los errores aparecen como códigos personalizados en los logs
#[error_code]
pub enum MyError {
#[msg("Vault is paused")]
VaultPaused,
}
// Mapeo: anchor error --program-id <PID> muestra el código -> nombreTrampas Comunes
- Reintentar sin leer los logs - Los usuarios envían repetidamente; quemas tarifas de prioridad en la misma falla determinista. Solución: Simula una vez, clasifica el error, corrige la causa raíz.
- RPC del clúster incorrecto - Una transacción de Devnet simulada contra un RPC de Mainnet devuelve
AccountNotFound. Solución: FijaRPC_URL, la red de la billetera y el?cluster=del explorador juntos. - Asumir que preflight siempre está activado - Algunas rutas del servidor desactivan preflight por latencia. Solución:
simulateTransactionexplícito en herramientas de soporte. - Cantidades decimales vs lamports - El cliente envía
1.5SOL como flotante; el programa espera lamports enteros. Solución:bigintde extremo a extremo; convierte solo en el límite de la UI. - Mapa de errores de IDL obsoleto - Programa v2 desplegado, el cliente aún decodifica códigos v1. Solución: Versiona el IDL en CI; regenera los clientes en cada lanzamiento del programa.
- Ignorar
unitsConsumed- Los programas cercanos al límite fallan solo bajo carga. Solución: Rastrea las CUs en métricas; alerta cuando el p95 excede el 80% del presupuesto.
Alternativas
| Alternativa | Usar Cuando | No Usar Cuando |
|---|---|---|
| Clasificación solo con el explorador | Soporte rápido de usuario único | Necesitas automatización reproducible |
CLI solana confirm -v | Bastión SSH con CLI 3.0.10 | Servidores de aplicaciones de producción sin claves |
| Replay local Surfpool / LiteSVM | Reproducir conjuntos de cuentas de mainnet | Necesitas comportamiento real de congestión de red |
| Logs de Geyser / indexador | Análisis de fallos masivos de alto volumen | Informe de usuario de transacción única |
Preguntas Frecuentes
¿Debo deshabilitar preflight en producción?
No para envíos dirigidos al usuario. Mantén preflight activado a menos que tengas una razón de latencia medida y simulación del lado del servidor como respaldo.
¿Dónde aparecen los logs `msg!` de Anchor?
En los logs de simulación y en los metadatos de la transacción confirmada a través del RPC getTransaction.
¿Cómo decodifico `Custom: 6003`?
Ejecuta anchor error 6003 contra el IDL de tu programa, o lee el orden del enum #[error_code] (los códigos comienzan en 6000 en Anchor).
¿Qué pasa si la simulación tiene éxito pero el envío falla?
Probablemente expiración del blockhash, caída de red o omisión del líder. Actualiza el blockhash, verifica el estado de la firma e inspecciona si la transacción aterrizó en un slot posterior.
¿`sigVerify: false` oculta errores de firma?
Sí. Usa sigVerify: true al depurar problemas de firmante y pagador de tarifas; usa false solo al probar la lógica del programa con fixtures sin firmar.
¿Cómo capturo logs en una ruta de API de Next.js?
Simula del lado del servidor con @solana/kit, devuelve { err, logs, unitsConsumed } estructurado al cliente; nunca expongas claves de firma.
¿Cuál es el límite predeterminado de unidades de cómputo?
200,000 CUs por instrucción a menos que lo aumentes con ComputeBudgetInstruction::set_compute_unit_limit.
¿Puede una instrucción fallida revertir toda la transacción?
Sí. Las transacciones de Solana son atómicas a menos que tu programa capture errores de CPI internamente.
¿Cómo correlaciono errores entre proveedores RPC?
Registra la firma, el slot, la hora del bloque, el nombre del proveedor y el JSON del error. Compara getTransaction entre un proveedor secundario.
¿Cuándo debo contactar al equipo del programa en lugar del equipo del cliente?
Custom: N de tu ID de programa -> programa. AccountNotFound en el ATA del usuario -> cliente. RPC 429/503 -> infraestructura.
Relacionado
- Transacciones que no Aterrizan - cuando la simulación pasa pero la inclusión falla
- Interrupciones de RPC e Infraestructura - fallos del lado del proveedor
- Listas de Verificación de Prevención - monitores y alertas después de incidentes
Versiones de Stack: Esta página fue 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, y LiteSVM 0.6.x.