Skip to content

feat(frontend): custom evm networks rpc provider#12498

Draft
sbpublic wants to merge 24 commits intomainfrom
feat/custom-evm-networks-provider
Draft

feat(frontend): custom evm networks rpc provider#12498
sbpublic wants to merge 24 commits intomainfrom
feat/custom-evm-networks-provider

Conversation

@sbpublic
Copy link
Copy Markdown
Collaborator

@sbpublic sbpublic commented Apr 20, 2026

Motivation

Stacked on top of #12497.

The custom EVM networks feature needs a runtime RPC path that does not depend on Infura/Alchemy, plus a safety check that rejects RPC endpoints whose eth_chainId does not match what the user entered (same behavior MetaMask applies on "Add network"). This PR adds both — pure runtime wiring, no UI yet.

Changes

  • Add CustomRpcProvider (src/frontend/src/eth/providers/custom-rpc.providers.ts) mirroring the method surface of InfuraProvider (balance, getFeeData, estimateGas / safeEstimateGas, sendTransaction, getTransactionCount, getBlockNumber), backed by ethers.JsonRpcProvider with a staticNetwork built from the user-supplied chainId.
  • Add customRpcProviders(network) factory with a (chainId, rpcUrl)-keyed cache so edits to either field produce a cache miss and stale providers never serve traffic.
  • Add verifyChainId({ rpcUrl, expectedChainId }) (src/frontend/src/eth/services/chain-id-verification.services.ts) returning a discriminated union (ok / mismatch / unreachable) so the UI can surface each failure mode distinctly.

Tests

Unit tests for CustomRpcProvider (constructor wiring, method delegation, safeEstimateGas success and error paths including analytics event, factory cache semantics) and for verifyChainId (match / mismatch / probe failure / constructor failure, including provider cleanup).

sbpublic and others added 9 commits April 20, 2026 12:55
Introduces the data contract for user-added EVM chains as the first
step toward MetaMask-style "Add custom network" support.

The `CustomEvmNetwork` type is kept separate from `EthereumNetwork`
because custom chains take a different runtime path (generic JSON-RPC,
no Infura/Alchemy, no CoinGecko/onramp integration). The `Evm` naming
reinforces the distinction at call sites.

Two shapes are defined: `CustomEvmNetwork` (runtime, with a branded
`NetworkId` symbol and `bigint` chainId) and `PersistedCustomEvmNetwork`
(localStorage-serializable, with a stringified chainId and no symbol).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Writable Svelte store backed by localStorage, exposing add / update /
remove / reset for user-added EVM networks. Serialization drops the
non-JSON `NetworkId` symbol and re-derives it deterministically from
`chainId` on load via a module-level cache, keeping symbol identity
stable across subscribers within a session.

Malformed or schema-incompatible persisted values fall back to an
empty list rather than throwing, matching the defensive posture of the
existing `initStorageStore` pattern.

`add` rejects duplicate chainIds; `update` throws when the chainId is
not present; `remove` is a no-op on missing entries.

Part of the MetaMask-style custom EVM networks feature (storage layer
only — UI and provider wiring come in follow-up PRs).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Covers initialization (empty / hydrated / malformed / schema-invalid),
stable `NetworkId` symbol derivation across reloads, and the full CRUD
surface (add / update / remove / reset) including duplicate rejection
and persistence shape.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Provider class mirroring the method surface of `InfuraProvider`
(balance, getFeeData, estimateGas / safeEstimateGas, sendTransaction,
getTransactionCount, getBlockNumber) but backed by a generic
`ethers.JsonRpcProvider` — no Infura/Alchemy integration.

The provider is constructed with `staticNetwork` from the user-supplied
`chainId`, avoiding an `eth_chainId` probe on every request; chain-ID
verification is done once at add time via a separate service
(introduced in a follow-up commit).

Also adds a module-level factory `customRpcProviders(network)` that
caches providers per `(chainId, rpcUrl)` pair, so edits to either field
produce a cache miss and stale providers never serve traffic.

Part of the MetaMask-style custom EVM networks feature (provider layer
only — UI wiring comes in a follow-up PR).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Covers constructor wiring (URL, static Network, chainId), delegation
of each method to the underlying `JsonRpcProvider`, `safeEstimateGas`
happy and error paths (including the analytics event emitted on
failure), and factory caching semantics — same `(chainId, rpcUrl)`
returns a cached instance, either field changing produces a fresh
provider.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Probes an RPC endpoint with `eth_chainId` (via ethers' `getNetwork`)
and compares against the value the user entered on the "Add network"
form. Mirrors the MetaMask safety check that prevents a phishing RPC
from masquerading as a known chain.

Returns a discriminated union (`ok` / `mismatch` / `unreachable`) so
the UI can surface each failure mode distinctly. A throwaway provider
is constructed without `staticNetwork` (so ethers actually issues the
probe) and destroyed in `finally` to release network resources.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Covers the three result variants (`ok`, `mismatch`, `unreachable`)
including the case where constructing the provider itself throws.
Also verifies the provider is destroyed when the probe completes and
is not created (so nothing to destroy) when construction fails.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@sbpublic sbpublic changed the title feat(frontend): custom evm networks provider feat(frontend): custom evm networks rpc provider Apr 20, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds runtime support for user-defined EVM networks by introducing a JSON-RPC-backed provider and an RPC safety check that validates eth_chainId against the user-entered chain ID, alongside the persisted custom-network contract/store needed to supply that runtime config.

Changes:

  • Added CustomRpcProvider + customRpcProviders(network) factory with (chainId, rpcUrl) caching for custom-network JSON-RPC calls.
  • Added verifyChainId({ rpcUrl, expectedChainId }) returning ok / mismatch / unreachable for UI-friendly RPC validation.
  • Introduced custom EVM network schema/types + localStorage-backed store, with unit tests for store/provider/service behavior.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/frontend/src/eth/providers/custom-rpc.providers.ts Implements JSON-RPC provider wrapper for custom chains and a cached factory.
src/frontend/src/eth/services/chain-id-verification.services.ts Adds RPC chain-id probe to prevent mismatched/phishing endpoints.
src/frontend/src/eth/schema/custom-network.schema.ts Defines runtime and persisted Zod schemas for custom EVM networks.
src/frontend/src/eth/types/custom-network.ts Exposes inferred TS types for custom network shapes.
src/frontend/src/eth/stores/custom-evm-networks.store.ts Adds localStorage-backed Svelte store for custom networks.
src/frontend/src/tests/eth/providers/custom-rpc.providers.spec.ts Unit tests for provider wiring, delegation, safeEstimateGas, and cache behavior.
src/frontend/src/tests/eth/services/chain-id-verification.services.spec.ts Unit tests for verifyChainId result modes and provider cleanup.
src/frontend/src/tests/eth/stores/custom-evm-networks.store.spec.ts Unit tests for store hydration/CRUD/persistence semantics.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/frontend/src/eth/stores/custom-evm-networks.store.ts
Comment thread src/frontend/src/eth/schema/custom-network.schema.ts Outdated
Comment thread src/frontend/src/eth/providers/custom-rpc.providers.ts
Comment thread src/frontend/src/eth/providers/custom-rpc.providers.ts
Comment thread src/frontend/src/eth/services/chain-id-verification.services.ts
Comment thread src/frontend/src/tests/eth/stores/custom-evm-networks.store.spec.ts
sbpublic and others added 13 commits April 20, 2026 13:47
Runtime schema requires a positive chainId but the persisted schema
previously accepted "0", letting invalid data sneak through hydration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…change

When the chainId is not present, the filter is a no-op; short-circuit so
we do not issue a redundant localStorage write on every removal attempt.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
loadFromStorage drops the entire persisted list when schema validation
fails, so an invalid write from add/update would wipe all custom networks
on the next reload. Run CustomEvmNetworkSchema.safeParse on the computed
entry and throw on failure, preserving the invariant that persisted
entries are always schema-valid.

Also align the add persist assertion with the remove one by including
the undefined iconUrl key in the expected payload.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
loadFromStorage previously returned all persisted entries verbatim. If
localStorage ever contained duplicate chainIds (manual edit, stale data
from an older buggy write) the resulting store would violate its
internal uniqueness invariant: update would only patch the first match,
remove would drop all matches, and add would reject the chainId
permanently. Dedupe deterministically (keep first) during hydration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
add previously computed and cached a NetworkId via toNetworkId before
schema validation. An invalid chainId (e.g. 0n) would throw after the
cache entry was populated, leaving a stale symbol in networkIdCache
forever. Add CustomEvmNetworkInputSchema (CustomEvmNetworkSchema without
the derived id) and run it against the raw input; only compute the
NetworkId once validation passes, so the cache never gains entries for
inputs that will be rejected.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Match the repo pattern (e.g. UtxosFeeStore, FeeRatePercentilesStore) by
having CustomEvmNetworksStore extend Readable<CustomEvmNetwork[]>. The
hand-rolled subscribe signature narrowed the type by omitting Svelte's
optional invalidate callback and made the store harder to pass to
helpers expecting a standard Readable.

Also document why add validates the pre-id input schema rather than the
full CustomEvmNetworkSchema: the derived id is deterministic given a
valid chainId, so a second pass would be redundant. The pre-id check
keeps the networkIdCache from accumulating entries for inputs that will
be rejected.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CustomEvmNetworkInput is already Omit<CustomEvmNetwork, 'id'>; the extra
Omit was a no-op that only obscured the expected shape.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…etworksStore

Two defense-in-depth changes on the store's write paths:

- add and update now build the stored entry from parsed.data rather than
  the raw input/merged object. Zod strips unknown keys by default, so
  parsed.data is the canonical schema shape; the prior code left any
  caller-supplied extra properties in the in-memory state.
- update explicitly pins id and chainId to the existing entry's values
  before validation. TypeScript already excludes these from
  CustomEvmNetworkPatch, but an `as any` caller could previously sneak
  them in; the schema would not catch an id that no longer corresponds
  to its chainId.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…istence

The generic UrlSchema permits the ipfs: protocol because it also
validates asset URLs (icons, metadata). An ipfs URL is not a valid
JSON-RPC endpoint, so the schema previously accepted values that would
inevitably fail at the first eth_* call. Add a dedicated RpcUrlSchema
that only allows https:/wss: and use it for rpcUrl in both the runtime
and persisted schemas. Other URL fields (explorerUrl, iconUrl) keep the
more permissive UrlSchema.

Also extend the update CRUD test to assert the modified list is
persisted to localStorage, not just mutated in memory.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…protocols

The docstring and test name referred to "ws(s)" but the schema only
allows wss:. Plain ws: is deliberately rejected (matching the existing
no-plain-http policy); update the wording to reflect that rather than
loosen the schema.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@sbpublic
Copy link
Copy Markdown
Collaborator Author

Recommendation: land (c)+(d) together (they're the same "cache lifecycle" story), and (e) separately as a small safety fix.

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.

2 participants