Live: simutrace.vercel.app
A browser-based tool that shows exactly how a Soroban smart contract call will change storage, before you submit a real transaction.
SimuTrace exists for one job: take a specific function call on a Soroban contract, simulate it, and show a clear before/after diff of the storage keys that call actually touches.
It does not:
- Provide a general contract browser or spec viewer (see Stellar Lab's Contract Explorer for that)
- Submit real transactions or request wallet signatures. All calls are read-only simulations.
- Support Stellar Asset Contracts (SACs) yet. SACs have no deployed WASM, so the current spec-fetching approach doesn't work for them. Custom Soroban contracts are supported.
Requires Node 22+.
git clone https://github.com/Hollujay/simutrace.git
cd simutrace
npm install
npm run devOpen the local URL, paste a deployed custom Soroban contract address on testnet, pick a function, fill in its arguments, and simulate. If the call writes to storage, you'll see each affected key with its value before and after.
Not yet deployed anywhere public; run it locally for now.
SimuTrace also ships a command-line entry point that runs the exact same simulation and diff logic as the browser app, useful for scripting or CI assertions.
npm run cli -- check --contract <id> --function <name> --network <testnet|mainnet> --args <key=value,...> [--json]--contractis the contract address to call.--functionis the function to simulate.--networkistestnetormainnet. Testnet uses SimuTrace's built-in RPC endpoint. Mainnet has no default endpoint (there is no single official public one), so you must also pass--rpc-url <url>pointing at your own provider.--argsis a comma-separated list ofname=valuepairs, matching the function's parameter names. Values are parsed the same way the web app's call builder parses them (numbers,true/false,G.../C...addresses, and so on).--jsonswitches the output from human-readable text to the machine-readable schema documented below.
Example (illustrative output, shaped like the fixture the test suite uses):
$ npm run cli -- check --contract CCJZ5DGASBWQXR5MPFCJXMBI333XE5U3FSJTNQU7RIKE3P5GN2K2WYD5 --function increment --network testnet --args amount=5
Contract: CCJZ5DGASBWQXR5MPFCJXMBI333XE5U3FSJTNQU7RIKE3P5GN2K2WYD5
Function: increment
Network: testnet
Cost: 100
Return value: 5
Ledger: 12345
Storage diff (1 changed of 1 total):
[changed] "counter"
before: 0
after: 5
0: the simulation ran successfully. This is returned regardless of whether the diff is empty or non-empty, an empty diff from a genuine simulation is a valid result.1: the simulation did not genuinely complete. This covers an unreachable RPC endpoint, an invalid contract or function, a simulation error reported by the RPC, invalid arguments, and invalid CLI usage. SimuTrace never prints an empty or misleading diff in place of a real failure; a non-zero exit always means the command has something specific to report.
--json prints one JSON object to stdout. On success:
restoreRequiredistruewhen the contract data has expired and needs to be restored before it can be simulated;diffis empty in that case because no simulation of the actual call could run yet, not because nothing changed.- Each
diffentry'sstatusis one of"added","changed","removed","unchanged".
On failure:
{
"ok": false,
"contract": "C...",
"function": "increment",
"network": "testnet",
"error": {
"kind": "contract-not-found",
"message": "No contract found with ID C...",
"details": { "contractId": "C..." }
}
}- A failure object never has a
difffield.error.kindmatches one of the error kinds the web app also reports (contract-not-found,sac-not-supported,no-embedded-spec,malformed-spec,simulation-failed,rpc-unreachable,rpc-error,invalid-argument).error.detailscarries the rest of that error's fields for scripting.
ContractInput -> contractSpec.ts -> FunctionList -> CallBuilder
|
v
simulateCall (simulateCall.ts)
|
+-------------+-------------+
v v
storageSnapshot.ts SimulationResult
(before, via footprint)
|
v
diff.ts -> StorageDiff
The simulation's footprint tells us which storage keys a call would touch. We read those keys' current values before simulating, then diff them against the values the simulation returns. This is why only the keys a specific call touches can be diffed, not a contract's full storage.
See CONTRIBUTING.md for setup and code style. Security issues should be reported privately, see SECURITY.md.
| Name | GitHub |
|---|---|
| Hollujay | @Hollujay |
{ "ok": true, "contract": "C...", "function": "increment", "network": "testnet", "restoreRequired": false, "returnValue": 5, "minResourceFee": "100", "latestLedger": 12345, "diff": [ { "key": "\"counter\"", "status": "changed", "before": 0, "after": 5 } ] }