# `Onchain.RPC`
[🔗](https://github.com/ZenHive/onchain-stack/blob/onchain-v0.16.0/packages/onchain/lib/onchain/rpc.ex#L1)

Shared JSON-RPC transport and typed Ethereum client.

Single calls and batches use `config :cartouche, :ethereum_node` for the URL,
`config :cartouche, Onchain.RPC, [...]` for transport defaults, and
`config :cartouche, :req_options, [...]` for global Req options. Per-call
`:rpc_url` / `:ethereum_node` and `:req_options` override these defaults.
A missing URL returns `{:error, {:missing_option, :ethereum_node}}`.
The former `:onchain` batch transport configuration is no longer used.

Both paths emit `[:onchain, :rpc, :request]` telemetry spans. Refusals now
uniformly return `:method_not_found`, `:namespace_unavailable`, or `:unavailable`
tags; other single-call error shapes remain unchanged. See `Onchain.RPC`'s
node-capability refusal documentation for the live Alchemy provenance and
message-sensitive meaning of -32600 and -32001.

### Node-capability refusals

A weaker node than this repo's archive endpoint routinely refuses a call for
one of three reasons: the method is not implemented, the provider has disabled
that namespace on the current plan, or the node cannot complete the request
(historical state pruned, method the gateway does not route, transient
overload). Classification runs in `Onchain.RPC` on the shared single-call and
batch paths (both item-level and top-level batch errors),
so a codegen'd wrapper, a hand-written wrapper, `call/3` and a batched call
apply the same rules to the same wire response.

The classifier is uniform; the provider's wire response is not. Verified live
on Alchemy mainnet 2026-08-25: historical `eth_feeHistory` at block 20_000_000
answers `-32001 "Unable to complete request at this time."` as a single call
(classified `{:unavailable, map}`), but the byte-identical request inside a
JSON-RPC array batch answers `-32000 "Internal error"` — alone or alongside a
healthy call — which stays `{:rpc_error, map}`. That is not a gap in the
classifier: `-32000 "Internal error"` is indistinguishable from a genuine
internal failure, and tagging it would invent a distinction the node does not
make. Batching can therefore downgrade a classifiable refusal to an
unclassifiable one; when a caller needs the capability signal, issue that
probe as a single call. Unrecognized codes keep `{:error, {:rpc_error, map}}`
unchanged — the classifier names distinguishable cases, it does not guess.

Each classified map retains the observed `:code` and `:message` (and `:data`
when the node sent it). Branch on the tag; inspect the map when you need the
wire detail.

- `{:error, {:method_not_found, map}}` — this node does not implement the
  method. The reliable standard signal is JSON-RPC `-32601` ("Method not
  found"). Hosted providers often misuse `-32600` ("Invalid Request") instead:
  Alchemy mainnet answers `-32600` `"Unsupported method: <method> on ETH_MAINNET"`
  and `-32600` `"eth_baseFee is not available on the ETH_MAINNET..."`. Those
  two message shapes are pinned from live responses; a bare `-32600` without
  them still passes through as `{:rpc_error, map}`, because other nodes use
  that code for genuinely malformed requests. Callers should pick a portable
  construction (see `Onchain.RPC.base_fee/1`) or a different method.

- `{:error, {:namespace_unavailable, map}}` — the method exists but this
  provider plan has disabled the namespace. Observed on Alchemy mainnet as
  `-32600` `"<method> is not available on the Free tier - upgrade to Pay As
  You Go, or Enterprise for access."` for `trace_*` and `debug_*`. Callers
  should use a plan that serves the namespace, or avoid it.

- `{:error, {:unavailable, map}}` — the node refused to complete a request it
  otherwise accepts. Observed on Alchemy mainnet as HTTP 503 / `-32001`
  `"Unable to complete request at this time."` for `eth_feeHistory` at block
  20_000_000, while `eth_feeHistory` at `"latest"` succeeds on the same URL.
  The identical wire error is also returned for some unimplemented methods
  (`erigon_getHeaderByNumber`), so this is **not** a unique pruned-history
  signal — treat it as "this node cannot serve this request". Callers that
  need historical state should retry against an archive endpoint.

Codes the classifier does not name, including `-32602` ("Invalid params" —
also what reth answers for `eth_getStorageValues` with empty params, which is
indistinguishable from a genuine bad-params error), reach the caller as
`{:error, {:rpc_error, map}}` exactly as before.

## Input adapters and bang functions

`eth_call/3`, `eth_estimate_gas/2`, `eth_get_code/2`, and
`eth_send_raw_transaction/2` accept the former Onchain inputs and delegate
to the typed methods. Hex-returning adapters preserve hex results and wrap
unclassified RPC errors as `{:rpc_error, map}`. The typed methods keep their
native error shapes. Both surfaces preserve classified node refusals.
`get_transaction_receipt/2` returns `Onchain.Receipt`, and
`get_block_by_number/2` returns `Onchain.Block`; logs use `Onchain.Filter.Log`.
Bang variants unwrap `{:ok, value}` and raise `RuntimeError` on `{:error, reason}`.

## Error Format

Hex-input adapters (`eth_call/3`, `eth_estimate_gas/2`, `eth_get_code/2`,
`eth_send_raw_transaction/2`, `get_transaction_receipt/2`, `fee_history/2`,
and `call/3`) return `{:error, {:rpc_error, map}}` for an unclassified RPC
error. A `code: 3` revert map includes `:revert` (raw bytes) and `:data`
(0x hex). When `:errors` names a matching custom-error signature, the map
also includes `:error_abi` and `:error_params`. Classified node refusals
stay `:method_not_found`, `:namespace_unavailable`, or `:unavailable`.
Typed methods such as `get_balance/2`, `get_block_by_number/2`, and
`call_trx/2` return the transport error directly.

Local signing through `Onchain.Signer` is the normal route for submitting
transactions (`eth_sendRawTransaction`). The node-custody methods
(`accounts/1`, `coinbase/1`, `fill_transaction/2`, `sign/3`,
`sign_transaction/2`, `send_transaction/2`) exist for nodes that hold keys.
The distinction is not key custody as such (`Onchain.Signer.CloudKMS`
already signs with a key cartouche does not hold) but which side constructs
the transaction: those methods move envelope construction, nonce and fee
policy, EIP-155 `v` derivation, and low-s normalization into the node.

`sign/3` (`eth_sign`) takes a message, not a pre-computed digest: the node
returns an EIP-191 signature over the data it is handed (`execution-apis`,
`src/eth/sign.yaml`). The method is historically dangerous because early
implementations signed the bytes verbatim, which let a caller pass a
transaction hash and get it signed; most nodes ship it disabled today.

## API Functions
| Function | Arity | Description | Param Kinds |
| --- | --- | --- | --- |
| `chain_id` | 1 | Get the network chain ID. | `opts: value` |
| `block_number` | 1 | Get the current block height. | `opts: value` |
| `blob_base_fee!` | 1 | Get the current base fee per blob gas. Raises on error. | `opts: value` |
| `fee_history!` | 2 | Fetch fee history. Raises on error. | `block_count: value`, `opts: value` |
| `fee_history` | 2 | Fetch base-fee history and per-block priority-fee percentiles (eth_feeHistory). | `block_count: value`, `opts: value` |
| `batch` | 2 | Generic JSON-RPC array batch — invoke many methods in one HTTP request. | `requests: value`, `opts: value` |
| `call!` | 3 | Generic JSON-RPC passthrough. Raises on error. | `method: value`, `params: value`, `opts: value` |
| `call` | 3 | Generic JSON-RPC passthrough — invoke any method not covered by a named wrapper. | `method: value`, `params: value`, `opts: value` |
| `eth_get_code!` | 2 | Fetch contract bytecode at an address. Raises on error. | `address: value`, `opts: value` |
| `eth_get_code` | 2 | Fetch contract bytecode at an address (eth_getCode). | `address: value`, `opts: value` |
| `get_transaction_count!` | 2 | Get the transaction count (nonce) of an address. Raises on error. | `address: value`, `opts: value` |
| `get_transaction_receipt!` | 2 | Get a transaction receipt by hash. Raises on error. | `tx_hash: value`, `opts: value` |
| `get_transaction_receipt` | 2 | Get a transaction receipt by hash (eth_getTransactionReceipt). | `tx_hash: value`, `opts: value` |
| `chain_id!` | 1 | Get the network chain ID. Raises on error. | `opts: value` |
| `get_block_access_list!` | 2 | Fetch an EIP-7928 block access list. Raises on error. | `block: value`, `opts: value` |
| `get_block_access_list` | 2 | Fetch an EIP-7928 block access list (eth_getBlockAccessList). | `block: value`, `opts: value` |
| `get_block_by_number!` | 2 | Fetch a block by number or tag. Raises on error. | `block_id: value`, `opts: value` |
| `block_number!` | 1 | Get the current block height. Raises on error. | `opts: value` |
| `get_balance!` | 2 | Get the ETH balance of an address in wei. Raises on error. | `address: value`, `opts: value` |
| `eth_send_raw_transaction!` | 2 | Broadcast a signed transaction. Raises on error. | `data: value`, `opts: value` |
| `eth_send_raw_transaction` | 2 | Broadcast a signed transaction. | `data: value`, `opts: value` |
| `eth_estimate_gas!` | 2 | Estimate the gas a transaction would consume. Raises on error. | `tx_params: value`, `opts: value` |
| `eth_estimate_gas` | 2 | Estimate the gas a transaction would consume. | `tx_params: value`, `opts: value` |
| `eth_call!` | 3 | Execute a read-only contract call. Raises on error. | `address: value`, `data: value`, `opts: value` |
| `eth_call` | 3 | Execute a read-only contract call (eth_call). | `address: value`, `data: value`, `opts: value` |
| `send_transaction` | 2 | Ask the node to sign and broadcast a transaction with a managed account. | `trx: value`, `opts: value` |
| `sign_transaction` | 2 | Ask the node to sign a transaction with a managed account. | `trx: value`, `opts: value` |
| `sign` | 3 | Ask the node to sign a message under EIP-191 with a managed account (`eth_sign`). | `account: value`, `message: value`, `opts: value` |
| `fill_transaction` | 2 | Ask the node to populate missing nonce, gas, and fee fields without signing. | `trx: value`, `opts: value` |
| `coinbase` | 1 | Return the node's coinbase (fee recipient) address. | `opts: value` |
| `accounts` | 1 | List addresses the node manages. | `opts: value` |
| `get_filter_logs` | 2 | Return every log matching a log filter, not only changes since the last poll. | `filter_id: value`, `opts: value` |
| `eth_get_logs` | 2 | Fetch event logs matching a stateless filter (eth_getLogs). | `filter: value`, `opts: value` |
| `new_pending_transaction_filter` | 1 | Create a node-side filter that records new pending transaction hashes. | `opts: value` |
| `new_block_filter` | 1 | Create a node-side filter that records new block hashes. | `opts: value` |
| `execute_trx` | 3 | Prepare, sign, and submit a transaction to the Ethereum network. | `contract: value`, `call_data: value`, `opts: value` |
| `prepare_trx` | 3 | Prepare and sign a transaction for later submission. | `contract: value`, `call_data: value`, `opts: value` |
| `fee_history` | 2 | Fetch and decode EIP-1559 fee history data. | `opts: value` |
| `max_priority_fee_per_gas` | 1 | Fetch the current max priority fee per gas. | `opts: value` |
| `blob_base_fee` | 1 | Fetch the current base fee per blob gas. | `opts: value` |
| `base_fee` | 1 | Fetch the EIP-1559 base fee per gas for the next block. | `opts: value` |
| `eth_get_proof` | 3 | Fetch an EIP-1186 account and storage proof. | `address: value`, `storage_keys: value`, `opts: value` |
| `eth_get_storage_at` | 3 | Read a 32-byte contract storage word. | `address: value`, `slot: value`, `opts: value` |
| `gas_price` | 1 | Fetch the current legacy gas price. | `opts: value` |
| `debug_trace_call` | 2 | Trace a transaction call speculatively with the debug trace API. | `trx: value`, `opts: value` |
| `trace_call_many` | 2 | Trace multiple transaction calls speculatively with the parity trace API. | `trxs: value`, `opts: value` |
| `trace_call` | 2 | Trace a transaction call speculatively with the parity trace API. | `trx: value`, `opts: value` |
| `trace_trx` | 2 | Fetch parity-style traces for a transaction by transaction hash. | `trx_id: value`, `opts: value` |
| `eth_get_block_receipts` | 2 | Fetch every receipt in a block (eth_getBlockReceipts). | `block: value`, `opts: value` |
| `eth_get_transaction_by_block_number_and_index` | 3 | Fetch one transaction by block number or tag and position (eth_getTransactionByBlockNumberAndIndex). | `block: value`, `transaction_index: value`, `opts: value` |
| `eth_get_transaction_by_block_hash_and_index` | 3 | Fetch one transaction by block hash and position (eth_getTransactionByBlockHashAndIndex). | `block_hash: value`, `transaction_index: value`, `opts: value` |
| `eth_get_transaction_by_hash` | 2 | Fetch one transaction by hash (eth_getTransactionByHash). | `tx_hash: value`, `opts: value` |
| `get_trx_receipt` | 2 | Fetch and decode a transaction receipt by transaction hash. | `trx_id: value`, `opts: value` |
| `get_block_by_hash` | 2 | Fetch a block by its 32-byte block hash. | `block_hash: value`, `opts: value` |
| `get_block_by_number` | 2 | Fetch a block by block number or block tag. | `block_number: value`, `opts: value` |
| `web3_client_version` | 1 | Fetch the node client version string. | `opts: value` |
| `net_peer_count` | 1 | Fetch the number of peers currently connected to the node. | `opts: value` |
| `net_listening` | 1 | Ask whether the node is listening for network connections. | `opts: value` |
| `eth_get_block_transaction_count_by_number` | 2 | Count transactions in the block with the given number or tag. | `block: value`, `opts: value` |
| `eth_get_block_transaction_count_by_hash` | 2 | Count transactions in the block with the given hash. | `block_hash: value`, `opts: value` |
| `eth_syncing` | 1 | Fetch the node's sync status. | `opts: value` |
| `eth_block_number` | 1 | Fetch the current Ethereum block number. | `opts: value` |
| `get_transaction_count` | 2 | Fetch an account transaction count at a block selector. | `address: value`, `opts: value` |
| `get_balance` | 2 | Fetch an account ETH balance at a block selector. | `address: value`, `opts: value` |
| `get_code` | 2 | Fetch contract bytecode at an address and block selector. | `address: value`, `opts: value` |
| `eth_capabilities` | 1 | Fetch the node's effective historical-data capabilities. | `opts: value` |
| `eth_config` | 1 | Fetch the node's EIP-7910 chain and fork configuration. | `opts: value` |
| `eth_chain_id` | 1 | Fetch the current Ethereum chain id. | `opts: value` |
| `estimate_gas` | 2 | Estimate gas for a transaction or call object. | `trx: value`, `opts: value` |
| `create_access_list` | 2 | Generate an EIP-2930 access list for a transaction or call object. | `trx: value`, `opts: value` |
| `call_trx` | 2 | Run `eth_call` against a transaction or call object without submitting it. | `trx: value`, `opts: value` |
| `send_trx` | 2 | Submit a signed Ethereum transaction to the network. | `trx: exchange_data`, `opts: value` |
| `get_nonce` | 2 | Fetch an account nonce at a block selector. | `account: value`, `opts: value` |
| `send_batch` | 2 | Send several Ethereum JSON-RPC requests as one array batch and return raw results in request order. | `requests: value`, `opts: value` |
| `send_rpc` | 3 | Send one Ethereum JSON-RPC request and optionally decode the result. | `method: value`, `params: value`, `opts: value` |

# `access_list_result`

```elixir
@type access_list_result() :: %{
  :access_list =&gt; Onchain.Transaction.V_2930.access_list(),
  :gas_used =&gt; non_neg_integer(),
  optional(:error) =&gt; String.t()
}
```

Decoded `eth_createAccessList` result, retaining an optional execution error.

# `invalid_params_error`

```elixir
@type invalid_params_error() :: {:invalid_params, Exception.t()}
```

Error returned when JSON encoding rejects the outbound request body.

# `rpc_error`

```elixir
@type rpc_error() :: %{
  :code =&gt; integer(),
  :message =&gt; String.t(),
  optional(:revert) =&gt; binary(),
  optional(:error_abi) =&gt; String.t(),
  optional(:error_params) =&gt; [term()],
  optional(:trace) =&gt; term()
}
```

Structured JSON-RPC error envelope returned by an Ethereum node or by local response validation.

# `send_rpc_error`

```elixir
@type send_rpc_error() ::
  rpc_error()
  | invalid_params_error()
  | Req.Response.t()
  | String.t()
  | {atom(), term()}
```

All values that can appear inside an `{:error, reason}` tuple returned by `send_rpc/3`.

# `accounts`

```elixir
@spec accounts(Keyword.t()) :: {:ok, [&lt;&lt;_::160&gt;&gt;]} | {:error, term()}
```

RPC call to list accounts the node holds keys for.

## Examples

    iex> Onchain.RPC.accounts()
    {:ok, [<<64, 125, 115, 216, 164, 158, 235, 133, 211, 44, 244, 101, 80, 125, 215, 29, 80, 113, 0, 193>>]}

# `base_fee`

```elixir
@spec base_fee(keyword()) :: {:ok, non_neg_integer()} | {:error, term()}
```

Returns the next block's base fee in wei via `eth_feeHistory(1, "latest", [])`.

The final `baseFeePerGas` entry is the next block's fee, as specified in
execution-apis v1.0.0-beta.7. This avoids hosted endpoints' differing handling
of the `pending` block tag. Uses the shared fee-history decoder and transport;
node errors propagate unchanged. An incomplete fee window returns
`{:error, :invalid_base_fee_history}`.

`eth_baseFee` landed on execution-apis `main` on 2026-06-15, but is in no
tagged release (latest verified: v1.0.0-beta.7). Alchemy and Infura mainnet
refused it in live probes on 2026-10-01; see `docs/base-fee-portability.md`.

# `batch`

```elixir
@spec batch([{String.t(), [term()]}], keyword()) :: {:ok, [term()]} | {:error, term()}
```

Invoke many raw JSON-RPC calls in one HTTP request.

Each request is a `{method, params}` tuple. Results are returned in request
order even when the node returns the JSON-RPC response array out of order.

# `blob_base_fee`

```elixir
@spec blob_base_fee(Keyword.t()) :: {:ok, non_neg_integer()} | {:error, term()}
```

RPC call to get the current base fee per blob gas.

## Examples

    iex> Onchain.RPC.blob_base_fee()
    {:ok, 42}

# `blob_base_fee!`

```elixir
@spec blob_base_fee!(keyword()) :: non_neg_integer()
```

Get the current base fee per blob gas. Raises on error.

## Parameters

  * `opts` - Options: :rpc_url, :timeout (default: `[]`, value)

## Returns

Base fee per blob gas in wei (`non_neg_integer`)

```elixir
# descripex:contract
%{
  params: %{
    opts: %{
      default: [],
      description: "Options: :rpc_url, :timeout",
      kind: :value
    }
  },
  returns: %{
    type: :non_neg_integer,
    description: "Base fee per blob gas in wei"
  }
}
```

# `block_number`

```elixir
@spec block_number(keyword()) :: {:ok, non_neg_integer()} | {:error, term()}
```

Get the current block height.

## Parameters

  * `opts` - Options: :rpc_url, :timeout (default: `[]`, value)

## Returns

Current block number (`{:ok, non_neg_integer} | {:error, term}`)

```elixir
# descripex:contract
%{
  params: %{
    opts: %{
      default: [],
      description: "Options: :rpc_url, :timeout",
      kind: :value
    }
  },
  returns: %{
    type: "{:ok, non_neg_integer} | {:error, term}",
    description: "Current block number"
  }
}
```

# `block_number!`

```elixir
@spec block_number!(keyword()) :: non_neg_integer()
```

Get the current block height. Raises on error.

## Parameters

  * `opts` - Options: :rpc_url, :timeout (default: `[]`, value)

## Returns

Current block number (`non_neg_integer`)

```elixir
# descripex:contract
%{
  params: %{
    opts: %{
      default: [],
      description: "Options: :rpc_url, :timeout",
      kind: :value
    }
  },
  returns: %{type: :non_neg_integer, description: "Current block number"}
}
```

# `call`

```elixir
@spec call(String.t(), [term()], keyword()) :: {:ok, term()} | {:error, term()}
```

Generic JSON-RPC passthrough — invoke any method not covered by a named wrapper.

## Parameters

  * `method` - JSON-RPC method name, e.g. "eth_getStorageAt", "debug_traceTransaction", "trace_call", "eth_feeHistory" (value)
  * `params` - List of params for the method, in the order the JSON-RPC spec requires (value)
  * `opts` - Options: :rpc_url, :timeout (default: `[]`, value)

## Returns

Raw decoded JSON result (no further decoding — caller knows what they asked for) or wrapped error tuple (`{:ok, term} | {:error, term}`)

```elixir
# descripex:contract
%{
  params: %{
    opts: %{
      default: [],
      description: "Options: :rpc_url, :timeout",
      kind: :value
    },
    params: %{
      description: "List of params for the method, in the order the JSON-RPC spec requires",
      kind: :value
    },
    method: %{
      description: "JSON-RPC method name, e.g. \"eth_getStorageAt\", \"debug_traceTransaction\", \"trace_call\", \"eth_feeHistory\"",
      kind: :value
    }
  },
  returns: %{
    type: "{:ok, term} | {:error, term}",
    description: "Raw decoded JSON result (no further decoding — caller knows what they asked for) or wrapped error tuple"
  }
}
```

# `call!`

```elixir
@spec call!(String.t(), [term()], keyword()) :: term()
```

Generic JSON-RPC passthrough. Raises on error.

## Parameters

  * `method` - JSON-RPC method name (value)
  * `params` - List of params for the method (value)
  * `opts` - Options: :rpc_url, :timeout (default: `[]`, value)

## Returns

Raw decoded JSON result (`term`)

```elixir
# descripex:contract
%{
  params: %{
    opts: %{
      default: [],
      description: "Options: :rpc_url, :timeout",
      kind: :value
    },
    params: %{description: "List of params for the method", kind: :value},
    method: %{description: "JSON-RPC method name", kind: :value}
  },
  returns: %{type: :term, description: "Raw decoded JSON result"}
}
```

# `call_trx`

```elixir
@spec call_trx(
  Onchain.Transaction.V1.t()
  | Onchain.Transaction.V2.t()
  | Onchain.Transaction.Call.t()
  | map(),
  Keyword.t()
) :: {:ok, binary()} | {:error, term()}
```

RPC call to call a transaction and preview results.

## Examples

    iex> Onchain.Transaction.V1.new(1, {100, :gwei}, 100_000, <<1::160>>, {2, :wei}, <<1, 2, 3>>)
    iex> |> Onchain.RPC.call_trx()
    {:ok, <<0x0c>>}

    iex> Onchain.Transaction.V2.new(1, {1, :gwei}, {100, :gwei}, 100_000, <<1::160>>, {2, :wei}, <<1, 2, 3>>, [<<2::160>>, <<3::160>>], :goerli)
    iex> |> Onchain.RPC.call_trx()
    {:ok, <<0x0d>>}

    iex> Onchain.Transaction.V1.new(1, {100, :gwei}, 100_000, <<1::160>>, {2, :wei}, <<1, 2, 3>>)
    iex> |> Onchain.RPC.call_trx(decode: :hex_unsigned)
    {:ok, 0x0c}

    iex> Onchain.Transaction.V1.new(1, {100, :gwei}, 100_000, <<10::160>>, {2, :wei}, <<1, 2, 3>>)
    iex> |> Onchain.RPC.call_trx()
    {:error, %{code: 3, message: "execution reverted", revert: <<61, 115, 139, 46>>}}

    iex> errors = ["Unauthorized()", "BadNonce()", "NotEnoughSigners()", "NotActiveWithdrawalAddress()", "NotActiveOperator()", "DuplicateSigners()"]
    iex> Onchain.Transaction.V1.new(1, {100, :gwei}, 100_000, <<10::160>>, {2, :wei}, <<1, 2, 3>>)
    iex> |> Onchain.RPC.call_trx(errors: errors)
    {:error, %{code: 3, message: "execution reverted", error_abi: "NotActiveOperator()", error_params: [], revert: <<61, 115, 139, 46>>}}

    iex> errors = ["Cool(uint256,string)"]
    iex> Onchain.Transaction.V1.new(1, {100, :gwei}, 100_000, <<11::160>>, {2, :wei}, <<1, 2, 3>>)
    iex> |> Onchain.RPC.call_trx(errors: errors)
    {:error, %{code: 3, message: "execution reverted", error_abi: "Cool(uint256,string)", error_params: [1, "cat"], revert: Onchain.ABI.encode("Cool(uint256,string)", [1, "cat"])}}

    iex> errors = ["Cool(uint256,string)"]
    iex> Onchain.Transaction.V1.new(1, {100, :gwei}, 100_000, <<12::160>>, {2, :wei}, <<1, 2, 3>>)
    iex> |> Onchain.RPC.call_trx(errors: errors)
    {:error, %{code: 3, message: "execution reverted", revert: <<>>}}

    iex> Onchain.Transaction.V1.new(1, {100, :gwei}, 100_000, <<13::160>>, {2, :wei}, <<1, 2, 3>>)
    iex> |> Onchain.RPC.call_trx()
    {:error, %{code: -32602, message: "Failed to decode transaction"}}

# `chain_id`

```elixir
@spec chain_id(keyword()) :: {:ok, non_neg_integer()} | {:error, term()}
```

Get the network chain ID.

## Parameters

  * `opts` - Options: :rpc_url, :timeout (default: `[]`, value)

## Returns

Chain ID (1 = mainnet, 11155111 = sepolia, etc.) (`{:ok, non_neg_integer} | {:error, term}`)

```elixir
# descripex:contract
%{
  params: %{
    opts: %{
      default: [],
      description: "Options: :rpc_url, :timeout",
      kind: :value
    }
  },
  returns: %{
    type: "{:ok, non_neg_integer} | {:error, term}",
    description: "Chain ID (1 = mainnet, 11155111 = sepolia, etc.)"
  }
}
```

# `chain_id!`

```elixir
@spec chain_id!(keyword()) :: non_neg_integer()
```

Get the network chain ID. Raises on error.

## Parameters

  * `opts` - Options: :rpc_url, :timeout (default: `[]`, value)

## Returns

Chain ID integer (`non_neg_integer`)

```elixir
# descripex:contract
%{
  params: %{
    opts: %{
      default: [],
      description: "Options: :rpc_url, :timeout",
      kind: :value
    }
  },
  returns: %{type: :non_neg_integer, description: "Chain ID integer"}
}
```

# `coinbase`

```elixir
@spec coinbase(Keyword.t()) :: {:ok, &lt;&lt;_::160&gt;&gt;} | {:error, term()}
```

RPC call to get the node's coinbase address.

## Examples

    iex> Onchain.RPC.coinbase()
    {:ok, <<254, 59, 85, 126, 143, 182, 43, 137, 244, 145, 107, 114, 27, 229, 92, 235, 130, 141, 189, 115>>}

# `create_access_list`

```elixir
@spec create_access_list(
  Onchain.Transaction.V1.t()
  | Onchain.Transaction.V2.t()
  | Onchain.Transaction.Call.t(),
  Keyword.t()
) :: {:ok, access_list_result()} | {:error, term()}
```

Generates the access list and gas used by a call at a block selector.

A node may return an `:error` string alongside a usable access list when
execution reverts; the field is retained in the successful result map.

## Examples

    iex> call = Onchain.Transaction.Call.new(<<1::160>>, <<0, 1>>)
    iex> {:ok, %{access_list: [{address, [storage_key]}], gas_used: gas_used}} =
    ...>   Onchain.RPC.create_access_list(call)
    iex> {address, storage_key, gas_used}
    {<<1::160>>, <<2::256>>, 26026}

# `debug_trace_call`

```elixir
@spec debug_trace_call(
  Onchain.Transaction.V1.t()
  | Onchain.Transaction.V2.t()
  | Onchain.Transaction.Call.t(),
  Keyword.t()
) :: {:ok, Onchain.DebugTrace.t()} | {:error, term()}
```

RPC to trace a transaction call speculatively via debug API.

## Examples

    iex> Onchain.Transaction.V1.new(1, {100, :gwei}, 100_000, <<1::160>>, {2, :wei}, <<1, 2, 3>>)
    ...> |> Onchain.RPC.debug_trace_call()
    {:ok,
      %Onchain.DebugTrace{
      failed: false,
      gas: 24034,
      return_value: ~h[0x0000000000000000000000000000000000000000000000000858898f93629000],
      struct_logs: [
        %Onchain.DebugTrace.StructLog{
          depth: 1,
          gas: 599978568,
          gas_cost: 3,
          op: :PUSH1,
          pc: 0,
          stack: []
        },
        %Onchain.DebugTrace.StructLog{
          depth: 1,
          gas: 599978565,
          gas_cost: 3,
          op: :PUSH1,
          pc: 2,
          stack: [~h[0x80]]
        },
        %Onchain.DebugTrace.StructLog{
          depth: 1,
          gas: 599978562,
          gas_cost: 12,
          op: :MSTORE,
          pc: 4,
          stack: [~h[0x80], ~h[0x40]]
        }
      ]
    }}

# `estimate_gas`

```elixir
@spec estimate_gas(
  Onchain.Transaction.V1.t()
  | Onchain.Transaction.V2.t()
  | Onchain.Transaction.Call.t()
  | map(),
  Keyword.t()
) :: {:ok, non_neg_integer()} | {:error, term()}
```

RPC call to call to estimate gas used by a given call.

## Examples

    iex> Onchain.Transaction.V1.new(1, {100, :gwei}, 100_000, <<1::160>>, {2, :wei}, <<1, 2, 3>>)
    iex> |> Onchain.RPC.estimate_gas()
    {:ok, 0x0d}

    iex> Onchain.Transaction.V2.new(1, {1, :gwei}, {100, :gwei}, 100_000, <<1::160>>, {2, :wei}, <<1, 2, 3>>, [<<2::160>>, <<3::160>>], :goerli)
    iex> |> Onchain.RPC.estimate_gas()
    {:ok, 0xdd}

    iex> use Onchain.Hex
    iex> Onchain.Transaction.V2.new(1, {1, :gwei}, {100, :gwei}, 100_000, <<10::160>>, {2, :wei}, <<1, 2, 3>>, [<<2::160>>, <<3::160>>], :goerli)
    iex> |> Onchain.RPC.estimate_gas()
    {:error, %{code: 3, message: "execution reverted: Dai/insufficient-balance", revert: ~h[0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000184461692f696e73756666696369656e742d62616c616e63650000000000000000]}}

# `eth_block_number`

```elixir
@spec eth_block_number(Keyword.t()) :: {:ok, non_neg_integer()} | {:error, term()}
```

RPC to get the current block number.

Docs: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_blocknumber

## Examples

    iex> Onchain.RPC.eth_block_number()
    {:ok, 0x44}

# `eth_call`

```elixir
@spec eth_call(String.t() | binary(), String.t(), keyword()) ::
  {:ok, String.t()} | {:error, term()}
```

Execute a read-only contract call (`eth_call`).

## Options

- `:rpc_url` — node URL (overrides `Application.get_env(:cartouche, :ethereum_node)`)
- `:timeout` — request timeout in ms (default 30_000)
- `:block` — block number / tag / 0x hex (default `"latest"`)
- `:errors` — list of Solidity custom-error signatures, e.g.
  `["InsufficientBalance(uint256,uint256)", "Unauthorized()"]`. When the call
  reverts with matching revert data, the error map carries decoded
  `:error_abi` + `:error_params` alongside the raw `:revert` binary and its
  hex mirror `:data`.

## Revert handling

On `code: 3` reverts the inner map widens — see the module's "Error Format"
section. Quick pattern-match shape:

    case Onchain.RPC.eth_call(token, calldata, errors: ["InsufficientBalance(uint256,uint256)"]) do
      {:ok, hex_result} ->
        # Decode hex_result with Onchain.ABI.decode_response/2
        :ok

      {:error, {:rpc_error, %{code: 3, error_abi: "InsufficientBalance(uint256,uint256)", error_params: [requested, available]}}} ->
        {:insufficient, requested, available}

      {:error, {:rpc_error, %{code: 3, data: hex_data}}} ->
        # Custom error not in :errors list (or :errors omitted) — fall back
        # to the hex-mirrored revert payload and decode out-of-band.
        # `Onchain.ABI.decode_hex_error/2` expects 0x hex, which is exactly :data.
        Onchain.ABI.decode_hex_error(hex_data, ["MyError(uint256)"])

      {:error, {:rpc_error, %{message: msg}}} ->
        {:rpc, msg}
    end

# `eth_call!`

```elixir
@spec eth_call!(String.t() | binary(), String.t(), keyword()) :: String.t()
```

Execute a read-only contract call. Raises on error.

## Parameters

  * `address` - Contract address as 0x hex string or 20-byte binary (value)
  * `data` - 0x-prefixed hex-encoded calldata (value)
  * `opts` - Options: :rpc_url, :timeout, :block (default: `[]`, value)

## Returns

Raw 0x-prefixed hex response (`string`)

```elixir
# descripex:contract
%{
  params: %{
    data: %{description: "0x-prefixed hex-encoded calldata", kind: :value},
    opts: %{
      default: [],
      description: "Options: :rpc_url, :timeout, :block",
      kind: :value
    },
    address: %{
      description: "Contract address as 0x hex string or 20-byte binary",
      kind: :value
    }
  },
  returns: %{type: :string, description: "Raw 0x-prefixed hex response"}
}
```

# `eth_capabilities`

```elixir
@spec eth_capabilities(Keyword.t()) ::
  {:ok, Onchain.RPC.Capabilities.t()} | {:error, term()}
```

Returns the node's head and resource-retention capabilities.

## Examples

    iex> {:ok, capabilities} = Onchain.RPC.eth_capabilities()
    iex> {capabilities.head.number, capabilities.blocks.oldest_block}
    {42, 0}

# `eth_chain_id`

```elixir
@spec eth_chain_id(Keyword.t()) :: {:ok, non_neg_integer()} | {:error, term()}
```

RPC to get the current chain id.

Docs: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_chainid

## Examples

    iex> Onchain.RPC.eth_chain_id()
    {:ok, 0x22}

# `eth_config`

```elixir
@spec eth_config(Keyword.t()) ::
  {:ok, Onchain.RPC.Configuration.t()} | {:error, term()}
```

Returns the current, next, and last configured Ethereum forks reported by the node.

## Examples

    iex> {:ok, config} = Onchain.RPC.eth_config()
    iex> {config.current.chain_id, config.current.fork_id}
    {1, <<7, 201, 70, 46>>}

# `eth_estimate_gas`

```elixir
@spec eth_estimate_gas(map(), keyword()) ::
  {:ok, non_neg_integer()} | {:error, term()}
```

Estimate the gas a transaction would consume.

## Parameters

  * `tx_params` - Transaction-params map with atom keys. Recognized: :from, :to, :data, :value, :gas, :gas_price, :max_fee_per_gas, :max_priority_fee_per_gas, :access_list. Absent keys are omitted from the call object. (value)
  * `opts` - Options: :rpc_url, :timeout, :block (default: `[]`, value)

## Returns

Estimated gas units as an integer (`{:ok, non_neg_integer()} | {:error, term()}`)

```elixir
# descripex:contract
%{
  params: %{
    opts: %{
      default: [],
      description: "Options: :rpc_url, :timeout, :block",
      kind: :value
    },
    tx_params: %{
      description: "Transaction-params map with atom keys. Recognized: :from, :to, :data, :value, :gas, :gas_price, :max_fee_per_gas, :max_priority_fee_per_gas, :access_list. Absent keys are omitted from the call object.",
      kind: :value
    }
  },
  returns: %{
    type: "{:ok, non_neg_integer()} | {:error, term()}",
    description: "Estimated gas units as an integer"
  }
}
```

# `eth_estimate_gas!`

```elixir
@spec eth_estimate_gas!(map(), keyword()) :: non_neg_integer()
```

Estimate the gas a transaction would consume. Raises on error.

## Parameters

  * `tx_params` - Transaction-params map with atom keys (see eth_estimate_gas/2) (value)
  * `opts` - Options: :rpc_url, :timeout, :block (default: `[]`, value)

## Returns

Estimated gas units (`integer`)

```elixir
# descripex:contract
%{
  params: %{
    opts: %{
      default: [],
      description: "Options: :rpc_url, :timeout, :block",
      kind: :value
    },
    tx_params: %{
      description: "Transaction-params map with atom keys (see eth_estimate_gas/2)",
      kind: :value
    }
  },
  returns: %{type: :integer, description: "Estimated gas units"}
}
```

# `eth_get_block_receipts`

```elixir
@spec eth_get_block_receipts(integer() | String.t(), Keyword.t()) ::
  {:ok, [Onchain.Receipt.t()] | nil} | {:error, term()}
```

Returns every receipt in the block identified by number, tag, or hash.

Defined by `execution-apis` v1.0.0-beta.7 (`src/eth/block.yaml`,
`eth_getBlockReceipts`). Each receipt is decoded by
`Onchain.Receipt.deserialize/1`, the same decoder as `get_trx_receipt/2`.
A null result is `{:ok, nil}`.

# `eth_get_block_transaction_count_by_hash`

```elixir
@spec eth_get_block_transaction_count_by_hash(String.t(), Keyword.t()) ::
  {:ok, non_neg_integer() | nil} | {:error, term()}
```

Returns how many transactions are in the block identified by `block_hash`.

Defined by `execution-apis` v1.0.0-beta.7 (`src/eth/block.yaml`,
`eth_getBlockTransactionCountByHash`). The result is a hex quantity, or
`null` when the node does not know the block.

## Examples

    iex> Onchain.RPC.eth_get_block_transaction_count_by_hash("0x" <> String.duplicate("cd", 32))
    {:ok, 2}

# `eth_get_block_transaction_count_by_number`

```elixir
@spec eth_get_block_transaction_count_by_number(integer() | String.t(), Keyword.t()) ::
  {:ok, non_neg_integer() | nil} | {:error, term()}
```

Returns how many transactions are in the block identified by number or tag.

Defined by `execution-apis` v1.0.0-beta.7 (`src/eth/block.yaml`,
`eth_getBlockTransactionCountByNumber`). The result is a hex quantity, or
`null` when the node does not know the block.

## Examples

    iex> Onchain.RPC.eth_get_block_transaction_count_by_number("latest")
    {:ok, 2}

# `eth_get_code`

```elixir
@spec eth_get_code(String.t() | binary(), keyword()) ::
  {:ok, String.t()} | {:error, term()}
```

Fetch contract bytecode at an address (eth_getCode).

## Parameters

  * `address` - Account address as 0x hex string or 20-byte binary (value)
  * `opts` - Options: :rpc_url, :timeout, :block (default: `[]`, value)

## Returns

0x-prefixed bytecode hex string, or "0x" for EOA addresses (`{:ok, hex_string} | {:error, term}`)

```elixir
# descripex:contract
%{
  params: %{
    opts: %{
      default: [],
      description: "Options: :rpc_url, :timeout, :block",
      kind: :value
    },
    address: %{
      description: "Account address as 0x hex string or 20-byte binary",
      kind: :value
    }
  },
  returns: %{
    type: "{:ok, hex_string} | {:error, term}",
    description: "0x-prefixed bytecode hex string, or \"0x\" for EOA addresses",
    example: "\"0x\" for EOAs, \"0x6080604052...\" for contracts"
  }
}
```

# `eth_get_code!`

```elixir
@spec eth_get_code!(String.t() | binary(), keyword()) :: String.t()
```

Fetch contract bytecode at an address. Raises on error.

## Parameters

  * `address` - Account address as 0x hex string or 20-byte binary (value)
  * `opts` - Options: :rpc_url, :timeout, :block (default: `[]`, value)

## Returns

0x-prefixed bytecode hex string (`string`)

```elixir
# descripex:contract
%{
  params: %{
    opts: %{
      default: [],
      description: "Options: :rpc_url, :timeout, :block",
      kind: :value
    },
    address: %{
      description: "Account address as 0x hex string or 20-byte binary",
      kind: :value
    }
  },
  returns: %{type: :string, description: "0x-prefixed bytecode hex string"}
}
```

# `eth_get_logs`

```elixir
@spec eth_get_logs(map(), Keyword.t()) ::
  {:ok, [Onchain.Filter.Log.t()]} | {:error, term()}
```

Stateless `eth_getLogs` query.

Accepts the execution-apis filter object: a block range (`fromBlock` /
`toBlock`) or a mutually exclusive `blockHash`, plus optional `address` and
`topics`. Logs are decoded with `Onchain.Filter.Log`, the same decoder
`Onchain.Filter` and `get_filter_logs/2` use. That decoder expects the
mined-log fields (block hash, block number, and transaction hash present).

## Examples

    iex> {:ok, [log]} = Onchain.RPC.eth_get_logs(%{from_block: 16, to_block: 16})
    iex> log.block_number
    16

# `eth_get_proof`

```elixir
@spec eth_get_proof(String.t() | binary(), [String.t()], keyword()) ::
  {:ok, Onchain.RPC.Proof.t()} | {:error, term()}
```

Fetches an EIP-1186 account and storage proof as `Onchain.RPC.Proof`.

Defined by execution-apis v1.0.0-beta.7. Keys are hex strings of 1 to 64
digits, left-padded to 32 bytes on the wire;
`:block` selects a block number or tag (default `"latest"`). Quantities decode
to integers and addresses, hashes and proof nodes to bytes. No local Merkle
verification is performed.

Observed 2026-10-01: Alchemy mainnet served DAI proofs at latest, block
18,000,000 and block 1 (before deployment, with empty storage proof nodes).
The archive node served latest but refused both historical blocks with
`-32602`, `"distance to target block exceeds maximum proof window"`.
Availability is endpoint-specific; node refusals propagate unchanged.
See `docs/state-read-portability.md` for verbatim evidence.

# `eth_get_storage_at`

```elixir
@spec eth_get_storage_at(String.t() | binary(), String.t(), keyword()) ::
  {:ok, &lt;&lt;_::256&gt;&gt;} | {:error, term()}
```

Reads a 32-byte storage word via `eth_getStorageAt` (execution-apis v1.0.0-beta.7).

Accepts a hex slot of up to 32 bytes, including leading zeroes. `:block` selects
a block number or tag (default `"latest"`). Returns raw bytes.

Observed 2026-10-01: Alchemy mainnet and the archive node returned DAI slot 1
at block 18,000,000; Alchemy also returned zero at block 1 (before deployment).
This does not establish an unlimited history guarantee. Verbatim responses
and refusals are recorded in `docs/state-read-portability.md`.

# `eth_get_transaction_by_block_hash_and_index`

```elixir
@spec eth_get_transaction_by_block_hash_and_index(
  String.t(),
  non_neg_integer() | String.t(),
  Keyword.t()
) :: {:ok, Onchain.Transaction.Info.t()} | {:error, term()}
```

Returns the transaction at `transaction_index` in the block identified by `block_hash`.

Defined by `execution-apis` v1.0.0-beta.7 (`src/eth/transaction.yaml`,
`eth_getTransactionByBlockHashAndIndex`). Same envelope as
`eth_get_transaction_by_hash/2`. A null result is `{:error, :not_found}`.

# `eth_get_transaction_by_block_number_and_index`

```elixir
@spec eth_get_transaction_by_block_number_and_index(
  integer() | String.t(),
  non_neg_integer() | String.t(),
  Keyword.t()
) :: {:ok, Onchain.Transaction.Info.t()} | {:error, term()}
```

Returns the transaction at `transaction_index` in the block identified by number or tag.

Defined by `execution-apis` v1.0.0-beta.7 (`src/eth/transaction.yaml`,
`eth_getTransactionByBlockNumberAndIndex`). Same envelope as
`eth_get_transaction_by_hash/2`. A null result is `{:error, :not_found}`.

# `eth_get_transaction_by_hash`

```elixir
@spec eth_get_transaction_by_hash(String.t(), Keyword.t()) ::
  {:ok, Onchain.Transaction.Info.t()} | {:error, term()}
```

Returns the transaction identified by `tx_hash`.

Defined by `execution-apis` v1.0.0-beta.7 (`src/eth/transaction.yaml`,
`eth_getTransactionByHash`). The result is a `Onchain.Transaction.Info`
envelope: inclusion metadata plus the typed transaction from
`Vn.from_json/1`. Block fields are `nil` when the transaction is pending.
`block_timestamp` is also `nil` when the node omits `blockTimestamp`
(Alchemy mainnet sometimes does, observed 2026-10-01).
A null result is `{:error, :not_found}`.

# `eth_send_raw_transaction`

```elixir
@spec eth_send_raw_transaction(String.t(), keyword()) ::
  {:ok, String.t()} | {:error, term()}
```

Broadcast a signed transaction.

## Parameters

  * `data` - 0x-prefixed hex-encoded signed transaction (value)
  * `opts` - Options: :rpc_url, :timeout (default: `[]`, value)

## Returns

Transaction hash as 0x hex string (`{:ok, tx_hash} | {:error, term}`)

```elixir
# descripex:contract
%{
  params: %{
    data: %{
      description: "0x-prefixed hex-encoded signed transaction",
      kind: :value
    },
    opts: %{
      default: [],
      description: "Options: :rpc_url, :timeout",
      kind: :value
    }
  },
  returns: %{
    type: "{:ok, tx_hash} | {:error, term}",
    description: "Transaction hash as 0x hex string",
    example: "0xabc123..."
  }
}
```

# `eth_send_raw_transaction!`

```elixir
@spec eth_send_raw_transaction!(String.t(), keyword()) :: String.t()
```

Broadcast a signed transaction. Raises on error.

## Parameters

  * `data` - 0x-prefixed hex-encoded signed transaction (value)
  * `opts` - Options: :rpc_url, :timeout (default: `[]`, value)

## Returns

Transaction hash as 0x hex string (`string`)

```elixir
# descripex:contract
%{
  params: %{
    data: %{
      description: "0x-prefixed hex-encoded signed transaction",
      kind: :value
    },
    opts: %{
      default: [],
      description: "Options: :rpc_url, :timeout",
      kind: :value
    }
  },
  returns: %{type: :string, description: "Transaction hash as 0x hex string"}
}
```

# `eth_syncing`

```elixir
@spec eth_syncing(Keyword.t()) ::
  {:ok, false | Onchain.RPC.SyncStatus.t()} | {:error, term()}
```

Returns the node's sync status from `eth_syncing`.

`execution-apis` v1.0.0-beta.7 (`src/eth/client.yaml`, schema `SyncingStatus`)
defines one union: `false` when the node is not syncing, or a progress object
whose quantity fields are `startingBlock`, `currentBlock`, and `highestBlock`.
This decodes that union to `false` or `%Onchain.RPC.SyncStatus{}`.

## Examples

    iex> Onchain.RPC.eth_syncing()
    {:ok, false}

# `execute_trx`

```elixir
@spec execute_trx(&lt;&lt;_::160&gt;&gt;, binary() | {String.t(), [term()]}, Keyword.t()) ::
  {:ok, binary()} | {:error, term()}
```

Helper function to work with other Cartouche modules to get a nonce, sign a transction, and transmit it to the network.

If you need higher-level functionality, like manual nonce tracking, you may want to use the more granular function calls.

Options:
  * `gas_price` - Set the base gas for the transaction, overrides all other gas prices listed below (default `nil`) [note: only compatible with V1 transaction]
  * `base_fee` - Set the base price for the transaction, if nil, will use base gas price from `eth_gasPrice` call (default `nil`) [note: only compatible with V2 transactions]
  * `base_fee_buffer` - Buffer for the gas price when estimating gas (default: 1.2 = 120%) [note: only compatible with V2 transactions]
  * `priority_fee` - Additional gas to send as a priority fee. (default: `{0, :gwei}`) [note: only compatible with V2 transactions]
  * `gas_limit` - Set the gas limit for the transaction (default: calls `eth_estimateGas`)
  * `gas_buffer` - Buffer if estimating gas limit (default: 1.5 = 150%)
  * `value` - Value to provide with transaction in wei (default: 0)
  * `nonce` - Nonce to send with transaction. (default: lookup via `eth_transactionCount`)
  * `verify` - Verify the function is likely to succeed (default: true)
  * `trx_type` - :v1 for V1 (pre-EIP-1559 transactions), :v2 for V2 (EIP-1559) transactions, and `nil` for auto-detect.

  Note: if we don't `verify`, then `estimateGas` will likely fail if the transaction were to fail.
        To prevent this, `gas_limit` should always be supplied when `verify` is set to false.

## Examples
    iex> signer_proc = Onchain.Test.Signer.start_signer()
    iex> {:ok, trx_id} = Onchain.RPC.execute_trx(<<1::160>>, {"baz(uint,address)", [50, :binary.decode_unsigned(<<1::160>>)]}, gas_price: {50, :gwei}, value: 0, signer: signer_proc)
    iex> <<nonce::integer-size(8), gas_price::integer-size(64), gas_limit::integer-size(24), to::binary>> = trx_id
    iex> {nonce, gas_price, gas_limit, to}
    {4, 50000000000, 20, <<1::160>>}

    iex> signer_proc = Onchain.Test.Signer.start_signer()
    iex> Onchain.RPC.execute_trx(<<10::160>>, {"baz(uint,address)", [50, <<1::160>> |> :binary.decode_unsigned]}, gas_price: {50, :gwei}, gas_limit: 100_000, value: 0, nonce: 10, signer: signer_proc)
    {:error, %{code: 3, message: "execution reverted", revert: <<61, 115, 139, 46>>}}

    iex> # Set base fee and priority fee (v2)
    iex> signer_proc = Onchain.Test.Signer.start_signer()
    iex> {:ok, trx_id} = Onchain.RPC.execute_trx(<<10::160>>, {"baz(uint,address)", [50, <<1::160>> |> :binary.decode_unsigned]}, base_fee: {1, :gwei}, priority_fee: {3, :gwei}, gas_limit: 100_000, value: 0, nonce: 10, verify: false, signer: signer_proc)
    iex> <<nonce::integer-size(8), max_priority_fee_per_gas::integer-size(64), max_fee_per_gas::integer-size(64), gas_limit::integer-size(24), to::binary>> = trx_id
    iex> {nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, to}
    {10, 3000000000, 4000000000, 100000, <<10::160>>}

# `fee_history`

```elixir
@spec fee_history(Keyword.t() | pos_integer()) ::
  {:ok, Onchain.FeeHistory.t()} | {:error, term()}
```

RPC call to call to get the Eip-1559 fee history data.

## Examples

    iex> Onchain.RPC.fee_history()
    {:ok, %Onchain.FeeHistory{
      base_fee_per_gas: [20566340803, 20460504186, 19629790325, 19239635811, 19090900440, 19048391846],
      gas_used_ratio: [0.4794155666666667, 0.3375966, 0.42049746666666665, 0.4690773, 0.49109343333333333],
      oldest_block: 16607861,
      reward: [[1000000000, 1000000000, 1500000000], [1000000000, 1000000000, 2000000000], [1000000000, 1000000000, 1000000000], [780000000, 1000000000, 2000000000], [1000000000, 1000000000, 1500000000]]
    }}

# `fee_history`

```elixir
@spec fee_history(pos_integer(), keyword()) ::
  {:ok, Onchain.FeeHistory.t()} | {:error, term()}
```

Fetch base-fee history and per-block priority-fee percentiles (eth_feeHistory).

## Parameters

  * `block_count` - Number of recent blocks to query, 1..1024 (EIP-1474 cap) (value)
  * `opts` - Options: :newest_block (default "latest"), :reward_percentiles (default [50] — list of ints 0..100, monotonically non-decreasing), :rpc_url, :timeout (default: `[]`, value)

## Returns

Deserialized fee history struct: oldest_block, base_fee_per_gas (block_count + 1 entries), gas_used_ratio, reward (block_count rows × length(reward_percentiles) cols) (`{:ok, Onchain.FeeHistory.t()} | {:error, term}`)

```elixir
# descripex:contract
%{
  params: %{
    opts: %{
      default: [],
      description: "Options: :newest_block (default \"latest\"), :reward_percentiles (default [50] — list of ints 0..100, monotonically non-decreasing), :rpc_url, :timeout",
      kind: :value
    },
    block_count: %{
      description: "Number of recent blocks to query, 1..1024 (EIP-1474 cap)",
      kind: :value
    }
  },
  returns: %{
    type: "{:ok, Onchain.FeeHistory.t()} | {:error, term}",
    description: "Deserialized fee history struct: oldest_block, base_fee_per_gas (block_count + 1 entries), gas_used_ratio, reward (block_count rows × length(reward_percentiles) cols)"
  }
}
```

# `fee_history!`

```elixir
@spec fee_history!(pos_integer(), keyword()) :: Onchain.FeeHistory.t()
```

Fetch fee history. Raises on error.

## Parameters

  * `block_count` - Number of recent blocks to query, 1..1024 (value)
  * `opts` - Options: :newest_block, :reward_percentiles, :rpc_url, :timeout (default: `[]`, value)

## Returns

Deserialized fee history struct (`Onchain.FeeHistory.t()`)

```elixir
# descripex:contract
%{
  params: %{
    opts: %{
      default: [],
      description: "Options: :newest_block, :reward_percentiles, :rpc_url, :timeout",
      kind: :value
    },
    block_count: %{
      description: "Number of recent blocks to query, 1..1024",
      kind: :value
    }
  },
  returns: %{
    type: "Onchain.FeeHistory.t()",
    description: "Deserialized fee history struct"
  }
}
```

# `fill_transaction`

```elixir
@spec fill_transaction(
  Onchain.Transaction.V1.t()
  | Onchain.Transaction.V2.t()
  | Onchain.Transaction.Call.t(),
  Keyword.t()
) :: {:ok, struct()} | {:error, term()}
```

RPC call to fill missing transaction fields via `eth_fillTransaction`.

The node populates nonce, gas, and fee fields and returns the completed
unsigned transaction. Spec-conforming nodes return a `FillTransactionResult`
carrying only `tx`; geth returns a `{raw, tx}` pair. Both shapes decode into a
`Onchain.Transaction` struct.

Decoding prefers the `tx` object `execution-apis` requires over geth's
additional `raw` field, because `raw` serializes an unsigned transaction in
the signed wire format and so cannot be told apart from a signed one.

A typed envelope's unsigned form is `nil` signature fields; a legacy one keeps
the chain id in `v` with `r`/`s` zero.

Legacy fills need a chain id, which `Onchain.Transaction.V1` holds in `v`
until a signature replaces it. geth omits `chainId` from an unsigned legacy
result; pass `chain_id:` to supply it. Without it — from either side — the
call errors rather than return a transaction that would sign to the wrong
address, and so does a `chain_id:` that disagrees with the one the response
names: `Onchain.Signer` takes the chain id from its caller rather than from
the struct, so a disagreement signs a digest the encoded payload does not
match.

## Examples

    iex> {:ok, trx} =
    ...>   Onchain.Transaction.V1.new(1, {100, :gwei}, 100_000, <<1::160>>, {2, :wei}, <<1, 2, 3>>, :kovan)
    ...>   |> Onchain.RPC.fill_transaction()
    iex> trx.nonce
    1

# `gas_price`

```elixir
@spec gas_price(Keyword.t()) :: {:ok, non_neg_integer()} | {:error, term()}
```

RPC call to call to get the current gas price.

## Examples

    iex> Onchain.RPC.gas_price()
    {:ok, 1000000000}

# `get_balance`

```elixir
@spec get_balance(binary(), Keyword.t()) ::
  {:ok, non_neg_integer()} | {:error, term()}
```

RPC to get an account's eth balance.

Docs: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_getbalance

## Examples

    iex> Onchain.RPC.get_balance(~h[0x0000000000000000000000000000000000000001])
    {:ok, 0x55}

# `get_balance!`

```elixir
@spec get_balance!(String.t() | binary(), keyword()) :: non_neg_integer()
```

Get the ETH balance of an address in wei. Raises on error.

## Parameters

  * `address` - Account address as 0x hex string or 20-byte binary (value)
  * `opts` - Options: :rpc_url, :timeout, :block (default: `[]`, value)

## Returns

Balance in wei (`non_neg_integer`)

```elixir
# descripex:contract
%{
  params: %{
    opts: %{
      default: [],
      description: "Options: :rpc_url, :timeout, :block",
      kind: :value
    },
    address: %{
      description: "Account address as 0x hex string or 20-byte binary",
      kind: :value
    }
  },
  returns: %{type: :non_neg_integer, description: "Balance in wei"}
}
```

# `get_block_access_list`

```elixir
@spec get_block_access_list(integer() | String.t(), keyword()) ::
  {:ok, [map()] | nil} | {:error, term()}
```

Fetch an EIP-7928 block access list (eth_getBlockAccessList).

## Parameters

  * `block` - Block number, tag, or 0x-prefixed 32-byte block hash (value)
  * `opts` - Options: :rpc_url, :timeout (default: `[]`, value)

## Returns

Raw camelCase EIP-7928 account-access entries, or nil when unavailable (`{:ok, [map] | nil} | {:error, term}`)

```elixir
# descripex:contract
%{
  params: %{
    block: %{
      description: "Block number, tag, or 0x-prefixed 32-byte block hash",
      kind: :value
    },
    opts: %{
      default: [],
      description: "Options: :rpc_url, :timeout",
      kind: :value
    }
  },
  returns: %{
    type: "{:ok, [map] | nil} | {:error, term}",
    description: "Raw camelCase EIP-7928 account-access entries, or nil when unavailable"
  }
}
```

# `get_block_access_list!`

```elixir
@spec get_block_access_list!(integer() | String.t(), keyword()) :: [map()] | nil
```

Fetch an EIP-7928 block access list. Raises on error.

## Parameters

  * `block` - Block number, tag, or 0x-prefixed 32-byte block hash (value)
  * `opts` - Options: :rpc_url, :timeout (default: `[]`, value)

## Returns

Raw camelCase account-access entries or nil (`[map] | nil`)

```elixir
# descripex:contract
%{
  params: %{
    block: %{
      description: "Block number, tag, or 0x-prefixed 32-byte block hash",
      kind: :value
    },
    opts: %{
      default: [],
      description: "Options: :rpc_url, :timeout",
      kind: :value
    }
  },
  returns: %{
    type: "[map] | nil",
    description: "Raw camelCase account-access entries or nil"
  }
}
```

# `get_block_by_hash`

```elixir
@spec get_block_by_hash(binary(), Keyword.t()) ::
  {:ok, Onchain.Block.t() | nil} | {:error, term()}
```

RPC to get a block by its block hash.

Docs: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_getblockbyhash

## Examples

    iex> Onchain.RPC.get_block_by_hash(~h[0xdc0818cf78f21a8e70579cb46a43643f78291264dda342ae31049421c82d21ae])
    {:ok, %Onchain.Block{
      difficulty: 0x4ea3f27bc,
      extra_data: ~h[0x476574682f4c5649562f76312e302e302f6c696e75782f676f312e342e32],
      gas_limit: 0x1388,
      gas_used: 0x0,
      hash: ~h[0xdc0818cf78f21a8e70579cb46a43643f78291264dda342ae31049421c82d21ae],
      logs_bloom: ~h[0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000],
      miner: ~h[0xbb7b8287f3f0a933474a79eae42cbca977791171],
      mix_hash: ~h[0x4fffe9ae21f1c9e15207b1f472d5bbdd68c9595d461666602f2be20daf5e7843],
      nonce: 0x689056015818adbe,
      number: 0x1b4,
      parent_hash: ~h[0xe99e022112df268087ea7eafaf4790497fd21dbeeb6bd7a1721df161a6657a54],
      receipts_root: ~h[0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421],
      sha3_uncles: ~h[0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347],
      size: 0x220,
      state_root: ~h[0xddc8b0234c2e0cad087c8b389aa7ef01f7d79b2570bccb77ce48648aa61c904d],
      timestamp: 0x55ba467c,
      total_difficulty: 0x78ed983323d,
      transactions: [],
      transactions_root: ~h[0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421],
      uncles: [],
      base_fee_per_gas: nil,
      withdrawals_root: nil,
      withdrawals: nil,
      parent_beacon_block_root: nil,
      blob_gas_used: nil,
      excess_blob_gas: nil
    }}

## Options

- `:include_transaction_details` — when `true`, the node returns full transaction
  objects in `transactions`; when `false` (default), just hashes. Forwarded to
  `eth_getBlockByHash` as the second wire param (real nodes reject single-param
  calls with `-32602 Invalid params`). Note: `Onchain.Block.deserialize/1`
  decodes full transaction objects when requested and retains hashes otherwise.

Plus any option accepted by `send_rpc/3` (e.g. `:ethereum_node`, `:timeout`, `:req_options`).

# `get_block_by_number`

```elixir
@spec get_block_by_number(non_neg_integer() | String.t(), Keyword.t()) ::
  {:ok, Onchain.Block.t() | nil} | {:error, term()}
```

RPC to get a block by its block number.

Docs: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_getblockbynumber

## Examples

    iex> Onchain.RPC.get_block_by_number(55)
    {:ok, %Onchain.Block{
      difficulty: 0x4ea3f27bc,
      extra_data: ~h[0x476574682f4c5649562f76312e302e302f6c696e75782f676f312e342e32],
      gas_limit: 0x1388,
      gas_used: 0x0,
      hash: ~h[0xdc0818cf78f21a8e70579cb46a43643f78291264dda342ae31049421c82d21ae],
      logs_bloom: ~h[0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000],
      miner: ~h[0xbb7b8287f3f0a933474a79eae42cbca977791171],
      mix_hash: ~h[0x4fffe9ae21f1c9e15207b1f472d5bbdd68c9595d461666602f2be20daf5e7843],
      nonce: 0x689056015818adbe,
      number: 0x1b4,
      parent_hash: ~h[0xe99e022112df268087ea7eafaf4790497fd21dbeeb6bd7a1721df161a6657a54],
      receipts_root: ~h[0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421],
      sha3_uncles: ~h[0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347],
      size: 0x220,
      state_root: ~h[0xddc8b0234c2e0cad087c8b389aa7ef01f7d79b2570bccb77ce48648aa61c904d],
      timestamp: 0x55ba467c,
      total_difficulty: 0x78ed983323d,
      transactions: [],
      transactions_root: ~h[0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421],
      uncles: [],
      base_fee_per_gas: nil,
      withdrawals_root: nil,
      withdrawals: nil,
      parent_beacon_block_root: nil,
      blob_gas_used: nil,
      excess_blob_gas: nil
    }}

## Options

- `:include_transaction_details` — when `true`, the node returns full transaction
  objects in `transactions`; when `false` (default), just hashes. Forwarded to
  `eth_getBlockByNumber` as the second wire param. Note: `Onchain.Block.deserialize/1`
  decodes full transaction objects when requested and retains hashes otherwise.

Plus any option accepted by `send_rpc/3` (e.g. `:ethereum_node`, `:timeout`, `:req_options`).

# `get_block_by_number!`

```elixir
@spec get_block_by_number!(integer() | String.t(), keyword()) ::
  Onchain.Block.t() | nil
```

Fetch a block by number or tag. Raises on error.

## Parameters

  * `block_id` - Block number (integer) or tag string (value)
  * `opts` - Options: :rpc_url, :timeout (default: `[]`, value)

## Returns

Decoded Onchain.Block struct (`map | nil`)

```elixir
# descripex:contract
%{
  params: %{
    opts: %{
      default: [],
      description: "Options: :rpc_url, :timeout",
      kind: :value
    },
    block_id: %{
      description: "Block number (integer) or tag string",
      kind: :value
    }
  },
  returns: %{type: "map | nil", description: "Decoded Onchain.Block struct"}
}
```

# `get_code`

```elixir
@spec get_code(binary(), Keyword.t()) :: {:ok, binary()} | {:error, term()}
```

RPC call to get code for a contract at an address.

## Examples

    iex> Onchain.RPC.get_code(<<1::160>>)
    {:ok, <<0x11, 0x22, 0x33>>}

# `get_filter_logs`

```elixir
@spec get_filter_logs(String.t(), Keyword.t()) ::
  {:ok, [Onchain.Filter.Log.t()]} | {:error, term()}
```

RPC call to fetch the full log backlog for a filter.

## Examples

    iex> {:ok, [log]} = Onchain.RPC.get_filter_logs("0xf11735")
    iex> log.address
    <<181, 165, 242, 38, 148, 53, 44, 21, 176, 3, 35, 132, 74, 213, 69, 171, 178, 177, 16, 40>>

# `get_nonce`

```elixir
@spec get_nonce(&lt;&lt;_::160&gt;&gt;, Keyword.t()) ::
  {:ok, non_neg_integer()} | {:error, term()}
```

RPC call to get account nonce.

## Examples

    iex> use Onchain.Hex
    iex> Onchain.RPC.get_nonce(~h[0x407d73d8a49eeb85d32cf465507dd71d507100c1])
    {:ok, 4}

# `get_transaction_count`

```elixir
@spec get_transaction_count(binary(), Keyword.t()) ::
  {:ok, non_neg_integer()} | {:error, term()}
```

RPC to get an account's transaction count (i.e. nonce)

Docs: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_gettransactioncount

## Examples

    iex> Onchain.RPC.get_transaction_count(~h[0x0000000000000000000000000000000000000001])
    {:ok, 0x4}

# `get_transaction_count!`

```elixir
@spec get_transaction_count!(String.t() | binary(), keyword()) :: non_neg_integer()
```

Get the transaction count (nonce) of an address. Raises on error.

## Parameters

  * `address` - Account address as 0x hex string or 20-byte binary (value)
  * `opts` - Options: :rpc_url, :timeout, :block (default: `[]`, value)

## Returns

Transaction count (nonce) (`non_neg_integer`)

```elixir
# descripex:contract
%{
  params: %{
    opts: %{
      default: [],
      description: "Options: :rpc_url, :timeout, :block",
      kind: :value
    },
    address: %{
      description: "Account address as 0x hex string or 20-byte binary",
      kind: :value
    }
  },
  returns: %{type: :non_neg_integer, description: "Transaction count (nonce)"}
}
```

# `get_transaction_receipt`

```elixir
@spec get_transaction_receipt(String.t(), keyword()) ::
  {:ok, Onchain.Receipt.t() | nil} | {:error, term()}
```

Get a transaction receipt by hash (eth_getTransactionReceipt).

## Parameters

  * `tx_hash` - 0x-prefixed hex transaction hash (value)
  * `opts` - Options: :rpc_url, :timeout (default: `[]`, value)

## Returns

Onchain.Receipt struct, or nil if the transaction is pending/unknown (`{:ok, map | nil} | {:error, term}`)

```elixir
# descripex:contract
%{
  params: %{
    opts: %{
      default: [],
      description: "Options: :rpc_url, :timeout",
      kind: :value
    },
    tx_hash: %{description: "0x-prefixed hex transaction hash", kind: :value}
  },
  returns: %{
    type: "{:ok, map | nil} | {:error, term}",
    description: "Onchain.Receipt struct, or nil if the transaction is pending/unknown"
  }
}
```

# `get_transaction_receipt!`

```elixir
@spec get_transaction_receipt!(String.t(), keyword()) :: Onchain.Receipt.t() | nil
```

Get a transaction receipt by hash. Raises on error.

## Parameters

  * `tx_hash` - 0x-prefixed hex transaction hash (value)
  * `opts` - Options: :rpc_url, :timeout (default: `[]`, value)

## Returns

Onchain.Receipt struct or nil (`map | nil`)

```elixir
# descripex:contract
%{
  params: %{
    opts: %{
      default: [],
      description: "Options: :rpc_url, :timeout",
      kind: :value
    },
    tx_hash: %{description: "0x-prefixed hex transaction hash", kind: :value}
  },
  returns: %{type: "map | nil", description: "Onchain.Receipt struct or nil"}
}
```

# `get_trx_receipt`

```elixir
@spec get_trx_receipt(binary() | String.t(), Keyword.t()) ::
  {:ok, Onchain.Receipt.t() | nil} | {:error, term()}
```

RPC call to get a transaction receipt. Note, this will return {:ok, %Onchain.Receipt{}} or {:ok, nil} if the
receipt is not yet available.

## Examples

    iex> Onchain.RPC.get_trx_receipt(~h[0x85d995eba9763907fdf35cd2034144dd9d53ce32cbec21349d4b12823c6860c5])
    {:ok,
      %Onchain.Receipt{
        transaction_hash: ~h[0x85d995eba9763907fdf35cd2034144dd9d53ce32cbec21349d4b12823c6860c5],
        transaction_index: 0x66,
        block_hash: ~h[0xa957d47df264a31badc3ae823e10ac1d444b098d9b73d204c40426e57f47e8c3],
        block_number: 0xeff35f,
        from: ~h[0x6221a9c005f6e47eb398fd867784cacfdcfff4e7],
        to: ~h[0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2],
        cumulative_gas_used: 0xa12515,
        effective_gas_price: 0x5a9c688d4,
        gas_used: 0xb4c8,
        contract_address: nil,
        logs: [
          %Onchain.Filter.Log{
            log_index: 1,
            block_number: 0x01b4,
            block_hash: ~h[0xaa8216c5785ac562ff41e2dcfdf5785ac562ff41e2dcfdf829c5a142f1fccd7d],
            transaction_hash: ~h[0xaadf829c5a142f1fccd7d8216c5785ac562ff41e2dcfdf5785ac562ff41e2dcf],
            transaction_index: 0,
            address: ~h[0x16c5785ac562ff41e2dcfdf829c5a142f1fccd7d],
            data: ~h[0x0000000000000000000000000000000000000000000000000000000000000000],
            topics: [
              ~h[0x59ebeb90bc63057b6515673c3ecf9438e5058bca0f92585014eced636878c9a5]
            ]
          }
        ],
        logs_bloom: ~h[0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001],
        type: 0x02,
        status: 0x01,
      }
    }

    iex> Onchain.RPC.get_trx_receipt("0x85d995eba9763907fdf35cd2034144dd9d53ce32cbec21349d4b12823c6860c5")
    {:ok,
      %Onchain.Receipt{
        transaction_hash: ~h[0x85d995eba9763907fdf35cd2034144dd9d53ce32cbec21349d4b12823c6860c5],
        transaction_index: 0x66,
        block_hash: ~h[0xa957d47df264a31badc3ae823e10ac1d444b098d9b73d204c40426e57f47e8c3],
        block_number: 0xeff35f,
        from: ~h[0x6221a9c005f6e47eb398fd867784cacfdcfff4e7],
        to: ~h[0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2],
        cumulative_gas_used: 0xa12515,
        effective_gas_price: 0x5a9c688d4,
        gas_used: 0xb4c8,
        contract_address: nil,
        logs: [
          %Onchain.Filter.Log{
            log_index: 1,
            block_number: 0x01b4,
            block_hash: ~h[0xaa8216c5785ac562ff41e2dcfdf5785ac562ff41e2dcfdf829c5a142f1fccd7d],
            transaction_hash: ~h[0xaadf829c5a142f1fccd7d8216c5785ac562ff41e2dcfdf5785ac562ff41e2dcf],
            transaction_index: 0,
            address: ~h[0x16c5785ac562ff41e2dcfdf829c5a142f1fccd7d],
            data: ~h[0x0000000000000000000000000000000000000000000000000000000000000000],
            topics: [
              ~h[0x59ebeb90bc63057b6515673c3ecf9438e5058bca0f92585014eced636878c9a5]
            ]
          }
        ],
        logs_bloom: ~h[0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001],
        type: 0x02,
        status: 0x01,
      }
    }

    iex> Onchain.RPC.get_trx_receipt("0xf9e69be4f1ae524854e14dc820c519d8f2b86e52c60e54448abf920d22fb6fe2")
    {:ok, %Onchain.Receipt{
      transaction_hash: ~h[0xf9e69be4f1ae524854e14dc820c519d8f2b86e52c60e54448abf920d22fb6fe2],
      transaction_index: 0,
      block_hash: ~h[0x4bc3c26b1a599ced9876d9bf9a17c9bd58ec8b71a68e75335de7f2820e9336ca],
      block_number: 10493428,
      from: ~h[0xb03d1100c68e58aa1895f8c1f230c0851ff41851],
      to: ~h[0x9d8ec03e9ddb71f04da9db1e38837aaac1782a97],
      cumulative_gas_used: 222642,
      effective_gas_price: 1200000010,
      gas_used: 222642,
      contract_address: nil,
      logs: [
        %Onchain.Filter.Log{
          removed: false,
          log_index: 0,
          block_number: 10493428,
          block_hash: ~h[0x4bc3c26b1a599ced9876d9bf9a17c9bd58ec8b71a68e75335de7f2820e9336ca],
          transaction_hash: ~h[0xf9e69be4f1ae524854e14dc820c519d8f2b86e52c60e54448abf920d22fb6fe2],
          transaction_index: 0,
          address: ~h[0x9d8ec03e9ddb71f04da9db1e38837aaac1782a97],
          data: ~h[0x000000000000000000000000cb372382aa9a9e6f926714f4305afac4566f75380000000000000000000000000000000000000000000000000000000000000000],
          topics: [
            ~h[0x3ffe5de331422c5ec98e2d9ced07156f640bb51e235ef956e50263d4b28d3ae4],
            ~h[0x0000000000000000000000002326aba712500ae3114b664aeb51dba2c2fb416d],
            ~h[0x0000000000000000000000002326aba712500ae3114b664aeb51dba2c2fb416d]
          ]
        },
        %Onchain.Filter.Log{
          removed: false,
          log_index: 1,
          block_number: 10493428,
          block_hash: ~h[0x4bc3c26b1a599ced9876d9bf9a17c9bd58ec8b71a68e75335de7f2820e9336ca],
          transaction_hash: ~h[0xf9e69be4f1ae524854e14dc820c519d8f2b86e52c60e54448abf920d22fb6fe2],
          transaction_index: 0,
          address: ~h[0xcb372382aa9a9e6f926714f4305afac4566f7538],
          data: ~h[0x0000000000000000000000000000000000000000000000000000000000000000],
          topics: [
            ~h[0xe0d20d95fbbe7375f6edead77b5ce5c5b096e7dac85848c45c37a95eaf17fe62],
            ~h[0x0000000000000000000000009d8ec03e9ddb71f04da9db1e38837aaac1782a97],
            ~h[0x00000000000000000000000054f0a87eb5c8c8ba70243de1ac19e735b41b10a2],
            ~h[0x0000000000000000000000000000000000000000000000000000000000000000]
          ]
        },
        %Onchain.Filter.Log{
          removed: false,
          log_index: 2,
          block_number: 10493428,
          block_hash: ~h[0x4bc3c26b1a599ced9876d9bf9a17c9bd58ec8b71a68e75335de7f2820e9336ca],
          transaction_hash: ~h[0xf9e69be4f1ae524854e14dc820c519d8f2b86e52c60e54448abf920d22fb6fe2],
          transaction_index: 0,
          address: ~h[0xcb372382aa9a9e6f926714f4305afac4566f7538],
          data: <<>>,
          topics: [
            ~h[0x0000000000000000000000000000000000000000000000000000000000000055]
          ]
        }
      ],
      logs_bloom: ~h[0x00800000000000000000000400000000000000000000000000000000000000000000000000000000000000000000002000200040000000000000000200001000000000000000000000000000000000000000000000000000000000000010000000008000020000004000000200000800000000000000000000220000000000000000000000000800000000000400000000000000000000000000000000000000000000040000000000008000008000000000000000000000000000000004000000800000000000004000000000000000000000000000000004080000000020000000000000000080000000000000000000000000000000000000000000000000],
      type: 0,
      status: 1
    }}

    iex> Onchain.RPC.get_trx_receipt(<<1::256>>)
    {:error, "failed to decode `eth_getTransactionReceipt` response: %FunctionClauseError{module: Onchain.Hex, function: :decode_hex_, arity: 1, kind: nil, args: nil, clauses: nil}"}

    iex> Onchain.RPC.get_trx_receipt(<<1::256>>, verbose: true)
    {:error, "failed to decode `eth_getTransactionReceipt` response: %FunctionClauseError{module: Onchain.Hex, function: :decode_hex_, arity: 1, kind: nil, args: nil, clauses: nil}"}

    iex> Onchain.RPC.get_trx_receipt(<<2::256>>)
    {:ok, nil}

# `max_priority_fee_per_gas`

```elixir
@spec max_priority_fee_per_gas(Keyword.t()) ::
  {:ok, non_neg_integer()} | {:error, term()}
```

RPC call to call to get the current max priority fee per gas.

## Examples

    iex> Onchain.RPC.max_priority_fee_per_gas()
    {:ok, 1000000001}

# `net_listening`

```elixir
@spec net_listening(Keyword.t()) :: {:ok, boolean()} | {:error, term()}
```

Returns whether the node is listening for peer connections (`net_listening`).

`net_listening` is not in any tagged `execution-apis` release. It landed on
`main` after v1.0.0-beta.7 in commit `7c58b32` (#843, 2026-08-24). This
wrapper does not make the method standard.

Observed 2026-10-01 on the hosted lanes from the multi-endpoint seam: Alchemy
(`ETHEREUM_ALCHEMY_URL`) and Infura (`ETHEREUM_INFURA_URL`) both answered
HTTP 200 with JSON `true`. Neither lane refused the method.

A refusing endpoint's JSON-RPC error is classified by `send_rpc/3`, not by a
per-method probe. `-32601`, and `-32600` whose message says the method is
unsupported or not available, become
`{:error, {:method_not_found, map}}`. A plan-disabled namespace becomes
`{:error, {:namespace_unavailable, map}}`. `-32001` whose message says the
node is unable to complete the request becomes
`{:error, {:unavailable, map}}`.

## Examples

    iex> Onchain.RPC.net_listening()
    {:ok, true}

# `net_peer_count`

```elixir
@spec net_peer_count(Keyword.t()) :: {:ok, non_neg_integer()} | {:error, term()}
```

Returns the node's connected peer count (`net_peerCount`).

`net_peerCount` is not in any tagged `execution-apis` release. It landed on
`main` after v1.0.0-beta.7 in commit `7c58b32` (#843, 2026-08-24). This
wrapper does not make the method standard.

Observed 2026-10-01 on Alchemy mainnet (`ETHEREUM_ALCHEMY_URL`), HTTP 400:

    {"code":-32600,"message":"net_peerCount is not available on the ETH_MAINNET. For more information see our docs: https://docs.alchemy.com/alchemy/documentation/apis/ethereum"}

That refusal surfaces as
`{:error, {:method_not_found, %{code: -32600, message: "net_peerCount is not available on the ETH_MAINNET. For more information see our docs: https://docs.alchemy.com/alchemy/documentation/apis/ethereum"}}}`.
The same classification is `send_rpc/3`'s shared refusal tag. There is no
separate capability probe. Infura answered a peer-count quantity on the same
day; a hosted success is not evidence the method is standard.

## Examples

    iex> Onchain.RPC.net_peer_count()
    {:ok, 2}

# `new_block_filter`

```elixir
@spec new_block_filter(Keyword.t()) :: {:ok, String.t()} | {:error, term()}
```

RPC call to create a new block filter.

## Examples

    iex> Onchain.RPC.new_block_filter()
    {:ok, "0xb10cf11e"}

# `new_pending_transaction_filter`

```elixir
@spec new_pending_transaction_filter(Keyword.t()) ::
  {:ok, String.t()} | {:error, term()}
```

RPC call to create a new pending-transaction filter.

## Examples

    iex> Onchain.RPC.new_pending_transaction_filter()
    {:ok, "0xpend1ng"}

# `prepare_trx`

```elixir
@spec prepare_trx(&lt;&lt;_::160&gt;&gt;, binary() | {String.t(), [term()]}, Keyword.t()) ::
  {:ok, Onchain.Transaction.V1.t() | Onchain.Transaction.V2.t()}
  | {:error, term()}
```

Helper function to work with other Cartouche modules to get a nonce, sign a transction, and prepare it to be submitted on-chain.

If you need higher-level functionality, like manual nonce tracking, you may want to use the more granular function calls.

Options:
  * `gas_price` - Set the gas price for a v1 (non-Eip1559) transaction, if nil, comes from `eth_gasPrice` (default `nil`) [note: only compatible with V1 transaction]
  * `base_fee` - Set the base price for the transaction, if nil, will use base gas price from `eth_feeHistory` (default `nil`) [note: only compatible with V2 transactions]
  * `base_fee_buffer` - Buffer for the gas price or base fee when estimating gas price. Ingored if `gas_price` (for v1) or `base_fee` (for v2) is specified directly (default: 1.2 = 120%)
  * `priority_fee` - Additional gas to send as a priority fee. (default: `{0, :gwei}`) [note: only compatible with V2 transactions]
  * `gas_limit` - Set the gas limit for the transaction (default: calls `eth_estimateGas`)
  * `gas_buffer` - Buffer if estimating gas limit (default: 1.5 = 150%)
  * `value` - Value to provide with transaction in wei (default: 0)
  * `nonce` - Nonce to send with transaction. (default: lookup via `eth_transactionCount`)
  * `verify` - Verify the function is likely to succeed (default: true)
  * `trx_type` - :v1 for V1 (pre-EIP-1559 transactions), :v2 for V2 (EIP-1559) transactions, and `nil` for auto-detect.

  Note: if we don't `verify`, then `estimateGas` will likely fail if the transaction were to fail.
        To prevent this, `gas_limit` should always be supplied when `verify` is set to false.

## Examples
    iex> signer_proc = Onchain.Test.Signer.start_signer()
    iex> {:ok, trx} = Onchain.RPC.prepare_trx(<<1::160>>, {"baz(uint,address)", [50, :binary.decode_unsigned(<<1::160>>)]}, gas_price: {50, :gwei}, nonce: 10, value: 0, signer: signer_proc)
    iex> %{trx|v: nil, r: nil, s: nil}
    %Onchain.Transaction.V1{
      nonce: 10,
      gas_price: 50000000000,
      gas_limit: 20,
      to: <<1::160>>,
      value: 0,
      data: <<162, 145, 173, 214, 0::248, 50, 0::248, 1>>
    }

    iex> signer_proc = Onchain.Test.Signer.start_signer()
    iex> {:ok, trx} = Onchain.RPC.prepare_trx(<<1::160>>, {"baz(uint,address)", [50, <<1::160>> |> :binary.decode_unsigned]}, gas_price: {50, :gwei}, gas_limit: 100_000, value: 0, signer: signer_proc)
    iex> %{trx|v: nil, r: nil, s: nil}
    %Onchain.Transaction.V1{
      nonce: 4,
      gas_price: 50000000000,
      gas_limit: 100000,
      to: <<1::160>>,
      value: 0,
      data: <<162, 145, 173, 214, 0::248, 50, 0::248, 1>>
    }

    iex> signer_proc = Onchain.Test.Signer.start_signer()
    iex> {:ok, trx} = Onchain.RPC.prepare_trx(<<1::160>>, {"baz(uint,address)", [50, <<1::160>> |> :binary.decode_unsigned]}, gas_price: {50, :gwei}, gas_limit: 100_000, value: 0, nonce: 10, signer: signer_proc)
    iex> %{trx|v: nil, r: nil, s: nil}
    %Onchain.Transaction.V1{
      nonce: 10,
      gas_price: 50000000000,
      gas_limit: 100000,
      to: <<1::160>>,
      value: 0,
      data: <<162, 145, 173, 214, 0::248, 50, 0::248, 1>>
    }

    iex> signer_proc = Onchain.Test.Signer.start_signer()
    iex> Onchain.RPC.prepare_trx(<<10::160>>, {"baz(uint,address)", [50, <<1::160>> |> :binary.decode_unsigned]}, gas_price: {50, :gwei}, gas_limit: 100_000, value: 0, nonce: 10, signer: signer_proc)
    {:error, %{code: 3, message: "execution reverted", revert: <<61, 115, 139, 46>>}}

    iex> # Set gas price directly
    iex> signer_proc = Onchain.Test.Signer.start_signer()
    iex> {:ok, trx} = Onchain.RPC.prepare_trx(<<10::160>>, {"baz(uint,address)", [50, <<1::160>> |> :binary.decode_unsigned]}, gas_price: {50, :gwei}, gas_limit: 100_000, value: 0, nonce: 10, verify: false, signer: signer_proc)
    iex> %{trx|v: nil, r: nil, s: nil}
    %Onchain.Transaction.V1{
      nonce: 10,
      gas_price: 50000000000,
      gas_limit: 100000,
      to: <<10::160>>,
      value: 0,
      data: <<162, 145, 173, 214, 0::248, 50, 0::248, 1>>
    }

    iex> # Default gas price v1
    iex> signer_proc = Onchain.Test.Signer.start_signer()
    iex> {:ok, trx} = Onchain.RPC.prepare_trx(<<10::160>>, {"baz(uint,address)", [50, <<1::160>> |> :binary.decode_unsigned]}, gas_limit: 100_000, trx_type: :v1, value: 0, nonce: 10, verify: false, signer: signer_proc)
    iex> %{trx|v: nil, r: nil, s: nil}
    %Onchain.Transaction.V1{
      nonce: 10,
      gas_price: 1200000000,
      gas_limit: 100000,
      to: <<10::160>>,
      value: 0,
      data: <<162, 145, 173, 214, 0::248, 50, 0::248, 1>>
    }

    iex> # Default gas price v2
    iex> signer_proc = Onchain.Test.Signer.start_signer()
    iex> {:ok, trx} = Onchain.RPC.prepare_trx(<<10::160>>, {"baz(uint,address)", [50, <<1::160>> |> :binary.decode_unsigned]}, gas_limit: 100_000, trx_type: :v2, value: 0, nonce: 10, verify: false, signer: signer_proc)
    iex> %{trx|signature_y_parity: nil, signature_r: nil, signature_s: nil}
    %Onchain.Transaction.V2{
      chain_id: 5,
      nonce: 10,
      gas_limit: 100000,
      destination: <<10::160>>,
      amount: 0,
      max_fee_per_gas: 25679608965,
      max_priority_fee_per_gas: 1000000001,
      data: <<162, 145, 173, 214, 0::248, 50, 0::248, 1>>,
      access_list: []
    }

    iex> # Default gas price (trx_type: nil)
    iex> signer_proc = Onchain.Test.Signer.start_signer()
    iex> {:ok, trx} = Onchain.RPC.prepare_trx(<<10::160>>, {"baz(uint,address)", [50, <<1::160>> |> :binary.decode_unsigned]}, gas_limit: 100_000, value: 0, nonce: 10, verify: false, signer: signer_proc)
    iex> %{trx|signature_y_parity: nil, signature_r: nil, signature_s: nil}
    %Onchain.Transaction.V2{
      chain_id: 5,
      nonce: 10,
      gas_limit: 100000,
      destination: <<10::160>>,
      amount: 0,
      max_fee_per_gas: 25679608965,
      max_priority_fee_per_gas: 1000000001,
      data: <<162, 145, 173, 214, 0::248, 50, 0::248, 1>>,
      access_list: []
    }

    iex> # Set priority fee (v2)
    iex> signer_proc = Onchain.Test.Signer.start_signer()
    iex> {:ok, trx} = Onchain.RPC.prepare_trx(<<10::160>>, {"baz(uint,address)", [50, <<1::160>> |> :binary.decode_unsigned]}, priority_fee: {3, :gwei}, gas_limit: 100_000, value: 0, nonce: 10, verify: false, signer: signer_proc)
    iex> %{trx|signature_y_parity: nil, signature_r: nil, signature_s: nil}
    %Onchain.Transaction.V2{
      chain_id: 5,
      nonce: 10,
      gas_limit: 100000,
      destination: <<10::160>>,
      amount: 0,
      max_fee_per_gas: 27679608964,
      max_priority_fee_per_gas: 3000000000,
      data: <<162, 145, 173, 214, 0::248, 50, 0::248, 1>>,
      access_list: []
    }

    iex> # Set base fee and priority fee (v2)
    iex> signer_proc = Onchain.Test.Signer.start_signer()
    iex> {:ok, trx} = Onchain.RPC.prepare_trx(<<10::160>>, {"baz(uint,address)", [50, <<1::160>> |> :binary.decode_unsigned]}, base_fee: {1, :gwei}, priority_fee: {3, :gwei}, gas_limit: 100_000, value: 0, nonce: 10, verify: false, signer: signer_proc)
    iex> %{trx|signature_y_parity: nil, signature_r: nil, signature_s: nil}
    %Onchain.Transaction.V2{
      chain_id: 5,
      nonce: 10,
      gas_limit: 100000,
      destination: <<10::160>>,
      amount: 0,
      max_fee_per_gas: 4000000000,
      max_priority_fee_per_gas: 3000000000,
      data: <<162, 145, 173, 214, 0::248, 50, 0::248, 1>>,
      access_list: []
    }

    iex> # Sets chain id
    iex> signer_proc = Onchain.Test.Signer.start_signer()
    iex> {:ok, trx} = Onchain.RPC.prepare_trx(<<10::160>>, {"baz(uint,address)", [50, <<1::160>> |> :binary.decode_unsigned]}, base_fee: {1, :gwei}, priority_fee: {3, :gwei}, gas_limit: 100_000, value: 0, nonce: 10, verify: false, signer: signer_proc, chain_id: 99)
    iex> %{trx|signature_y_parity: nil, signature_r: nil, signature_s: nil}
    %Onchain.Transaction.V2{
      chain_id: 99,
      nonce: 10,
      gas_limit: 100000,
      destination: <<10::160>>,
      amount: 0,
      max_fee_per_gas: 4000000000,
      max_priority_fee_per_gas: 3000000000,
      data: <<162, 145, 173, 214, 0::248, 50, 0::248, 1>>,
      access_list: []
    }

# `send_batch`

```elixir
@spec send_batch([{String.t(), [term()]}], keyword()) ::
  {:ok, [term()]} | {:error, term()}
```

Sends a JSON-RPC array batch and returns raw results in request order. Uses the same options as `send_rpc/3`.

# `send_rpc`

```elixir
@spec send_rpc(binary(), [term()], Keyword.t()) ::
  {:ok, term()} | {:error, send_rpc_error()} | :invalid_hex
```

Simple RPC client for a JSON-RPC Ethereum node.

## Examples

    iex> Onchain.RPC.send_rpc("net_version", [])
    {:ok, "3"}

    iex> use Onchain.Hex
    iex> Onchain.RPC.send_rpc("get_balance", ["0x407d73d8a49eeb85d32cf465507dd71d507100c1", "latest"], ethereum_node: "http://example.com")
    {:ok, "0x0234c8a3397aab58"}

    iex> match?({:error, {:invalid_params, %Jason.EncodeError{}}}, Onchain.RPC.send_rpc(<<255>>, []))
    true

    iex> match?({:error, {:invalid_params, %Protocol.UndefinedError{}}}, Onchain.RPC.send_rpc("net_version", [self()]))
    true

## Options

Common options (other RPC wrappers forward `opts` here):

- `:rpc_url` / `:ethereum_node` — node URL (`:rpc_url` takes precedence); falls back to `Application.get_env(:cartouche, :ethereum_node)`
- `:retry` — opt-in `[max_retries: 2, backoff_ms: 100]`; only transport failures
  retry, before typed decoding. JSON-RPC application errors are final.
- `:timeout` — Req `receive_timeout` in ms
- `:headers` — extra request headers
- `:verbose` — when `true`, decode failures log at `:error` instead of `:info`
- `:req_options` — a keyword list merged into the `Req.request/1` options (highest
  precedence), exposing Req's whole pipeline (retries, redirects, a custom `finch:`
  pool, telemetry, proxies, plugs). A global default can be set with
  `config :cartouche, :req_options, [...]`. Tests stub the transport by passing
  `req_options: [plug: ...]` (or configuring `config :cartouche, Onchain.RPC, plug: ...`).

# `send_transaction`

```elixir
@spec send_transaction(
  Onchain.Transaction.V1.t()
  | Onchain.Transaction.V2.t()
  | Onchain.Transaction.Call.t(),
  Keyword.t()
) :: {:ok, binary()} | {:error, term()}
```

RPC call to `eth_sendTransaction`.

## Examples

    iex> {:ok, hash} =
    ...>   Onchain.Transaction.V1.new(1, {100, :gwei}, 100_000, <<1::160>>, {2, :wei}, <<1, 2, 3>>)
    ...>   |> Onchain.RPC.send_transaction()
    iex> Base.encode16(hash, case: :lower)
    "abababababababababababababababababababababababababababababababab"

# `send_trx`

```elixir
@spec send_trx(
  Onchain.Transaction.V1.t() | Onchain.Transaction.V2.t() | String.t(),
  Keyword.t()
) ::
  {:ok, binary()} | {:error, term()}
```

RPC call to send a raw transaction.

## Examples

    iex> signer_proc = Onchain.Test.Signer.start_signer()
    iex> {:ok, signed_trx} = Onchain.Transaction.build_signed_trx(<<1::160>>, 5, {"baz(uint,address)", [50, :binary.decode_unsigned(<<1::160>>)]}, {50, :gwei}, 100_000, 0, chain_id: :goerli, signer: signer_proc)
    iex> {:ok, trx_id} = Onchain.RPC.send_trx(signed_trx)
    iex> <<nonce::integer-size(8), gas_price::integer-size(64), gas_limit::integer-size(24), to::binary>> = trx_id
    iex> {nonce, gas_price, gas_limit, to}
    {5, 50000000000, 100000, <<1::160>>}

    iex> signer_proc = Onchain.Test.Signer.start_signer()
    iex> {:ok, signed_trx} = Onchain.Transaction.build_signed_trx_v2(<<1::160>>, 5, {"baz(uint,address)", [50, :binary.decode_unsigned(<<1::160>>)]}, {50, :gwei}, {10, :gwei}, 100_000, 0, [], chain_id: :goerli, signer: signer_proc)
    iex> {:ok, trx_id} = Onchain.RPC.send_trx(signed_trx)
    iex> <<nonce::integer-size(8), max_priority_fee_per_gas::integer-size(64), max_fee_per_gas::integer-size(64), gas_limit::integer-size(24), to::binary>> = trx_id
    iex> {nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, to}
    {5, 50000000000, 10000000000, 100000, <<1::160>>}

# `sign`

```elixir
@spec sign(&lt;&lt;_::160&gt;&gt;, binary(), Keyword.t()) :: {:ok, binary()} | {:error, term()}
```

RPC call to `eth_sign`.

The node signs `message` with a managed account and returns an EIP-191
signature over it — `message` is the data to be signed, not a digest the
node signs verbatim. Most nodes disable this method; when they do, the
node's error is returned unchanged.

## Examples

    iex> {:ok, signature} = Onchain.RPC.sign(<<1::160>>, "hello")
    iex> byte_size(signature)
    65

# `sign_transaction`

```elixir
@spec sign_transaction(
  Onchain.Transaction.V1.t()
  | Onchain.Transaction.V2.t()
  | Onchain.Transaction.Call.t(),
  Keyword.t()
) :: {:ok, struct()} | {:error, term()}
```

RPC call to `eth_signTransaction`.

The node signs with its keystore and returns a raw transaction, which is
decoded through `Onchain.Transaction.decode/1`.

## Examples

    iex> {:ok, trx} =
    ...>   Onchain.Transaction.V1.new(1, {100, :gwei}, 100_000, <<1::160>>, {2, :wei}, <<1, 2, 3>>, :kovan)
    ...>   |> Onchain.RPC.sign_transaction()
    iex> trx.nonce
    1

# `trace_call`

```elixir
@spec trace_call(
  Onchain.Transaction.V1.t()
  | Onchain.Transaction.V2.t()
  | Onchain.Transaction.Call.t(),
  Keyword.t()
) :: {:ok, Onchain.TraceCall.t()} | {:error, term()}
```

RPC to trace a transaction call speculatively.

## Examples

    iex> Onchain.Transaction.V1.new(1, {100, :gwei}, 100_000, <<1::160>>, {2, :wei}, <<1, 2, 3>>)
    ...> |> Onchain.RPC.trace_call()
    {:ok,
      %Onchain.TraceCall{
        output: "",
        state_diff: nil,
        trace: [
          %Onchain.RPC.Trace{
            action: %Onchain.RPC.Trace.Action{
              call_type: "call",
              init: nil,
              from: <<0::160>>,
              gas: 499_978_072,
              input: ~h[0xd1692f56000000000000000000000000142da9114e5a98e015aa95afca0585e84832a612000000000000000000000000142da9114e5a98e015aa95afca0585e84832a6120000000000000000000000000000000000000000000000000000000000000000],
              to: ~h[0x13172EE393713FBA9925A9A752341EBD31E8D9A7],
              value: 0
            },
            block_hash: nil,
            block_number: nil,
            gas_used: 492_166_471,
            error: "Reverted",
            output: "",
            result_code: nil,
            result_address: nil,
            subtraces: 1,
            trace_address: [],
            transaction_hash: nil,
            transaction_position: nil,
            type: "call"
          },
          %Onchain.RPC.Trace{
            action: %Onchain.RPC.Trace.Action{
              call_type: nil,
              init: ~h[0x60e03461009157601f6101ec38819003918201601f19168301916001600160401b038311848410176100965780849260609460405283398101031261009157610047816100ac565b906100606040610059602084016100ac565b92016100ac565b9060805260a05260c05260405161012b90816100c18239608051816088015260a051816045015260c0518160c60152f35b600080fd5b634e487b7160e01b600052604160045260246000fd5b51906001600160a01b03821682036100915756fe608060405260043610156013575b3660ba57005b6000803560e01c8063238ac9331460775763c34c08e51460325750600d565b34607457806003193601126074576040517f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03168152602090f35b80fd5b5034607457806003193601126074577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166080908152602090f35b600036818037808036817f00000000000000000000000000000000000000000000000000000000000000005af4903d918282803e60f357fd5bf3fea264697066735822122032b5603d6937ceb7a252e16379744d8545670ff4978c8d76c985d051dfcfe46c64736f6c6343000817003300000000000000000000000049e5d261e95f6a02505078bb339fecb210a0b634000000000000000000000000142da9114e5a98e015aa95afca0585e84832a612000000000000000000000000142da9114e5a98e015aa95afca0585e84832a612],
              from: ~h[0x13172EE393713FBA9925A9A752341EBD31E8D9A7],
              gas: 492_133_529,
              input: nil,
              to: nil,
              value: 0
            },
            block_hash: nil,
            block_number: nil,
            gas_used: nil,
            error: "contract address collision",
            output: nil,
            result_code: nil,
            result_address: nil,
            subtraces: 0,
            trace_address: [0],
            transaction_hash: nil,
            transaction_position: nil,
            type: "create"
          }
        ],
        vm_trace: nil
      }
    }

    iex> Onchain.Transaction.V2.new(1, {1, :gwei}, {100, :gwei}, 100_000, <<1::160>>, {2, :wei}, <<1, 2, 3>>, [<<2::160>>, <<3::160>>], :goerli)
    ...> |> Onchain.RPC.trace_call()
    {:ok,
      %Onchain.TraceCall{
        output: "",
        state_diff: nil,
        trace: [
          %Onchain.RPC.Trace{
            action: %Onchain.RPC.Trace.Action{
              call_type: "call",
              init: nil,
              from: <<0::160>>,
              gas: 499_978_072,
              input: ~h[0xd1692f56000000000000000000000000142da9114e5a98e015aa95afca0585e84832a612000000000000000000000000142da9114e5a98e015aa95afca0585e84832a6120000000000000000000000000000000000000000000000000000000000000000],
              to: ~h[0x13172EE393713FBA9925A9A752341EBD31E8D9A7],
              value: 0
            },
            block_hash: nil,
            block_number: nil,
            gas_used: 492_166_471,
            error: "Reverted",
            output: "",
            result_code: nil,
            result_address: nil,
            subtraces: 1,
            trace_address: [],
            transaction_hash: nil,
            transaction_position: nil,
            type: "call"
          },
          %Onchain.RPC.Trace{
            action: %Onchain.RPC.Trace.Action{
              call_type: nil,
              init: ~h[0x60e03461009157601f6101ec38819003918201601f19168301916001600160401b038311848410176100965780849260609460405283398101031261009157610047816100ac565b906100606040610059602084016100ac565b92016100ac565b9060805260a05260c05260405161012b90816100c18239608051816088015260a051816045015260c0518160c60152f35b600080fd5b634e487b7160e01b600052604160045260246000fd5b51906001600160a01b03821682036100915756fe608060405260043610156013575b3660ba57005b6000803560e01c8063238ac9331460775763c34c08e51460325750600d565b34607457806003193601126074576040517f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03168152602090f35b80fd5b5034607457806003193601126074577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166080908152602090f35b600036818037808036817f00000000000000000000000000000000000000000000000000000000000000005af4903d918282803e60f357fd5bf3fea264697066735822122032b5603d6937ceb7a252e16379744d8545670ff4978c8d76c985d051dfcfe46c64736f6c6343000817003300000000000000000000000049e5d261e95f6a02505078bb339fecb210a0b634000000000000000000000000142da9114e5a98e015aa95afca0585e84832a612000000000000000000000000142da9114e5a98e015aa95afca0585e84832a612],
              from: ~h[0x13172EE393713FBA9925A9A752341EBD31E8D9A7],
              gas: 492_133_529,
              input: nil,
              to: nil,
              value: 0
            },
            block_hash: nil,
            block_number: nil,
            gas_used: nil,
            error: "contract address collision",
            output: nil,
            result_code: nil,
            result_address: nil,
            subtraces: 0,
            trace_address: [0],
            transaction_hash: nil,
            transaction_position: nil,
            type: "create"
          }
        ],
        vm_trace: nil
      }
    }

# `trace_call_many`

```elixir
@spec trace_call_many(
  [
    Onchain.Transaction.V1.t()
    | Onchain.Transaction.V2.t()
    | Onchain.Transaction.Call.t()
    | {Onchain.Transaction.V1.t()
       | Onchain.Transaction.V2.t()
       | Onchain.Transaction.Call.t(), &lt;&lt;_::160&gt;&gt; | nil}
  ],
  Keyword.t()
) :: {:ok, [Onchain.TraceCall.t()]} | {:error, term()}
```

RPC to trace many transaction calls speculatively.

## Examples

    iex> t1 = Onchain.Transaction.V1.new(1, {100, :gwei}, 100_000, <<1::160>>, {2, :wei}, <<1, 2, 3>>)
    iex> t2 = Onchain.Transaction.V2.new(1, {1, :gwei}, {100, :gwei}, 100_000, <<1::160>>, {2, :wei}, <<1, 2, 3>>, [<<2::160>>, <<3::160>>], :goerli)
    iex> Onchain.RPC.trace_call_many([t1, t2])
    {:ok, [
      %Onchain.TraceCall{
        output: "",
        state_diff: nil,
        trace: [
          %Onchain.RPC.Trace{
            action: %Onchain.RPC.Trace.Action{
              call_type: "call",
              init: nil,
              from: <<0::160>>,
              gas: 499_978_072,
              input: ~h[0xd1692f56000000000000000000000000142da9114e5a98e015aa95afca0585e84832a612000000000000000000000000142da9114e5a98e015aa95afca0585e84832a6120000000000000000000000000000000000000000000000000000000000000000],
              to: ~h[0x13172EE393713FBA9925A9A752341EBD31E8D9A7],
              value: 0
            },
            block_hash: nil,
            block_number: nil,
            gas_used: 492_166_471,
            error: "Reverted",
            output: "",
            result_code: nil,
            result_address: nil,
            subtraces: 1,
            trace_address: [],
            transaction_hash: nil,
            transaction_position: nil,
            type: "call"
          },
          %Onchain.RPC.Trace{
            action: %Onchain.RPC.Trace.Action{
              call_type: nil,
              init: ~h[0x60e03461009157601f6101ec38819003918201601f19168301916001600160401b038311848410176100965780849260609460405283398101031261009157610047816100ac565b906100606040610059602084016100ac565b92016100ac565b9060805260a05260c05260405161012b90816100c18239608051816088015260a051816045015260c0518160c60152f35b600080fd5b634e487b7160e01b600052604160045260246000fd5b51906001600160a01b03821682036100915756fe608060405260043610156013575b3660ba57005b6000803560e01c8063238ac9331460775763c34c08e51460325750600d565b34607457806003193601126074576040517f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03168152602090f35b80fd5b5034607457806003193601126074577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166080908152602090f35b600036818037808036817f00000000000000000000000000000000000000000000000000000000000000005af4903d918282803e60f357fd5bf3fea264697066735822122032b5603d6937ceb7a252e16379744d8545670ff4978c8d76c985d051dfcfe46c64736f6c6343000817003300000000000000000000000049e5d261e95f6a02505078bb339fecb210a0b634000000000000000000000000142da9114e5a98e015aa95afca0585e84832a612000000000000000000000000142da9114e5a98e015aa95afca0585e84832a612],
              from: ~h[0x13172EE393713FBA9925A9A752341EBD31E8D9A7],
              gas: 492_133_529,
              input: nil,
              to: nil,
              value: 0
            },
            block_hash: nil,
            block_number: nil,
            gas_used: nil,
            error: "contract address collision",
            output: nil,
            result_code: nil,
            result_address: nil,
            subtraces: 0,
            trace_address: [0],
            transaction_hash: nil,
            transaction_position: nil,
            type: "create"
          }
        ],
        vm_trace: nil
      },
      %Onchain.TraceCall{
        output: ~h[0x00000000000000000000000079EDBC4F3A6AA2266CD469CC544501743BE8B078],
        state_diff: nil,
        trace: [
          %Onchain.RPC.Trace{
            action: %Onchain.RPC.Trace.Action{
              call_type: "call",
              init: nil,
              from: <<0::160>>,
              gas: 499_945_916,
              input: ~h[0xd6d38d3f0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000081a60808060405234610016576107fe908161001c8239f35b600080fdfe6040608081526004908136101561001557600080fd5b600091823560e01c80630c0a769b146102eb57806350a4548914610255578063c3da3590146100fc5763f1afb11f1461004d57600080fd5b8291346100f85760803660031901126100f857610068610389565b61007061039f565b6100786103b5565b6001600160a01b03908116929091606435918390610097848288610482565b1693843b156100f457879460649386928851998a978896634232cd6360e01b88521690860152602485015260448401525af19081156100eb57506100d85750f35b6100e1906103fc565b6100e85780f35b80fd5b513d84823e3d90fd5b8780fd5b5050fd5b503461025157606036600319011261025157610116610389565b67ffffffffffffffff929060243584811161024d5761013890369085016103cb565b9190946044359081116102495761015290369086016103cb565b9590928681036102395791958793926001600160a01b0380891693909290865b83811061017d578780f35b6101a88561019461018f848887610448565b61046e565b168c6101a184878c610448565b3591610482565b6101b661018f828685610448565b6101c182858a610448565b3590873b15610235578a51631e573fb760e31b81526001600160a01b03909116818d019081526020810192909252908990829081906040010381838b5af1801561022b57908991610217575b5050600101610172565b610220906103fc565b6100f457873861020d565b8a513d8b823e3d90fd5b8980fd5b845163b4fa3fb360e01b81528690fd5b8680fd5b8580fd5b8280fd5b50346102515760a03660031901126102515761026f610389565b9161027861039f565b6102806103b5565b906001600160a01b039060643582811691908290036102e6578288971693843b156100f457879460849385879389519a8b988997639032317760e01b895216908701521660248501526044840152833560648401525af19081156100eb57506100d85750f35b600080fd5b5090346102515760603660031901126102515782610307610389565b61030f61039f565b604435916001600160a01b03906103298482858516610482565b1690813b15610385578451631e573fb760e31b81526001600160a01b039091169581019586526020860192909252909384919082908490829060400103925af19081156100eb5750610379575080f35b610382906103fc565b80f35b8380fd5b600435906001600160a01b03821682036102e657565b602435906001600160a01b03821682036102e657565b604435906001600160a01b03821682036102e657565b9181601f840112156102e65782359167ffffffffffffffff83116102e6576020808501948460051b0101116102e657565b67ffffffffffffffff811161041057604052565b634e487b7160e01b600052604160045260246000fd5b90601f8019910116810190811067ffffffffffffffff82111761041057604052565b91908110156104585760051b0190565b634e487b7160e01b600052603260045260246000fd5b356001600160a01b03811681036102e65790565b60405163095ea7b360e01b602082018181526001600160a01b0385166024840152604480840196909652948252949390926104be606485610426565b83516000926001600160a01b039291858416918591829182855af1906104e26105a4565b82610572575b5081610567575b50156104ff575b50505050509050565b60405196602088015216602486015280604486015260448552608085019085821067ffffffffffffffff8311176105535750610548939461054391604052826105fc565b6105fc565b8038808080806104f6565b634e487b7160e01b81526041600452602490fd5b90503b1515386104ef565b8051919250811591821561058a575b505090386104e8565b61059d92506020809183010191016105e4565b3880610581565b3d156105df573d9067ffffffffffffffff821161041057604051916105d3601f8201601f191660200184610426565b82523d6000602084013e565b606090565b908160209103126102e6575180151581036102e65790565b60408051908101916001600160a01b031667ffffffffffffffff8311828410176104105761066c926040526000806020958685527f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c656487860152868151910182855af16106666105a4565b916106f4565b8051908282159283156106dc575b505050156106855750565b6084906040519062461bcd60e51b82526004820152602a60248201527f5361666545524332303a204552433230206f7065726174696f6e20646964206e6044820152691bdd081cdd58d8d9595960b21b6064820152fd5b6106ec93508201810191016105e4565b38828161067a565b919290156107565750815115610708575090565b3b156107115790565b60405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152606490fd5b8251909150156107695750805190602001fd5b6040519062461bcd60e51b82528160208060048301528251908160248401526000935b8285106107af575050604492506000838284010152601f80199101168101030190fd5b848101820151868601604401529381019385935061078c56fea264697066735822122065151e6cccce6828ff0901f46ab142cb8aa214fc37379817e3635a556dd638a564736f6c63430008170033000000000000],
              to: ~h[0x2926631647877E9A84BB7E3A0821D643BF8D63C0],
              value: 0
            },
            block_hash: nil,
            block_number: nil,
            gas_used: 4298,
            error: nil,
            output: ~h[0x00000000000000000000000079EDBC4F3A6AA2266CD469CC544501743BE8B078],
            result_code: nil,
            result_address: nil,
            subtraces: 0,
            trace_address: [],
            transaction_hash: nil,
            transaction_position: nil,
            type: "call"
          }
        ],
        vm_trace: nil
      },
      %Onchain.TraceCall{
        output: <<130, 180, 41, 0>>,
        state_diff: nil,
        trace: [
          %Onchain.RPC.Trace{
            action: %Onchain.RPC.Trace.Action{
              call_type: "call",
              init: nil,
              from: <<0::160>>,
              gas: 499_977_072,
              input: ~h[0xdd560874000000000000000000000000000000000000000000000000000000000000000400000000000000000000000079edbc4f3a6aa2266cd469cc544501743be8b078000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000640c0a769b000000000000000000000000aec1f48e02cfb822be958b68c7957156eb3f0b6e0000000000000000000000001c7d4b196cb0c7b01d743fbc6116a902379c723800000000000000000000000000000000000000000000000000000000000f429000000000000000000000000000000000000000000000000000000000],
              to: ~h[0x6E995746B61C48C5BDF58FC788B1AEA08DFB7E43],
              value: 0
            },
            block_hash: nil,
            block_number: nil,
            gas_used: 4202,
            error: "Reverted",
            output: ~h[0x82B42900],
            result_code: nil,
            result_address: nil,
            subtraces: 1,
            trace_address: [],
            transaction_hash: nil,
            transaction_position: nil,
            type: "call"
          },
          %Onchain.RPC.Trace{
            action: %Onchain.RPC.Trace.Action{
              call_type: "delegatecall",
              init: nil,
              from: ~h[0x6E995746B61C48C5BDF58FC788B1AEA08DFB7E43],
              gas: 492_162_171,
              input: ~h[0xdd560874000000000000000000000000000000000000000000000000000000000000000400000000000000000000000079edbc4f3a6aa2266cd469cc544501743be8b078000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000640c0a769b000000000000000000000000aec1f48e02cfb822be958b68c7957156eb3f0b6e0000000000000000000000001c7d4b196cb0c7b01d743fbc6116a902379c723800000000000000000000000000000000000000000000000000000000000f429000000000000000000000000000000000000000000000000000000000],
              to: ~h[0x49E5D261E95F6A02505078BB339FECB210A0B634],
              value: 0
            },
            block_hash: nil,
            block_number: nil,
            gas_used: 1362,
            error: "Reverted",
            output: <<130, 180, 41, 0>>,
            result_code: nil,
            result_address: nil,
            subtraces: 1,
            trace_address: [0],
            transaction_hash: nil,
            transaction_position: nil,
            type: "call"
          },
          %Onchain.RPC.Trace{
            action: %Onchain.RPC.Trace.Action{
              call_type: "staticcall",
              init: nil,
              from: ~h[0x6E995746B61C48C5BDF58FC788B1AEA08DFB7E43],
              gas: 484_471_386,
              input: ~h[0xC34C08E5],
              to: ~h[0x6E995746B61C48C5BDF58FC788B1AEA08DFB7E43],
              value: 0
            },
            block_hash: nil,
            block_number: nil,
            gas_used: 190,
            error: nil,
            output: ~h[0x000000000000000000000000142DA9114E5A98E015AA95AFCA0585E84832A612],
            result_code: nil,
            result_address: nil,
            subtraces: 0,
            trace_address: [0, 0],
            transaction_hash: nil,
            transaction_position: nil,
            type: "call"
          }
        ],
        vm_trace: nil
      }
    ]}

# `trace_trx`

```elixir
@spec trace_trx(binary() | String.t(), Keyword.t()) ::
  {:ok, [Onchain.RPC.Trace.t()]} | {:error, term()}
```

RPC call to get a transaction receipt

## Examples

    iex> Onchain.RPC.trace_trx("0x85d995eba9763907fdf35cd2034144dd9d53ce32cbec21349d4b12823c6860c5")
    {:ok,
      [
      %Onchain.RPC.Trace{
        action: %Onchain.RPC.Trace.Action{
          call_type: "call",
          from: ~h[0x83806d539d4ea1c140489a06660319c9a303f874],
          gas: 0x01a1f8,
          input: <<>>,
          to: ~h[0x1c39ba39e4735cb65978d4db400ddd70a72dc750],
          value: 0x7a16c911b4d00000,
        },
        block_hash: ~h[0x7eb25504e4c202cf3d62fd585d3e238f592c780cca82dacb2ed3cb5b38883add],
        block_number: 3068185,
        gas_used: 0x2982,
        output: <<>>,
        subtraces: 2,
        trace_address: [~h[0x1c39ba39e4735cb65978d4db400ddd70a72dc750]],
        transaction_hash: ~h[0x17104ac9d3312d8c136b7f44d4b8b47852618065ebfa534bd2d3b5ef218ca1f3],
        transaction_position: 2,
        type: "call"
      },
      %Onchain.RPC.Trace{
        action: %Onchain.RPC.Trace.Action{
          call_type: "call",
          from: ~h[0x83806d539d4ea1c140489a06660319c9a303f874],
          gas: 0x01a1f8,
          input: <<>>,
          to: ~h[0x1c39ba39e4735cb65978d4db400ddd70a72dc750],
          value: 0x7a16c911b4d00000,
        },
        block_hash: ~h[0x7eb25504e4c202cf3d62fd585d3e238f592c780cca82dacb2ed3cb5b38883add],
        block_number: 3068186,
        gas_used: 0x2982,
        output: <<>>,
        subtraces: 2,
        trace_address: [~h[0x1c39ba39e4735cb65978d4db400ddd70a72dc750]],
        transaction_hash: ~h[0x17104ac9d3312d8c136b7f44d4b8b47852618065ebfa534bd2d3b5ef218ca1f3],
        transaction_position: 2,
        type: "call"
      }
    ]}

# `web3_client_version`

```elixir
@spec web3_client_version(Keyword.t()) :: {:ok, String.t()} | {:error, term()}
```

Returns the node client's version string (`web3_clientVersion`).

`web3_clientVersion` is not in `execution-apis` at any tag or on `main`.
EIP-1474 is the only definition.

Observed 2026-10-01, HTTP 200, no JSON-RPC refusal. Alchemy
(`ETHEREUM_ALCHEMY_URL`) returned
`"reth/v2.5.2-5a6940e/x86_64-unknown-linux-gnu"`. Infura
(`ETHEREUM_INFURA_URL`) returned
`"Geth/v1.17.6-stable-3d84c6b2/linux-arm64/go1.27.1"`.

A refusing endpoint's JSON-RPC error is classified by `send_rpc/3`, not by a
per-method probe: `{:error, {:method_not_found, map}}`,
`{:error, {:namespace_unavailable, map}}`, or
`{:error, {:unavailable, map}}`.

## Examples

    iex> Onchain.RPC.web3_client_version()
    {:ok, "TestClient/v1"}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
