Antes de transmitir una transacción de Sui, puedes simularla a través de JSON-RPC usando devInspectTransaction o dryRunTransactionBlock. Estos métodos ejecutan la transacción contra el estado actual sin confirmarla, devolviendo los TransactionEffects y el estado de ejecución. Esto te permite detectar conflictos de versión de objetos, gas insuficiente y errores a nivel de comando sin costo alguno, asegurando que tu transacción tenga éxito cuando realmente se envíe.
Respuesta directa: simula primero, paga después
Las transacciones de Sui no se ejecutan en el vacío: operan sobre un conjunto de objetos propios y compartidos, cada uno con una versión específica. Una transacción que tiene éxito contra una versión de objeto puede fallar contra otra. Para evitar desperdiciar gas en transacciones fallidas, Sui proporciona dos métodos JSON-RPC que te permiten simular una transacción antes de transmitirla: devInspectTransaction y dryRunTransactionBlock. Ambos devuelven los TransactionEffects que resultarían, incluyendo un estado de ejecución de success o failure. Al inspeccionar estos efectos, puedes detectar conflictos de versión de objetos, gas insuficiente y errores a nivel de comando antes de que te cuesten algo.
Esta guía explica el mecanismo detrás de estos métodos de simulación, cómo decodificar los efectos devueltos, y proporciona un script TypeScript reproducible usando el SDK oficial @mysten/sui/client. Aprenderás a distinguir entre una simulación exitosa y una fallida, y cómo manejar los errores comunes que tropiezan a los desarrolladores.
Entendiendo la ejecución de transacciones Sui y el versionado de objetos
En Sui, una transacción es una estructura TransactionData que contiene un TransactionBlock programable. Este bloque consiste en comandos que operan sobre entradas, que son objetos propios, objetos compartidos o valores puros. Cada objeto en Sui tiene un ID único y un número de versión que aumenta monótonamente. Cuando se ejecuta una transacción, los validadores verifican que las versiones de objeto referenciadas en la transacción coincidan con las versiones actuales en la cadena. Si no coinciden, la transacción falla con un conflicto de versión de objeto.
Este versionado es crucial para la simulación: cuando simulas una transacción, debes asegurarte de que las versiones de objeto que proporcionas son las que pretendes usar. Si obtienes la referencia de un objeto (ID y versión) y luego construyes una transacción, pero el objeto es mutado por otra transacción antes de que la envíes, tu simulación puede tener éxito mientras que el envío real falla. Por lo tanto, siempre obtén las referencias de objeto más recientes inmediatamente antes de construir tu transacción.
Los dos métodos de simulación difieren en su enfoque. dryRunTransactionBlock ejecuta la transacción como si se estuviera enviando, requiriendo un objeto de gas y un presupuesto de gas. Devuelve los efectos exactos que se confirmarían, incluyendo el gas utilizado. devInspectTransaction, por otro lado, ejecuta la transacción contra un conjunto de objetos suministrados sin cobrar gas y sin requerir un objeto de gas. Asume un presupuesto de gas infinito y está destinado al desarrollo y pruebas. Los resultados de devInspectTransaction no deben tomarse como el resultado exacto en mainnet, porque usa los objetos que proporcionas, no necesariamente el estado actual en la cadena.
Ambos métodos son de solo lectura: no mutan el estado. Sin embargo, dryRunTransactionBlock requiere un pago de gas y fallará si el presupuesto de gas es insuficiente, mientras que devInspectTransaction no. Esto hace que devInspectTransaction sea ideal para escenarios hipotéticos, como probar un nuevo comando contra un estado de objeto hipotético.
Los dos puntos de entrada de simulación: dryRunTransactionBlock vs devInspectTransaction
La API JSON-RPC de Sui proporciona dos métodos principales para simular transacciones. Ten en cuenta que los nombres de los métodos han evolucionado: devInspectTransactionBlock fue renombrado a devInspectTransaction en versiones recientes del SDK, y los nombres antiguos están marcados como obsoletos en varias referencias. Usa siempre los nombres de método actuales según tu SDK y la versión de API de tu endpoint; la documentación JSON-RPC de Sui y las referencias a dryRunTransactionBlock y devInspectTransaction describen las firmas actuales. Por ejemplo, el SDK @mysten/sui/client expone client.devInspectTransaction y client.dryRunTransactionBlock.
dryRunTransactionBlock toma un TransactionBlock (o sus bytes serializados) y una dirección de sender. Ejecuta la transacción contra el estado actual, usando el objeto de gas especificado en la transacción. Devuelve una DryRunTransactionBlockResponse que contiene los effects y cualquier error. Los effects incluyen el status (success o failure), el gasUsed, y la lista de objetos creados, mutados y eliminados.
devInspectTransaction toma una dirección de sender, un TransactionBlock, y opcionalmente una lista de gasPrice y epoch. No requiere un objeto de gas; en su lugar, usa una moneda de gas simulada con un saldo infinito. Devuelve una DevInspectResponse con los effects y results para cada comando. Los effects incluyen un resumen de gasUsed, pero como el presupuesto de gas es infinito, el costo real de gas no es representativo de una transacción real.
Diferencia clave: dryRunTransactionBlock fallará si el presupuesto de gas es insuficiente, mientras que devInspectTransaction no. Por lo tanto, para probar si tu transacción tendrá éxito con un presupuesto de gas específico, usa dryRunTransactionBlock. Para probar la lógica de tu transacción sin preocuparte por el gas, usa devInspectTransaction.
Decodificando el estado de ejecución y los efectos
La parte más importante de una respuesta de simulación es effects.status. Este es un objeto con un campo status que es 'success' o 'failure'. Si es 'failure', el campo error contiene una cadena que describe el error. Los errores comunes incluyen 'MoveAbort', 'MoveModule', 'U64Wrap' y conflictos de versión de objeto.
Un fallo a nivel de comando ocurre cuando un comando específico en el bloque de transacción falla. Por ejemplo, un error MoveAbort indica que un módulo Move abortó, a menudo debido a una aserción fallida. Un conflicto de versión de objeto ocurre cuando la versión del objeto de entrada no coincide con la versión actual en la cadena. Este es un problema común cuando construyes una transacción usando referencias de objeto obsoletas.
Es crucial distinguir entre un error RPC y una llamada RPC exitosa con un estado de fallo. Si la llamada de simulación en sí devuelve un error (por ejemplo, parámetros inválidos), eso es un problema del lado del cliente. Si la llamada tiene éxito pero effects.status es 'failure', eso significa que la transacción fallaría si se enviara. Muchos desarrolladores tratan erróneamente cualquier error como un fallo RPC, pero debes verificar el campo effects.status.
Los effects también contienen un objeto gasUsed con computationCost, storageCost y storageRebate. En una ejecución de prueba, estos reflejan el gas real que se usaría. En una inspección de desarrollo, el gas usado se calcula asumiendo un presupuesto infinito, por lo que el gasUsed puede ser más alto de lo que realmente pagarías. Usa siempre dryRunTransactionBlock para estimar los costos reales de gas.
Ejemplo reproducible: Simulando una transferencia de monedas con @mysten/sui/client
El siguiente script TypeScript demuestra cómo simular una transferencia de monedas simple usando devInspectTransaction y dryRunTransactionBlock. Se conecta a un endpoint de Sui, construye una transacción que transfiere una cantidad específica de SUI de una dirección a otra, y luego la simula. El script imprime el estado de ejecución, el resumen de gas y cualquier error.
Para ejecutar este script, necesitarás Node.js y el paquete @mysten/sui. Instálalo con npm install @mysten/sui. Reemplaza la URL del endpoint y las direcciones con las tuyas. El script usa el método devInspectTransaction, pero puedes cambiar fácilmente a dryRunTransactionBlock descomentando las líneas relevantes.
Nota: El script asume que tienes un objeto (una moneda) para transferir. Necesitarás proporcionar el ID del objeto y su versión. En un escenario real, obtendrías estos usando suix_getOwnedObjects o suix_getDynamicField.
import { SuiClient, getFullnodeUrl } from '@mysten/sui/client';
import { Transaction } from '@mysten/sui/transactions';
// Connect to a Sui endpoint (replace with your endpoint)
const client = new SuiClient({ url: getFullnodeUrl('mainnet') });
async function simulateTransfer() {
const sender = '0xYOUR_SENDER_ADDRESS';
const recipient = '0xRECIPIENT_ADDRESS';
const coinObjectId = '0xCOIN_OBJECT_ID';
const amount = 1000; // in MIST
// Build a transaction to transfer `amount` from the coin to recipient
const tx = new Transaction();
const coin = tx.object(coinObjectId);
tx.transferObjects([coin], recipient);
// Simulate using devInspectTransaction (no gas required)
const devInspectResult = await client.devInspectTransaction({
sender,
transactionBlock: tx,
});
console.log('DevInspect Status:', devInspectResult.effects.status);
console.log('DevInspect Gas Used:', devInspectResult.effects.gasUsed);
// To simulate with a gas budget, use dryRunTransactionBlock
// You need to set the gas budget and gas payment in the transaction
// tx.setGasBudget(1000);
// tx.setGasPayment([{ objectId: gasCoinId, version: gasCoinVersion, digest: gasCoinDigest }]);
// const dryRunResult = await client.dryRunTransactionBlock({
// transactionBlock: tx,
// sender,
// });
// console.log('DryRun Status:', dryRunResult.effects.status);
}
simulateTransfer().catch(console.error);Simulando escenarios de fallo: conflictos de versión de objeto y gas insuficiente
Para entender las formas de fallo, es instructivo simular intencionalmente una versión de objeto incorrecta o un presupuesto de gas insuficiente. El siguiente script demuestra cómo crear una transacción que referencia una versión antigua de un objeto, causando un conflicto de versión, y cómo simular una transacción con un presupuesto de gas demasiado bajo.
Para el conflicto de versión de objeto, puedes establecer manualmente la versión del objeto de moneda a una versión anterior (por ejemplo, versión 1) mientras que la versión actual es más alta. Esto hará que la simulación falle con un error que indica un desajuste de versión.
Para gas insuficiente, puedes establecer un presupuesto de gas muy bajo en la transacción y luego llamar a dryRunTransactionBlock. La simulación devolverá un estado de fallo con un error sobre gas insuficiente.
La tabla a continuación resume los estados de cadena comunes y sus significados. Completa la columna 'Fix' según tu escenario.
// Simulate an object version conflict
async function simulateVersionConflict() {
const sender = '0xYOUR_SENDER_ADDRESS';
const recipient = '0xRECIPIENT_ADDRESS';
const coinObjectId = '0xCOIN_OBJECT_ID';
// Assume the current version is 5, but we use version 1
const staleVersion = 1;
const staleDigest = '0xSTALE_DIGEST';
const tx = new Transaction();
const coin = tx.objectRef({
objectId: coinObjectId,
version: staleVersion,
digest: staleDigest,
});
tx.transferObjects([coin], recipient);
const result = await client.devInspectTransaction({
sender,
transactionBlock: tx,
});
console.log('Version Conflict Status:', result.effects.status);
console.log('Error:', result.effects.status.error);
}
// Simulate insufficient gas using dryRunTransactionBlock
async function simulateInsufficientGas() {
const sender = '0xYOUR_SENDER_ADDRESS';
const recipient = '0xRECIPIENT_ADDRESS';
const coinObjectId = '0xCOIN_OBJECT_ID';
const gasCoinId = '0xGAS_COIN_ID';
const gasCoinVersion = 1;
const gasCoinDigest = '0xGAS_COIN_DIGEST';
const tx = new Transaction();
const coin = tx.object(coinObjectId);
tx.transferObjects([coin], recipient);
tx.setGasBudget(1); // absurdly low
tx.setGasPayment([{ objectId: gasCoinId, version: gasCoinVersion, digest: gasCoinDigest }]);
const result = await client.dryRunTransactionBlock({
transactionBlock: tx,
sender,
});
console.log('Insufficient Gas Status:', result.effects.status);
console.log('Error:', result.effects.status.error);
}Tabla de decisión para completar sobre estados de simulación
Cuando simulas una transacción, encontrarás varios estados de cadena. La tabla a continuación enumera los comunes y sus significados. Úsala para diagnosticar problemas rápidamente.
Estado / Error Significado Solución successLa transacción tendría éxito. Envíala. failureconMoveAbortUn módulo Move abortó, a menudo debido a una aserción fallida. Inspecciona el código Move y el código de aborto. failureconMoveModuleOcurrió un error en un módulo Move, como una función faltante o un desajuste de tipos. Verifica la interfaz del módulo y los comandos de tu transacción. failureconU64WrapUn entero sin signo de 64 bits se desbordó. Ajusta tus cálculos para evitar el desbordamiento. failureconInsufficientGasEl presupuesto de gas es demasiado bajo. Aumenta el presupuesto de gas. failureconObjectVersionConflictLa versión del objeto en la transacción no coincide con la versión actual en la cadena. Obtén la referencia de objeto más reciente y reconstruye la transacción. failureconObjectDeletedEl objeto ha sido eliminado. Usa un objeto diferente o recréalo. failureconCommandArgumentErrorUn argumento de comando es inválido. Verifica los argumentos pasados al comando.
Errores comunes y lista de verificación de solución de problemas
Simular transacciones es sencillo, pero varios errores pueden llevar a confusión. Usa esta lista de verificación para evitarlos:
- Siempre obtén las referencias de objeto más recientes antes de construir tu transacción. Usa
suix_getDynamicFieldosuix_getOwnedObjectspara obtener la versión y el digest actuales. Las referencias obsoletas causan conflictos de versión.
- Verifica el campo
effects.status, no solo la respuesta RPC. Una llamada RPC exitosa puede devolver un estado de transacción fallido.
- Comprende la diferencia entre
devInspectTransactionydryRunTransactionBlock. El primero no requiere gas y usa un presupuesto infinito; el segundo simula con tu presupuesto de gas y pago especificados.
- Para la estimación de gas, usa
dryRunTransactionBlock. ElgasUseddedevInspectTransactionno es representativo porque asume un presupuesto infinito.
- Ten en cuenta los cambios de nombres de métodos.
devInspectTransactionBlockestá obsoleto; usadevInspectTransactionen los SDK actuales. Verifica los nombres de métodos soportados por la versión de API de tu endpoint.
- Al simular objetos compartidos, asegúrate de proporcionar la versión correcta del objeto compartido. Los objetos compartidos tienen una versión que se incrementa con cada transacción, por lo que debes obtener la más reciente.
- Si estás usando un bloque de transacción programable, asegúrate de que todos los comandos sean válidos y que las entradas estén correctamente referenciadas. Un solo comando inválido hará que toda la simulación falle.
Limitaciones y compensaciones de la simulación
La simulación es una herramienta poderosa, pero tiene limitaciones. devInspectTransaction no cobra gas y usa una moneda de gas simulada, por lo que no puede detectar errores de gas insuficiente. También usa los objetos que proporcionas, que pueden no reflejar el estado actual en la cadena si no los obtienes frescos. Por lo tanto, un devInspectTransaction exitoso no garantiza que la transacción real tenga éxito.
dryRunTransactionBlock es más preciso porque usa el objeto de gas real y el estado actual. Sin embargo, todavía no garantiza el éxito porque el estado puede cambiar entre la simulación y el envío real. Por ejemplo, otra transacción podría mutar un objeto que estás usando, causando un conflicto de versión.
Otra limitación es que la simulación no ejecuta código Move que tenga efectos secundarios fuera del alcance de la transacción. Por ejemplo, si tu transacción llama a una función Move que emite un evento, el evento no se emitirá durante la simulación. Esto es esperado, ya que la simulación es de solo lectura.
Finalmente, los resultados de la simulación son tan buenos como los datos que proporcionas. Si usas referencias de objeto obsoletas o parámetros incorrectos, la simulación será engañosa. Siempre obtén los datos más recientes antes de simular.
Próximos pasos y lecturas adicionales
Ahora que entiendes cómo simular transacciones Sui, puedes integrar esto en tu flujo de trabajo de desarrollo para ahorrar tiempo y dinero. Para profundizar tu conocimiento, explora los siguientes recursos:
- Guía RPC de Sui (Asistente RPC) para una visión general completa de los métodos RPC de Sui.
- Suscripciones WebSocket de Sui para aprender a suscribirte a los efectos de transacción en tiempo real.
- Tiempos de espera RPC de Sui para entender el comportamiento de tiempo de espera al simular transacciones grandes.
- Consulta del estado histórico de Sui a través de RPC para obtener versiones de objetos pasadas para pruebas.
- Monitoreo de endpoints RPC para asegurarte de que tu endpoint sea confiable para simulaciones.
- Centro de aprendizaje de OnFinality para más guías sobre Sui y otras redes.
- Resumen de la red Sui para información general sobre Sui.
- Servicio API para obtener un endpoint dedicado para tu desarrollo.
- Precios RPC para entender el costo de las llamadas RPC, incluyendo simulaciones.