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#
| Parameter | Type | Required | Description |
|---|---|---|---|
transaction | TransactionData | Yes | BCS-encoded transaction bytes |
read_mask | FieldMask | No | Fields to include in response |
Field Mask Options#
| Path | Description |
|---|---|
effects | Transaction execution effects |
events | Events that would be emitted |
object_changes | Object modifications |
balance_changes | Balance 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#
- TypeScript
- Python
- Go
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);
import grpc
from google.protobuf import field_mask_pb2
import livedata_service_pb2
import livedata_service_pb2_grpc
ENDPOINT = 'api-sui-mainnet-full.n.dwellir.com'
API_TOKEN = 'your_api_token_here'
def simulate_transaction(transaction_bytes: bytes):
credentials = grpc.ssl_channel_credentials()
channel = grpc.secure_channel(ENDPOINT, credentials)
client = livedata_service_pb2_grpc.LiveDataServiceStub(channel)
request = livedata_service_pb2.SimulateTransactionRequest(
transaction=livedata_service_pb2.TransactionData(
bcs=transaction_bytes
),
read_mask=field_mask_pb2.FieldMask(
paths=['effects', 'events', 'balance_changes']
)
)
metadata = [('x-api-key', API_TOKEN)]
response = client.SimulateTransaction(request, metadata=metadata)
print(f'Status: {response.effects.status.status}')
print(f'Gas Used: {response.effects.gas_used.computation_cost}')
print(f'Balance Changes: {len(response.balance_changes)}')
channel.close()
return response
# Usage
tx_bytes = build_transaction() # Your transaction builder
result = simulate_transaction(tx_bytes)
package main
import (
"context"
"fmt"
"log"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/types/known/fieldmaskpb"
pb "your-module/gen/sui/rpc/v2beta2"
)
const (
endpoint = "api-sui-mainnet-full.n.dwellir.com"
apiToken = "your_api_token_here"
)
func simulateTransaction(txBytes []byte) (*pb.SimulateTransactionResponse, error) {
creds := credentials.NewClientTLSFromCert(nil, "")
conn, err := grpc.Dial(endpoint, grpc.WithTransportCredentials(creds))
if err != nil {
return nil, err
}
defer conn.Close()
client := pb.NewLiveDataServiceClient(conn)
ctx := metadata.AppendToOutgoingContext(
context.Background(),
"x-api-key", apiToken,
)
req := &pb.SimulateTransactionRequest{
Transaction: &pb.TransactionData{
Bcs: txBytes,
},
ReadMask: &fieldmaskpb.FieldMask{
Paths: []string{"effects", "events", "balance_changes"},
},
}
resp, err := client.SimulateTransaction(ctx, req)
if err != nil {
return nil, err
}
fmt.Printf("Status: %s\n", resp.Effects.Status.Status)
fmt.Printf("Gas Used: %s\n", resp.Effects.GasUsed.ComputationCost)
return resp, nil
}
func main() {
txBytes := buildTransaction() // Your transaction builder
result, err := simulateTransaction(txBytes)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Simulation result: %+v\n", result)
}
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.