Matrix logo

Simulate

The simulator performs eth_call dry runs without broadcasting. It is a first-class verb in the engine, reflecting the simulation-first design principle: agents dry-run before th...

Source file: internal/simulate/simulate.go

The simulator performs eth_call dry runs without broadcasting. It is a first-class verb in the engine, reflecting the simulation-first design principle: agents dry-run before they commit.


Design decisions

No wallet required

Simulation is a read-only operation. No signer is needed. This means the simulator works even when the wallet is unconfigured, making it safe for exploration and debugging.

Timeout bounded

Every simulation runs with a 30-second context timeout. This prevents runaway calls on slow or unresponsive RPCs. The timeout is hardcoded; a future enhancement could make it configurable per-chain.

Revert capture

If eth_call reverts, the error is captured in SimulateResponse.Revert and the envelope is ok: false with code: SIMULATE_FAILED. The gas estimate is still returned (from a separate eth_estimateGas call) so the agent knows what the call would have cost.

Optional debug trace

When SimulateRequest.Trace is true, the simulator runs debug_traceCall after the eth_call. The trace is returned as raw JSON (any) in SimulateResponse.Trace. If tracing fails (e.g., RPC doesn't support it), the error is silently ignored — the primary eth_call result is still returned.

Chain resolution

The simulator uses the same chain resolution as deploy/call: chain_id → registry lookup, rpc_url → inline RPC, or active chain from registry. This ensures consistency across all chain-facing verbs.


Request/response types

type SimulateRequest struct {
    ChainID string `json:"chain_id,omitempty"`
    RPCURL  string `json:"rpc_url,omitempty"`
    From    string `json:"from,omitempty"`
    To      string `json:"to"`
    Data    string `json:"data,omitempty"`
    Value   string `json:"value,omitempty"`
    Block   string `json:"block,omitempty"`
    Trace   bool   `json:"trace,omitempty"`
}

type SimulateResponse struct {
    Result      string `json:"result,omitempty"`
    GasEstimate uint64 `json:"gas_estimate,omitempty"`
    Revert      string `json:"revert,omitempty"`
    Trace       any    `json:"trace,omitempty"`
}

Simulation flow

SimulateRequest
    │
    ▼
Resolve chain profile (chain_id / rpc_url / active)
    │
    ▼
Dial RPC client
    │
    ▼
eth_call (30s timeout)
    │
    ├── Success → result hex
    │
    └── Revert → capture reason, return error envelope
    │
    ▼
eth_estimateGas (for gas estimate, even on revert)
    │
    ▼
Optional debug_traceCall (if Trace=true)
    │
    ▼
Return SimulateResponse

Error codes

CodeRetryMeaning
SIMULATE_FAILEDnoeth_call reverted or RPC error
CHAIN_NOT_FOUNDnoUnknown chain_id
CHAIN_RPC_FAILEDyesRPC dial or transport error
INVALID_REQUESTnoMissing to address

Modifying the simulator

What to changeWhere
Add simulation state overrideinternal/simulate/simulate.go — pass state override to eth_call
Make timeout configurableinternal/simulate/simulate.go — add field to SimulateRequest
Add trace formattinginternal/simulate/simulate.go — parse trace into structured format
Add block overrideinternal/simulate/simulate.go — pass block number to CallMessage