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
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
| Metric | Value |
|---|---|
| Typical Latency | 50-150ms |
| Response Size | 1-10KB (varies by transaction) |
| Cache Recommended | Yes (short TTL) |
| Rate Limit Impact | Medium |
Common Errors
| Error Code | Scenario | Solution |
|---|---|---|
INVALID_ARGUMENT | Malformed transaction bytes | Verify BCS encoding |
FAILED_PRECONDITION | Insufficient gas or invalid inputs | Check transaction parameters |
UNAVAILABLE | Network issues | Implement retry logic |
Comparison with JSON-RPC
| Feature | gRPC SimulateTransaction | JSON-RPC sui_dryRunTransactionBlock |
|---|---|---|
| Latency | 50-150ms | 100-300ms |
| Response Size | Smaller (binary) | Larger (JSON) |
| Type Safety | Strong (protobuf) | Weak (JSON) |
| Streaming Support | Yes (future) | No |
Related Methods
- ExecuteTransaction - Execute validated transactions
- GetBalance - Check balances before simulation
Need help? Contact support@dwellir.com or check the gRPC overview.
ListOwnedObjects
Query all objects owned by a Sui address via gRPC with pagination support. Essential for wallet interfaces, portfolio tracking, and asset discovery with Dwellir's infrastructure.
SubscribeCheckpoints
Stream Sui blockchain checkpoints in real-time using gRPC server-streaming RPC. Monitor network activity, track finality, and build event-driven applications with Dwellir's high-performance infrastructure.