Skip to main content

simulateTransaction RPC method guide

Overview

The simulateTransaction endpoint in Soroban RPC allows you to submit a trial contract invocation to simulate how it would be executed by the Stellar network. This simulation calculates the effective transaction data, required authorizations, and minimal resource fee. It provides a way to test and analyze the potential outcomes of a transaction without actually submitting it to the network. It can be a nice way to get contract data as well sometimes.

While calling the method on the rpc server is not the ONLY way to simulate a transaction, it will likely be the most common and easiest way.

Here we will look at the objects involved and their definitions.

RPC Services

Stellar Development Foundation provides a testnet RPC service at http://soroban-testnet.stellar.org. For public network providers please refer to the Ecosystem RPC Providers list.

Testnet Endpoint:

https://soroban-testnet.stellar.org:443

SimulateTransactionParams is the argument passed to the simulateTransaction RPC endpoint:

interface SimulateTransactionParams {
transaction: string; // The Stellar transaction to be simulated, serialized as a base64 string.
resourceConfig?: {
instructionLeeway: number; // Allow this many extra instructions when budgeting resources.
};
}

SimulateTransactionResult is the return result from the call. It includes many useful things!

Things simulateTransaction is used for:

  1. Preparing invokeHostFunctionOp Transactions: Anytime you need to submit an invokeHostFunctionOp transaction to the network.
  2. Footprint Determination: To determine the ledger footprint, which includes all the data entries the transaction will read or write.
  3. Authorization Identification: To identify the authorizations required for the transaction.
  4. Error Detection: To detect potential errors and issues before actual submission, saving time and network resources.
  5. Restoring Archived Ledger Entries or Contract Code: To prepare and restore archived data before actual transaction submission.
  6. Simulating Contract Getter Calls: To retrieve certain data from the contract without affecting the ledger state. (It's worth noting you could also retrieve certain contract data direct from the ledgerkeys without simulation if it doesn't require any manipulation within the contract logic.)
  7. Resource Calculation: To calculate the necessary resources (CPU instructions, memory, etc.) that a transaction will consume.

How to Call simulateTransaction

Using Fetch

Here's an example of how to call the simulateTransaction endpoint directly using fetch in JavaScript:

async function simulateTransaction(transactionXDR) {
const requestBody = {
jsonrpc: "2.0",
id: 8675309,
method: "simulateTransaction",
params: {
transaction: transactionXDR,
resourceConfig: {
instructionLeeway: 50000,
},
},
};

const response = await fetch("https://soroban-testnet.stellar.org:443", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
});

const result = await response.json();
console.log(JSON.parse(result));
}

// Example XDR transaction envelope
// Replace the following placeholder with your actual XDR transaction envelope
const transactionXDR =
"AAAAAgAAAAAg4dbAxsGAGICfBG3iT2cKGYQ6hK4sJWzZ6or1C5v6GAAAAGQAJsOiAAAAEQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAABzAP+dP0PsNzYvFF1pv7a8RQXwH5eg3uZBbbWjE9PwAsAAAAJaW5jcmVtZW50AAAAAAAAAgAAABIAAAAAAAAAACDh1sDGwYAYgJ8EbeJPZwoZhDqEriwlbNnqivULm/oYAAAAAwAAAAMAAAAAAAAAAAAAAAA=";
// An example of where to get the XDR is from TransactionBuilder class from the sdk as shown in the next example.
simulateTransaction(transactionXDR);

Using the JavaScript SDK

The Stellar SDK provides a convenient method to simulate a transaction:

import {
Keypair,
SorobanRpc,
scValToNative,
TransactionBuilder,
BASE_FEE,
Networks,
Operation,
} from "@stellar/stellar-sdk";

const FRIENDBOT_URL = "https://friendbot-testnet.stellar.org/";

const rpc_url = "https://soroban-testnet.stellar.org:443";

// Generate a new keypair for transaction authorization.
const keypair = Keypair.random();
const secret = keypair.secret();
const publicKey = keypair.publicKey();
console.log("publicKey:", publicKey);
// you need to fund the account.
await fetch(`https://friendbot-testnet.stellar.org/?addr=${publicKey}`).then(
(res) => {
console.log(`funded account: ${publicKey}`);
},
);

// Initialize the rpcServer
const RpcServer = new SorobanRpc.Server(rpc_url, { allowHttp: true });

// Load the account (getting the sequence number for the account and making an account object.)
const account = await RpcServer.getAccount(publicKey);

// Define the transaction
const transaction = new TransactionBuilder(account, {
fee: BASE_FEE,
})
.setNetworkPassphrase(Networks.TESTNET)
.setTimeout(30)
.addOperation(
Operation.invokeContractFunction({
function: "symbol",
// the contract function and address need to be set by you.
contract: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
args: [],
}),
)
.build();
// If you want to get this as an XDR string directly, you would use `transaction.toXDR('base64')`

RpcServer.simulateTransaction(transaction).then((sim) => {
console.log("cost:", sim.cost);
console.log("result:", sim.result);
// the result is a ScVal and so we can parse that to human readable output using the sdk's `scValToNative` function:
console.log("humanReadable Result:", scValToNative(sim.result?.retval));
console.log("error:", sim.error);
console.log("latestLedger:", sim.latestLedger);
});

Running the example

To run the above code, you will need to install the Stellar SDK into your project. You can do this by running the following command in your project directory:

npm install @stellar/stellar-sdk

Once your project is set up, you can create a new mjs file and paste the code above. You can then run the file using Node.js by running:

node <filename>.mjs

Understanding the Footprint

A footprint is a set of ledger keys that the transaction will read or write. These keys are marked as either read-only or read-write:

  • Read-Only Keys: Available for reading only.
  • Read-Write Keys: Available for reading and writing.

The footprint ensures that a transaction is aware of all the ledger entries it will interact with, preventing unexpected errors during execution.

Assembling a Transaction

Once you have simulated the transaction and obtained the necessary data, you can assemble the transaction for actual submission. The assembleTransaction function in the SDK helps with this process, but you can also call prepareTransaction to have it both simulate and assemble the transaction for you in one step. Using the javascript SDK we can call assembleTransaction to easily assemble a transaction.

Handling Archived Ledger Entries

When a ledger entry is archived, it needs to be restored before the transaction can be submitted. This is indicated in the restorePreamble field of the result.

interface RestorePreamble {
minResourceFee: string; // Absolute minimum resource fee to add when submitting the RestoreFootprint operation.
transactionData: string; // The recommended Soroban Transaction Data to use when submitting the RestoreFootprint operation.
}

Here is an example for handling restoration using the restorePreamble to restore archived data:

// Make sure to add the necessary imports:
import { Contract } from "@stellar/stellar-sdk/contract";
import {
Account,
Keypair,
Operation,
SorobanDataBuilder,
SorobanRpc,
TimeoutInfinite,
Transaction,
TransactionBuilder,
scValToNative,
xdr,
} from "@stellar/stellar-sdk";
/**
* Simulates a restoration transaction to determine if restoration is needed.
* This function first checks the ledger entry for the given WASM hash. If the entry is found and has expired,
* it attempts a restoration. If the entry hasn't expired yet but the TTL needs extension, it proceeds with TTL extension.
* @param contract - The address of the contract to check
* @param txParams - The transaction parameters including account and signer.
* @returns A promise that resolves to a simulation response, indicating whether restoration or TTL extension is needed.
*/
export async function simulateRestorationIfNeeded(
contract: ContractAddress,
txParams: TxParams,
): Promise<
SorobanRpc.Api.SimulateTransactionRestoreResponse | string | undefined
> {
try {
const RpcServer = new SorobanRpc.Server(
"https://soroban-testnet.stellar.org",
{ allowHttp: true },
);
const account = await RpcServer.getAccount(txParams.account.accountId());
const contract = new Contract(contract);
const ledgerKey = contract.getFootprint();
const response = await RpcServer.getLedgerEntries(ledgerKey);
// Here we parse the response to make sure we got a response and that the liveUntilLedgerSeq parameter is there to make sure it's the response we want before continuing.
if (
response.entries &&
response.entries.length > 0 &&
response.entries[0].liveUntilLedgerSeq
) {
const expirationLedger = response.entries[0].liveUntilLedgerSeq;
const desiredLedgerSeq = response.latestLedger + 500000;
// Be very aware of how many ledgers you want to extend it by. It could quickly become extremely pricey in fees.
let extendLedgers = desiredLedgerSeq - expirationLedger;
if (extendLedgers < 10000) {
extendLedgers = 10000;
}
console.log("Expiration:", expirationLedger);
console.log("Desired TTL:", desiredLedgerSeq);
const sorobanData = new SorobanDataBuilder()
.setReadWrite([ledgerKey])
.build();
const restoreTx = new TransactionBuilder(
account,
txParams.txBuilderOptions,
)
.setSorobanData(sorobanData)
.addOperation(Operation.restoreFootprint({})) // The actual restore operation
.build();
// Simulate a transaction with a restoration operation to check if it's necessary

const restorationSimulation: SorobanRpc.Api.SimulateTransactionResponse =
await RpcServer.simulateTransaction(restoreTx);

//check if restore is necessary. this code also checks if the simulation was successful.
const restoreNeeded = SorobanRpc.Api.isSimulationRestore(
restorationSimulation,
);
console.log(`restoration needed: ${restoreNeeded}`);
// Check if the simulation indicates a need for restoration
if (restoreNeeded) {
return restorationSimulation as SorobanRpc.Api.SimulateTransactionRestoreResponse;
} else {
console.log("No restoration needed., bumping the ttl.");
const account1 = await RpcServer.getAccount(
txParams.account.accountId(),
);

const bumpTTLtx = new TransactionBuilder(
account1,
txParams.txBuilderOptions,
)
.setSorobanData(
new SorobanDataBuilder().setReadWrite([ledgerKey]).build(),
)
.addOperation(
Operation.extendFootprintTtl({
extendTo: desiredLedgerSeq,
}),
) // The actual TTL extension operation
.build();
const ttlSimResponse: SorobanRpc.Api.SimulateTransactionResponse =
await RpcServer.simulateTransaction(bumpTTLtx);
const assembledTx = SorobanRpc.assembleTransaction(
bumpTTLtx,
ttlSimResponse,
).build();
const signedTx = new Transaction(
await txParams.signerFunction(assembledTx.toXDR()),
Networks.TESTNET,
);
// submit the assembled and signed transaction to bump it.
try {
const response = await sendTransaction(signedTx, (result) => {
console.log(`bump ttl for contract result: ${result}`);
return result;
});
return response;
} catch (error) {
console.error("Transaction submission failed with error:", error);
throw error;
}
}
} else {
console.log("No ledger entry found for the given WASM hash.");
}
} catch (error) {
console.error("Failed to simulate restoration:", error);
throw error;
}
}

/**
* Handles the restoration of a Soroban contract.
* @param {SorobanRpc.Api.SimulateTransactionRestoreResponse} simResponse - The simulation response containing restoration information.
* @param {TxParams} txParams - The transaction parameters.
* @returns {Promise<void>} A promise that resolves when the restoration transaction has been submitted.
*/
export async function handleRestoration(
simResponse: SorobanRpc.Api.SimulateTransactionRestoreResponse,
txParams: TxParams,
): Promise<void> {
const RpcServer = new SorobanRpc.Server(
"https://soroban-testnet.stellar.org",
{ allowHttp: true },
);
const restorePreamble = simResponse.restorePreamble;
console.log("Restoring for account:", txParams.account.accountId());
const account = await RpcServer.getAccount(txParams.account.accountId());
// Construct the transaction builder with the necessary parameters
const restoreTx = new TransactionBuilder(account, {
...txParams.txBuilderOptions,
fee: restorePreamble.minResourceFee, // Update fee based on the restoration requirement
})
.setSorobanData(restorePreamble.transactionData.build()) // Set Soroban Data from the simulation
.addOperation(Operation.restoreFootprint({})) // Add the RestoreFootprint operation
.build(); // Build the transaction

const simulation: SorobanRpc.Api.SimulateTransactionResponse =
await RpcServer.simulateTransaction(restoreTx);
const assembledTx = SorobanRpc.assembleTransaction(
restoreTx,
simulation,
).build();

const signedTx = new Transaction(
await txParams.signerFunction(assembledTx.toXDR()),
Networks.TESTNET,
);

console.log("Submitting restoration transaction");

try {
// Submit the transaction to the network
const response = await RpcServer.sendTransaction(signedTx);
console.log(
"Restoration transaction submitted successfully:",
response.hash,
);
} catch (error) {
console.error("Failed to submit restoration transaction:", error);
throw new Error("Restoration transaction failed");
}
}

Fees and Resource Usage

Soroban smart contracts on Stellar use a multidimensional resource fee model, charging fees for several resource types:

  1. CPU Instructions: Number of CPU instructions the transaction uses.
  2. Ledger Entry Accesses: Reading or writing any single ledger entry.
  3. Ledger I/O: Number of bytes read from or written to the ledger.
  4. Transaction Size: Size of the transaction submitted to the network in bytes.
  5. Events & Return Value Size: Size of the events produced by the contract and the return value of the top-level contract function.
  6. Ledger Space Rent: Payment for ledger entry TTL extensions and rent payments for increasing ledger entry size.

Fees are calculated based on the resource consumption declared in the transaction. Refundable fees are charged before execution and refunded based on actual usage, while non-refundable fees are calculated from CPU instructions, read bytes, write bytes, and transaction size. Check out this document for a more in depth understanding of fees and metering.

Preflight Error Handling

The preflight mechanism provides an estimation of CPU and memory consumption in a transaction. It helps identify potential errors and resource limitations before actual submission. Errors returned by the host are propagated through RPC and do not cover network errors or errors in RPC itself.

Common Preflight Errors, and more information can be found here.

Backend Code and Workflow

The simulateTransaction endpoint leverages various backend components to simulate the execution of a transaction. Here is a brief explanation of how it works:

  1. Invocation of Simulation:

    • The simulation is initiated by calling simulate_invoke_host_function_op which takes in parameters such as the transaction to be simulated, resource configuration, and other necessary details.
  2. Snapshot Source and Network Configuration:

    • The simulation utilizes a snapshot source (MockSnapshotSource) and network configuration (NetworkConfig) to mimic the state of the ledger and network conditions.
  3. Resource Calculation:

    • The function simulate_invoke_host_function_op_resources computes the resources (CPU instructions, memory bytes) required for the transaction by analyzing ledger changes.
  4. Execution and Result Handling:

    • The core of the execution is handled by invoke_host_function_in_recording_mode, which records the transaction's impact on the ledger.
    • The results are then processed, including any required authorizations, emitted events, and transaction data.
  5. Adjustments and Fees:

    • Adjustments to resource usage and fees are made based on predefined configurations (SimulationAdjustmentConfig), ensuring accurate fee estimation.

These functions are defined in the rs-soroban-env and also in a soroban-simulation crate and handle the core logic for simulating transactions.

Further Reading

For more information and examples, check out the code and other documentation: