Skip to main content

Transaction Simulation

Validate and analyze transactions before submitting them to the network.

Overview

Transaction simulation helps you:

  • Validate transactions before submission
  • Estimate fees accurately
  • Detect errors and warnings
  • Analyze input/output balance
  • Verify sufficient funds

Installation

import {
simulateTransaction,
formatSimulationResult,
type SimulationTx,
type SimulationResult,
} from 'cardano-devkit';

Basic Usage

// Get UTxOs for inputs
const utxos = await lucid.wallet().getUtxos();

// Define transaction
const result = simulateTransaction({
inputs: utxos.slice(0, 2),
outputs: [
{
address: "addr_test1...",
value: { lovelace: 5_000_000n },
},
{
address: "addr_test2...",
value: { lovelace: 10_000_000n },
},
],
});

// Check result
if (result.valid) {
console.log("✅ Transaction is valid");
console.log("Estimated fee:", result.estimatedFee);
console.log("Change:", result.change);
} else {
console.log("❌ Transaction invalid");
result.errors.forEach(e => console.log("Error:", e.message));
}

Simulation Input

Input Format

interface SimulationInput {
txHash: string;
outputIndex: number;
assets: {
lovelace: bigint;
[policyId_assetName: string]: bigint;
};
address?: string;
datum?: string;
datumHash?: string;
scriptRef?: string;
}

Output Format

interface SimulationOutput {
address: string;
value: {
lovelace: bigint;
[policyId_assetName: string]: bigint;
};
datum?: string;
datumHash?: string;
scriptRef?: string;
}

Complete Transaction

const tx: SimulationTx = {
inputs: [
{
txHash: "abc...",
outputIndex: 0,
assets: { lovelace: 20_000_000n },
},
],
outputs: [
{
address: "addr_test1...",
value: { lovelace: 5_000_000n },
},
],
mint?: {
[policyId + assetName]: 100n,
},
metadata?: {
674: { msg: ["Hello World"] },
},
scripts?: [
{ type: "plutus", size: 1500 },
],
};

const result = simulateTransaction(tx);

Simulation Result

interface SimulationResult {
valid: boolean;

// Balance calculations
totalInputs: bigint;
totalOutputs: bigint;
estimatedFee: bigint;
change: bigint;

// Counts
inputCount: number;
outputCount: number;

// Size estimate
sizeEstimate: number;

// Issues
errors: SimulationError[];
warnings: SimulationWarning[];

// Asset balance
assetBalance: Record<string, bigint>;
}

Error Types

interface SimulationError {
code: string;
message: string;
details?: Record<string, unknown>;
}

// Common error codes:
// - INSUFFICIENT_FUNDS: Not enough ADA
// - INSUFFICIENT_ASSETS: Not enough native tokens
// - NO_INPUTS: No inputs provided
// - NO_OUTPUTS: No outputs provided
// - NEGATIVE_CHANGE: Outputs exceed inputs
// - MIN_UTXO_NOT_MET: Output below minimum UTxO

Warning Types

interface SimulationWarning {
code: string;
message: string;
details?: Record<string, unknown>;
}

// Common warning codes:
// - DUST_OUTPUT: Output is close to minimum
// - HIGH_FEE: Fee seems unusually high
// - MANY_INPUTS: Large number of inputs
// - ASSET_IMBALANCE: Unbalanced native tokens

Native Asset Simulation

const result = simulateTransaction({
inputs: [
{
txHash: "abc...",
outputIndex: 0,
assets: {
lovelace: 10_000_000n,
[policyId + "TokenA"]: 1000n,
},
},
],
outputs: [
{
address: "addr_test1...",
value: {
lovelace: 3_000_000n,
[policyId + "TokenA"]: 500n,
},
},
],
});

// Check asset balance
console.log("Asset balance:", result.assetBalance);
// { "lovelace": 7000000n, "[policyId]TokenA": 500n }

With Minting

const result = simulateTransaction({
inputs: utxos,
outputs: [
{
address: "addr_test1...",
value: {
lovelace: 2_000_000n,
[policyId + "NewToken"]: 100n,
},
},
],
mint: {
[policyId + "NewToken"]: 100n, // Minting 100 tokens
},
});

Formatting Results

const result = simulateTransaction(tx);
console.log(formatSimulationResult(result));

Output:

╔═══════════════════════════════════════════════════════════════╗
║ 🔮 Transaction Simulation ║
╠═══════════════════════════════════════════════════════════════╣
║ Status: ✅ VALID ║
╟───────────────────────────────────────────────────────────────╢
║ Inputs: 2 UTxOs ║
║ Outputs: 2 ║
║ Size: ~350 bytes ║
╟───────────────────────────────────────────────────────────────╢
║ Balance ║
║ Total In: 50.000000 ADA ║
║ Total Out: 15.000000 ADA ║
║ Est. Fee: 0.200000 ADA ║
║ Change: 34.800000 ADA ║
╚═══════════════════════════════════════════════════════════════╝

CLI Usage

# Simulate from file
cardano-devkit simulate --file tx.json

# Simulate with inline data
cardano-devkit simulate \
-i '[{"txHash":"abc...","outputIndex":0,"assets":{"lovelace":"10000000"}}]' \
-o '[{"address":"addr_test1...","lovelace":"5000000"}]'

# Output as JSON
cardano-devkit simulate --file tx.json --json

Integration Example

import { simulateTransaction, createUTxOManager } from 'cardano-devkit';

async function safePayment(
lucid: LucidEvolution,
recipient: string,
amount: bigint
): Promise<string> {
// Get UTxOs
const utxos = await lucid.wallet().getUtxos();
const manager = createUTxOManager(utxos);

// Select inputs
const selection = manager.select(amount + 500_000n); // Amount + estimated fee
if (!selection.success) {
throw new Error(`Insufficient funds: ${selection.error}`);
}

// Simulate transaction
const simulation = simulateTransaction({
inputs: selection.selected,
outputs: [
{
address: recipient,
value: { lovelace: amount },
},
],
});

// Check for errors
if (!simulation.valid) {
const errorMessages = simulation.errors.map(e => e.message).join(", ");
throw new Error(`Simulation failed: ${errorMessages}`);
}

// Log warnings
simulation.warnings.forEach(w => {
console.warn(`Warning: ${w.message}`);
});

// Proceed with actual transaction
const tx = await lucid
.newTx()
.pay.ToAddress(recipient, { lovelace: amount })
.complete();

const signed = await tx.sign.withWallet().complete();
return signed.submit();
}

Best Practices

  1. Always simulate before submitting important transactions
  2. Check warnings even when valid - they may indicate suboptimal transactions
  3. Validate asset balances for native token transactions
  4. Consider fee margin - actual fee may vary slightly
  5. Handle errors gracefully with user-friendly messages