Skip to content

feat(architecture): generic "jsonrpc" passthrough architecture for non-EVM JSON-RPC nodes - #2

Draft
snowkide wants to merge 4 commits into
mainfrom
feat/jsonrpc-passthrough-architecture
Draft

snowkide wants to merge 4 commits into
mainfrom
feat/jsonrpc-passthrough-architecture

Conversation

@snowkide

Copy link
Copy Markdown

eRPC serves EVM and SVM chains, each with a chain model (chain-id or genesis detection, state poller, method hooks). Many other chains expose a plain JSON-RPC API (Bitcoin-family nodes, Stellar RPC and others) and only need eRPC's generic pipeline: upstream selection, failsafe policies, rate limiting, multiplexing, metrics and healthchecks.

This adds a third architecture, jsonrpc, that provides exactly that and nothing chain-specific, plus two wire-level changes such servers need.

Commits (reviewable separately):

  1. fix(json-rpc): treat "error": null in upstream responses as success. JSON-RPC 1.0-style servers (Bitcoin Core and forks) always send "error": null; today every successful call to such a node is parsed as a server error and retried elsewhere.
  2. feat(architecture): the jsonrpc architecture.
    • architecture/jsonrpc registers a no-op ArchitectureHandler; errors fall through to the default extractor.
    • Networks: architecture: jsonrpc with jsonRpc.id (defaults to the alias); network id jsonrpc:<id>; inferred from a jsonRpc block; lazily creatable from /<project>/jsonrpc/<id>.
    • Upstreams: type: jsonrpc with jsonRpc.networkId (type inferred when set). Bootstrap sends nothing to the node. HTTP and WebSocket clients are reused; gRPC endpoints are rejected.
    • Requests are validated as JSON-RPC and forwarded unchanged; finality is unknown unless set via methods.definitions; no cache (no block/slot model).
    • Healthcheck scoping and all:activeUpstreams understand jsonrpc networks. TypeScript config types regenerated with tygo.
  3. feat(json-rpc): upstreams[].jsonRpc.omitEmptyParams. Leaves params out of outbound requests whose params are empty instead of sending "params":[]; params is optional in JSON-RPC 2.0 and some servers reject an empty array for methods without parameters. Defaults to true for type: jsonrpc upstreams and false otherwise, so the wire format of EVM and SVM upstreams does not change unless the setting is turned on. Cache keys are unchanged (hashed from parsed params).
  4. docs: config/projects/networks and config/projects/upstreams.
networks:
  - architecture: jsonrpc
    alias: btc-mainnet
upstreams:
  - endpoint: https://btc-rpc.example.com
    jsonRpc:
      networkId: btc-mainnet

Verification

Based on erpc/erpc main @ 094b912.

  • go build ./..., go vet ./... and gofmt -l on touched files are clean.
  • make test-fast (what CI runs): passes, exit 0, no failing test.
  • New tests for the setting: TestJsonRpcRequest_WireRequest, TestUpstreamConfig_SetDefaults_OmitEmptyParams (false for evm, true for jsonrpc, explicit opt-out respected), TestHttpJsonRpcClient_OmitEmptyParams (default, disabled, enabled). The passthrough end-to-end test checks that a jsonrpc upstream receives no params for an empty call.
  • TypeScript config types regenerated with tygo generate.

AI assistance

  • Model: anthropic/claude-opus-5.5
  • Harness / skills: Claude Code
  • Human-reviewed: no, pending

🤖 Generated with Claude Code

snowkide and others added 4 commits October 15, 2026 09:56
JSON-RPC 1.0-style servers (Bitcoin Core and its forks, among others)
always include the "error" member and set it to null on success. The
response parser handed the literal "null" to ParseError, whose fallback
turned it into a server-side exception, so every successful call to such
a node was reported as a failure and retried against other upstreams.

Treat a null error as the absence of an error, both in the streaming
parser and in ParseError (used by the batch and from-bytes paths).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tecture

eRPC currently serves EVM and SVM chains, each with a chain model (chain-id
or genesis detection, state poller, method hooks). Many other chains expose
a plain JSON-RPC API (Bitcoin-family nodes, Stellar RPC, and others) and
only need eRPC's generic pipeline: upstream selection, failsafe policies,
rate limiting, multiplexing, metrics and healthchecks.

Add a third architecture, "jsonrpc", that provides exactly that and nothing
chain-specific:

- architecture/jsonrpc registers a no-op ArchitectureHandler through the
  architecture registry; its error extractor claims nothing, so errors
  fall through to the default extractor (HTTP status + standard JSON-RPC
  codes).
- Networks: `architecture: jsonrpc` with `jsonRpc.id` (defaults to the
  alias); network id "jsonrpc:<id>". Inferred from a `jsonRpc` block,
  never receives networkDefaults.evm, and can be created lazily from the
  URL (/<project>/jsonrpc/<id>) like other architectures.
- Upstreams: `type: jsonrpc` with `jsonRpc.networkId` (type is inferred
  when networkId is set). Bootstrap only derives the network id from
  config; nothing is sent to the node. They reuse the generic HTTP and
  WebSocket JSON-RPC clients; gRPC endpoints are rejected.
- Requests are validated as JSON-RPC and forwarded unchanged (no
  normalization); finality is unknown unless set via methods.definitions,
  and no cache is wired since there is no block/slot model.
- Healthcheck scoping and all:activeUpstreams counting understand
  jsonrpc networks.
- TypeScript config types regenerated with tygo; NetworkArchitecture and
  UpstreamType unions extended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"params" is optional in JSON-RPC 2.0, and some non-EVM JSON-RPC servers
reject an empty positional array ("params":[]) for methods without
parameters while accepting the member being absent.

The new upstream setting jsonRpc.omitEmptyParams leaves the member out of
outbound requests whose params are empty, for the HTTP client (single and
batch) and the WebSocket client. It defaults to true for upstreams of type
jsonrpc and false otherwise, so the wire format of evm and svm upstreams
is unchanged unless the setting is turned on.

The request cache hash is computed from the parsed params, not from the
wire bytes, so cache keys are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Describe the architecture, the networks[].jsonRpc and
upstreams[].jsonRpc.networkId fields, type inference and the
behavioral differences from evm/svm (no detection, no cache, unknown
finality), with YAML/TypeScript examples.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant