Docs

SimulateTransaction - Preview Transaction Effects

Simulate Sui transactions before execution to preview effects, gas costs, and potential errors via gRPC. Essential for safe transaction building with Dwellir.

Preview Transaction Effects Without Execution

The SimulateTransaction method allows you to preview the effects of a transaction before actually executing it on-chain. This is essential for estimating gas costs, validating transaction logic, detecting potential errors, and building safe user experiences that show expected outcomes before commitment.

Method Signature

Service: sui.rpc.v2.TransactionExecutionService Method: SimulateTransaction Type: Unary RPC

Use Cases

Gas Cost Estimation

TypeScript
interface GasEstimate {
  computationCost: bigint;
  storageCost: bigint;
  storageRebate: bigint;
  totalCost: bigint;
  estimatedSUI: string;
}

async function estimateGasCost(
  transactionBytes: Uint8Array
): Promise<GasEstimate> {
  const simulation = await simulateTransaction(transactionBytes);

  const gas = simulation.effects.gasUsed;
  const computation = BigInt(gas.computationCost);
  const storage = BigInt(gas.storageCost);
  const rebate = BigInt(gas.storageRebate);
  const total = computation + storage - rebate;

  return {
    computationCost: computation,
    storageCost: storage,
    storageRebate: rebate,
    totalCost: total,
    estimatedSUI: (Number(total) / 1_000_000_000).toFixed(9)
  };
}

// Usage
const txBytes = buildTransferTransaction(recipient, amount);
const estimate = await estimateGasCost(txBytes);

console.log(`Estimated gas: ${estimate.estimatedSUI} SUI`);
console.log(`Breakdown: Computation=${estimate.computationCost}, Storage=${estimate.storageCost}`);

Transaction Validation

TypeScript
interface ValidationResult {
  isValid: boolean;
  errors: string[];
  warnings: string[];
  simulation?: any;
}

async function validateTransaction(
  transactionBytes: Uint8Array
): Promise<ValidationResult> {
  const result: ValidationResult = {
    isValid: false,
    errors: [],
    warnings: []
  };

  try {
    const simulation = await simulateTransaction(transactionBytes);
    result.simulation = simulation;

    // Check execution status
    if (simulation.effects.status.status !== 'success') {
      result.errors.push(
        `Transaction would fail: ${simulation.effects.status.error || 'Unknown error'}`
      );
      return result;
    }

    // Check gas cost
    const totalGas = BigInt(simulation.effects.gasUsed.computationCost) +
                     BigInt(simulation.effects.gasUsed.storageCost) -
                     BigInt(simulation.effects.gasUsed.storageRebate);

    if (totalGas > BigInt(1_000_000_000)) {  // More than 1 SUI
      result.warnings.push(
        `High gas cost: ${Number(totalGas) / 1_000_000_000} SUI`
      );
    }

    // Check object deletions
    if (simulation.effects.deleted && simulation.effects.deleted.length > 0) {
      result.warnings.push(
        `Transaction will delete ${simulation.effects.deleted.length} objects`
      );
    }

    result.isValid = true;
    return result;

  } catch (error: any) {
    result.errors.push(`Simulation failed: ${error.message}`);
    return result;
  }
}

// Usage
const validation = await validateTransaction(txBytes);

if (!validation.isValid) {
  console.error('Transaction validation failed:', validation.errors);
  return;
}

if (validation.warnings.length > 0) {
  console.warn('Warnings:', validation.warnings);
}

console.log('Transaction is valid and ready to execute');

Preview Balance Changes

TypeScript
interface BalanceChange {
  owner: string;
  coinType: string;
  amount: string;
  isPositive: boolean;
}

async function previewBalanceChanges(
  transactionBytes: Uint8Array
): Promise<BalanceChange[]> {
  const simulation = await simulateTransaction(transactionBytes);

  return simulation.balance_changes.map((change: any) => {
    const amount = BigInt(change.amount);

    return {
      owner: change.owner,
      coinType: change.coin_type,
      amount: amount.toString().replace('-', ''),
      isPositive: amount > 0n
    };
  });
}

// Usage for user confirmation UI
const changes = await previewBalanceChanges(txBytes);

console.log('Transaction will:');
changes.forEach(change => {
  const direction = change.isPositive ? 'receive' : 'send';
  const amount = Number(change.amount) / 1_000_000_000;
  console.log(`  ${direction} ${amount} SUI`);
});

Safe Transaction Builder

TypeScript
class SafeTransactionBuilder {
  async buildAndValidate(
    sender: string,
    recipient: string,
    amount: bigint
  ): Promise<{ txBytes: Uint8Array; simulation: any }> {
    // Build transaction
    const txBytes = await this.buildTransferTransaction(
      sender,
      recipient,
      amount
    );

    // Simulate first
    const simulation = await simulateTransaction(txBytes);

    // Validate results
    if (simulation.effects.status.status !== 'success') {
      throw new Error(
        `Transaction would fail: ${simulation.effects.status.error}`
      );
    }

    // Check sender has sufficient balance including gas
    const balanceChanges = simulation.balance_changes.filter(
      (c: any) => c.owner === sender
    );

    const totalDeduction = balanceChanges.reduce(
      (sum: bigint, change: any) => {
        const amt = BigInt(change.amount);
        return amt < 0n ? sum + (-amt) : sum;
      },
      0n
    );

    // Get sender's current balance
    const currentBalance = await this.getBalance(sender);

    if (BigInt(currentBalance) < totalDeduction) {
      throw new Error('Insufficient balance including gas costs');
    }

    return { txBytes, simulation };
  }

  private async buildTransferTransaction(
    sender: string,
    recipient: string,
    amount: bigint
  ): Promise<Uint8Array> {
    // Implementation depends on your transaction builder
    throw new Error('Not implemented');
  }

  private async getBalance(owner: string): Promise<string> {
    // Implementation uses GetBalance method
    throw new Error('Not implemented');
  }
}

// Usage
const builder = new SafeTransactionBuilder();

try {
  const { txBytes, simulation } = await builder.buildAndValidate(
    senderAddress,
    recipientAddress,
    5_000_000_000n  // 5 SUI
  );

  console.log('Transaction validated successfully');
  console.log('Estimated gas:', simulation.effects.gasUsed.computationCost);

  // Now safe to execute
  // await executeTransaction(txBytes, signature);

} catch (error) {
  console.error('Transaction building failed:', error);
}

Smart Contract Interaction Preview

TypeScript
async function previewContractCall(
  transactionBytes: Uint8Array
): Promise<any> {
  const simulation = await simulateTransaction(transactionBytes);

  return {
    success: simulation.effects.status.status === 'success',
    gasUsed: simulation.effects.gasUsed,
    eventsEmitted: simulation.events.map((event: any) => ({
      type: event.type,
      sender: event.sender,
      data: event.data
    })),
    objectsCreated: simulation.effects.created,
    objectsMutated: simulation.effects.mutated,
    objectsDeleted: simulation.effects.deleted
  };
}

// Usage for DeFi operations
const swapTx = buildSwapTransaction(tokenIn, tokenOut, amountIn);
const preview = await previewContractCall(swapTx);

if (preview.success) {
  const swapEvent = preview.eventsEmitted.find(
    (e: any) => e.type.includes('SwapEvent')
  );

  console.log('Expected output:', swapEvent.data.amountOut);
  console.log('Gas cost:', preview.gasUsed.computationCost);
} else {
  console.error('Swap would fail');
}

Best Practices

Always Simulate Before Execute

TypeScript
async function safeExecuteTransaction(
  transactionBytes: Uint8Array,
  signature: Uint8Array,
  publicKey: Uint8Array
): Promise<any> {
  // Step 1: Simulate
  const simulation = await simulateTransaction(transactionBytes);

  // Step 2: Validate simulation results
  if (simulation.effects.status.status !== 'success') {
    throw new Error(
      `Simulation failed: ${simulation.effects.status.error || 'Unknown error'}`
    );
  }

  // Step 3: Execute only if simulation succeeds
  return await executeTransaction(transactionBytes, signature, publicKey);
}

Handle Simulation Errors

TypeScript
async function simulateWithErrorHandling(
  transactionBytes: Uint8Array
): Promise<any | null> {
  try {
    return await simulateTransaction(transactionBytes);
  } catch (error: any) {
    if (error.code === grpc.status.INVALID_ARGUMENT) {
      console.error('Invalid transaction format:', error.message);
      return null;
    }

    if (error.code === grpc.status.FAILED_PRECONDITION) {
      console.error('Transaction preconditions not met:', error.message);
      return null;
    }

    // Re-throw unexpected errors
    throw error;
  }
}

Cache Recent Simulations

TypeScript
class SimulationCache {
  private cache = new Map<string, any>();
  private ttl = 30000; // 30 seconds

  async simulate(transactionBytes: Uint8Array): Promise<any> {
    const key = this.hashTransaction(transactionBytes);
    const cached = this.cache.get(key);

    if (cached && Date.now() - cached.timestamp < this.ttl) {
      return cached.data;
    }

    const result = await simulateTransaction(transactionBytes);

    this.cache.set(key, {
      data: result,
      timestamp: Date.now()
    });

    return result;
  }

  private hashTransaction(bytes: Uint8Array): string {
    // Simple hash - use crypto hash in production
    return Array.from(bytes).join(',');
  }
}

Performance Characteristics

MetricValue
Typical Latency50-150ms
Response Size1-10KB (varies by transaction)
Cache RecommendedYes (short TTL)
Rate Limit ImpactMedium

Common Errors

Error CodeScenarioSolution
INVALID_ARGUMENTMalformed transaction bytesVerify BCS encoding
FAILED_PRECONDITIONInsufficient gas or invalid inputsCheck transaction parameters
UNAVAILABLENetwork issuesImplement retry logic

Comparison with JSON-RPC

FeaturegRPC SimulateTransactionJSON-RPC sui_dryRunTransactionBlock
Latency50-150ms100-300ms
Response SizeSmaller (binary)Larger (JSON)
Type SafetyStrong (protobuf)Weak (JSON)
Streaming SupportYes (future)No

Need help? Contact support@dwellir.com or check the gRPC overview.