Skip to main content

SimulateTransaction

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.v2beta2.LiveDataService Method: SimulateTransaction Type: Unary RPC

Parameters#

ParameterTypeRequiredDescription
transactionTransactionDataYesBCS-encoded transaction bytes
read_maskFieldMaskNoFields to include in response

Field Mask Options#

PathDescription
effectsTransaction execution effects
eventsEvents that would be emitted
object_changesObject modifications
balance_changesBalance alterations

Response Structure#

message SimulateTransactionResponse {
TransactionEffects effects = 1;
repeated Event events = 2;
repeated ObjectChange object_changes = 3;
repeated BalanceChange balance_changes = 4;
}

Transaction Effects#

message TransactionEffects {
ExecutionStatus status = 1;
GasUsed gas_used = 2;
repeated OwnedObjectRef modified_at_versions = 3;
repeated ObjectRef shared_objects = 4;
repeated ObjectRef created = 5;
repeated ObjectRef mutated = 6;
repeated ObjectRef unwrapped = 7;
repeated ObjectRef deleted = 8;
repeated ObjectRef wrapped = 9;
ObjectRef gas_object = 10;
repeated Event events_digest = 11;
repeated string dependencies = 12;
}

Code Examples#

import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';

const ENDPOINT = 'api-sui-mainnet-full.n.dwellir.com';
const API_TOKEN = 'your_api_token_here';

const packageDefinition = protoLoader.loadSync('./protos/livedata.proto', {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
includeDirs: ['./protos']
});

const protoDescriptor = grpc.loadPackageDefinition(packageDefinition) as any;
const credentials = grpc.credentials.createSsl();
const client = new protoDescriptor.sui.rpc.v2beta2.LiveDataService(ENDPOINT, credentials);

const metadata = new grpc.Metadata();
metadata.add('x-api-key', API_TOKEN);

async function simulateTransaction(
transactionBytes: Uint8Array
): Promise<any> {
return new Promise((resolve, reject) => {
const request = {
transaction: { bcs: transactionBytes },
read_mask: {
paths: ['effects', 'events', 'object_changes', 'balance_changes']
}
};

client.SimulateTransaction(request, metadata, (error: any, response: any) => {
if (error) {
console.error('SimulateTransaction error:', error.message);
reject(error);
return;
}

resolve(response);
});
});
}

// Usage
const txBytes = buildTransaction(); // Your transaction builder
const simulation = await simulateTransaction(txBytes);

console.log('Status:', simulation.effects.status.status);
console.log('Gas Cost:', simulation.effects.gasUsed.computationCost);
console.log('Events:', simulation.events.length);
console.log('Objects Created:', simulation.effects.created.length);

Use Cases#

Gas Cost Estimation#

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#

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#

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#

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#

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#

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#

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#

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.