Skip to content

chore(e2e): make the harness portable to any EVM chain in DEFAULT_STABLECOINS#2250

Open
ryanRfox wants to merge 1 commit intox402-foundation:mainfrom
ryanRfox:chore/e2e-portable-default-stablecoins
Open

chore(e2e): make the harness portable to any EVM chain in DEFAULT_STABLECOINS#2250
ryanRfox wants to merge 1 commit intox402-foundation:mainfrom
ryanRfox:chore/e2e-portable-default-stablecoins

Conversation

@ryanRfox
Copy link
Copy Markdown
Contributor

@ryanRfox ryanRfox commented May 8, 2026

Closes #2249

Summary

Makes the e2e harness portable to any EVM chain in the SDK's DEFAULT_STABLECOINS catalog. The visible knob is --evm-network=<caip2>; the rest of the PR is the plumbing that makes the knob actually work.

Four interlocking pieces:

  1. CLI override--evm-network=<caip2> selects the EVM chain for a run. Mode-default behavior preserved when the flag is omitted.
  2. SDK-imported chain registryEVM_NETWORK_CONFIGS is derived from DEFAULT_STABLECOINS at module load, replacing the prior 10-entry hand-curated table. New SDK chains propagate after pnpm install with no harness edit.
  3. Removes Base Sepolia hardcodes — EIP-712 token names in the four EVM resource servers (express, fastify, hono, next/proxy) move to a per-server lookup keyed by EVM_NETWORK. The Permit2 approval script reads its target from the resolved network config. The mock facilitator advertises the full scheme set (exact, upto, batch-settlement) and gates non-EVM kinds on env presence.
  4. EVM-only credential gating — an EVM-only run (--families=evm) no longer requires Solana, Aptos, Hedera, or Stellar wallets. The harness's existing requiredEnvByFamily post-filter check (test.ts line ~660) handles this uniformly across all five families; we removed an unconditional EVM+SVM gate that fired before scenario filtering and over-required both families regardless of --families.

Why

Without (2), the override in (1) has nothing to resolve to. Without (3), selecting a non-Base chain produces wrong EIP-712 signatures, wrong Permit2 setup targets, and incomplete scheme advertisement. Without (4), an EVM-only contributor must set up a Solana wallet they may never use, which raises the entry bar for chain-specific testing.

Concrete consequences this addresses:

  1. Chain-specific permit2 personalities — chains whose default asset specifies assetTransferMethod === "permit2" (tokens that ship without EIP-3009) — are unreachable today. Their unique EIP-712 domains, permit signatures, and contract behaviors get no runtime signal. (Note: the harness's permit2 endpoints are exercised on Base Sepolia today because USDC supports both methods; what's missing is the chain-specific personality.)
  2. Adding a new SDK chain requires harness source edits. The harness drifts from the SDK's chain catalog over time.
  3. Cross-chain regressions are invisible — a SDK change that breaks one chain or assetTransferMethod but not Base Sepolia passes the harness without signal.

After this PR, the harness exercises any chain in DEFAULT_STABLECOINS (including new additions like Radius Network from #2038) through one --evm-network=<caip2> flag, with no harness source edits.

What's in this PR

1. Networks core (e2e/src/networks/networks.ts)

The centerpiece. The hand-curated EVM_NETWORK_CONFIGS table is replaced with Object.fromEntries(Object.keys(DEFAULT_STABLECOINS).map(...)) — a runtime view of the SDK's chain catalog. permit2Asset for any caip2 lookup flows from DEFAULT_STABLECOINS[caip2].address. EVM_NETWORK_CONFIGS is exported so callers can introspect the supported chains.

resolveViemChain(caip2) looks up viem's chain database; for chains viem hasn't packaged it falls through to defineChain({ name: 'EVM <id>', rpcUrls: { default: { http: [] } } }) and the caller supplies EVM_RPC_URL. With the viem floor at ^2.48.11, every chain currently in DEFAULT_STABLECOINS is in viem's catalog, so the fallback is for forward-compatibility with future SDK chains that out-pace viem.

evmRpcUrl(caip2) resolves in three tiers: EVM_RPC_URL env override → viem chain default → ''. getNetworkSet(mode, evmCaip2?) accepts an optional CAIP-2 EVM override that overlays the mode default's EVM slot from EVM_NETWORK_CONFIGS. The mode-default NETWORK_SETS rows for Base / Base Sepolia retain their inline addresses — out-of-scope consolidation candidate.

2. CLI + harness wiring (e2e/src/cli/args.ts, e2e/test.ts)

  • --evm-network=<caip2> parsed in parseArgs() as evmNetwork?: string.
  • runTest() passes parsedArgs.evmNetwork into getNetworkSet(mode, ...).
  • getEvmClients() replaces evmNetwork === 'eip155:8453' ? base : baseSepolia
    with resolveViemChain(evmNetwork).
  • Removed an unconditional EVM+SVM env-vars check that fired before scenario
    filtering and over-required both families regardless of --families. The
    existing requiredEnvByFamily table-driven check (same file, line ~660)
    already handles family-conditional credentials uniformly across all five
    protocol families — --families=evm runs no longer require Solana wallets.

3. EVM resource servers

Each EVM-payable server constructs an EIP-712 payment domain. All four sites
previously embedded the binary EVM_NETWORK == "eip155:84532" ? "USDC" : "USD Coin"
ternary at multiple call points. Replaced with a per-server
EVM_PERMIT2_ASSET_NAMES: Record<string, string> lookup keyed by EVM_NETWORK,
with comments indicating where to add entries when new chains land.

  • e2e/servers/express/index.ts
  • e2e/servers/fastify/index.ts
  • e2e/servers/hono/index.ts
  • e2e/servers/next/proxy.ts
  • e2e/servers/mcp-typescript/index.ts — eip155:* network-prefix widening only

The exported resolveViemChain from networks.ts is consumed by e2e/test.ts.
The clients, the facilitator, and the Permit2 script keep their own local
chain-resolution helpers (each spawned process is its own pnpm sub-package and
imports from e2e/src/... cross workspace boundaries). All local helpers
mirror resolveViemChain's viem-DB lookup with a minimal defineChain fallback.

4. EVM clients

Each client previously selected viem/chains base vs baseSepolia via
evmNetwork === 'eip155:8453' ? base : baseSepolia. Replaced with a local
resolveEvmChain(network) helper that walks viem's chain database and falls
back to a minimal defineChain for chains viem hasn't yet packaged, so any EVM
chain in DEFAULT_STABLECOINS works.

  • e2e/clients/fetch/index.ts
  • e2e/clients/axios/index.ts
  • e2e/clients/mcp-typescript/index.ts — eip155:* network-prefix widening only

5. Facilitator + Permit2 + mock + generic-server

  • e2e/facilitators/typescript/index.ts — local getEvmChain(network) helper
    for createPublicClient (mirrors resolveViemChain's lookup pattern). SVM
    signer initialization is now lazy, gated on SVM_PRIVATE_KEY presence so
    EVM-only runs don't require it. Scheme registration is family-conditional.
  • e2e/scripts/permit2-approval.ts — reads target token from
    EVM_PERMIT2_ASSET (set by generic-server.ts from the resolved
    network config) with [tokenAddress] CLI override. Works on any chain
    in DEFAULT_STABLECOINS.
  • e2e/mock-facilitator/index.ts — advertises batch-settlement alongside
    exact/upto. Non-EVM kinds are gated on env presence so EVM-only runs
    see a coherent /supported response (matches the real TS facilitator).
  • e2e/src/servers/generic-server.ts — wires EVM_NETWORK, EVM_RPC_URL,
    and EVM_PERMIT2_ASSET into spawned server processes from the resolved
    NetworkConfig.

6. Package wiring + README

  • e2e/package.json — adds "@x402/evm": "workspace:*" (was reachable
    transitively via the workspace, now declared at the e2e top-level package).
  • e2e/pnpm-lock.yaml — workspace link side-effect.
  • e2e/README.md — documents --evm-network=<caip2>, the EVM_RPC_URL
    single-override env var, the SDK-import auto-propagation, and the
    EVM-only credential gating.

Tests

  • pnpm install --frozen-lockfile clean across typescript/, examples/typescript/, and e2e/.
  • tsc --noEmit from e2e/ (after pnpm -C typescript build) introduces zero new errors vs upstream/main; the 51 pre-existing top-level-await / missing-route-import / implicit-any errors are unchanged.
  • Module-load introspection of EVM_NETWORK_CONFIGS: 12 keys, names sourced from viem's chain database, RPC URLs populated for every chain in DEFAULT_STABLECOINS. New SDK chains propagate after pnpm install with no harness source edit.
  • On-chain harness slice on Base Sepolia (--testnet --families=evm --facilitators=typescript --servers=express --clients=fetch --min): 11/11 scenarios pass, exit 0, 28 settlement tx hashes status=0x1 via paced JSON-RPC (1.2 s sleep, sequential, no sampling). Run with no SVM env vars set — confirms requiredEnvByFamily correctly skips SVM credentials on --families=evm.
  • On-chain harness slice against a non-Base EVM chain via --evm-network=<caip2> --families=evm: the chain-pluggable path exercises end-to-end with EVM-only credentials. Zero Missing required environment errors reference any SVM variable.
  • viem floor is ^2.48.11; e2e/ lockfile resolves direct viem to 2.48.11. The complementary monorepo-wide floor bump is in chore: bump viem floor to ^2.48.11 across monorepo + refresh lockfiles #2242.

Migration

CI configurations that set per-chain RPC URL env vars (e.g.,
BASE_SEPOLIA_RPC_URL) must migrate to the unified EVM_RPC_URL. The harness
selects the chain via --evm-network=<caip2> (or the mode default) and reads
RPC override from EVM_RPC_URL only.

No impact on SDK consumers using the published packages.

Follow-up (not in this PR)

  • Codified drift-assertion test for the SDK-import (currently verified manually via module-load introspection).
  • Mock facilitator non-EVM kind parity with the real TS facilitator (advertised schemes are now in sync; non-EVM kind coverage may still diverge for AVM/Hedera/Stellar).
  • Consolidate the inline Base / Base Sepolia rows in NETWORK_SETS to derive from EVM_NETWORK_CONFIGS for full SDK-import symmetry.

Checklist

  • I have formatted and linted my code
  • All new and existing tests pass
  • My commits are signed (required for merge) -- you may need to rebase if you initially pushed unsigned commits
  • I added a changelog fragment for user-facing changes (docs-only changes can skip) — internal refactor, no @x402/* runtime API surface changes

AI disclosure

This PR used an agentic coding workflow and was reviewed by Ryan R. Fox (an actual human) before posting.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 8, 2026

@ryanRfox is attempting to deploy a commit to the Coinbase Team on Vercel.

A member of the Team first needs to authorize it.

@ryanRfox ryanRfox force-pushed the chore/e2e-portable-default-stablecoins branch 4 times, most recently from 0387732 to 26c331b Compare May 8, 2026 19:45
…BLECOINS

Makes the e2e harness portable to any EVM chain in the SDK's
DEFAULT_STABLECOINS catalog. The visible knob is --evm-network=<caip2>;
the rest is the plumbing that makes the knob actually work.

Four interlocking pieces:

1. CLI override - --evm-network=<caip2> selects the EVM chain for a run.
   Mode-default behavior preserved when the flag is omitted.

2. SDK-imported chain registry - EVM_NETWORK_CONFIGS is derived from
   DEFAULT_STABLECOINS at module load via Object.fromEntries(...), replacing
   the prior 10-entry hand-curated table. New SDK chains propagate after
   pnpm install with no harness edit.

3. Removes Base Sepolia hardcodes - EIP-712 token names in the four EVM
   resource servers (express, fastify, hono, next/proxy) move to a per-server
   EVM_PERMIT2_ASSET_NAMES lookup keyed by EVM_NETWORK. The Permit2 approval
   script reads its target from the resolved network config. The mock
   facilitator advertises the full scheme set (exact, upto, batch-settlement)
   and gates non-EVM kinds on env presence.

4. EVM-only credential gating - an EVM-only run (--families=evm) no longer
   requires Solana, Aptos, Hedera, or Stellar wallets. The harness's
   existing requiredEnvByFamily post-filter check (test.ts line ~660)
   handles this uniformly across all five families; the unconditional
   EVM+SVM gate that fired before scenario filtering and over-required both
   families is removed.

resolveViemChain(caip2) looks up viem's chain database; for chains viem
hasn't packaged it falls through to defineChain({ name: 'EVM <id>',
rpcUrls: { default: { http: [] } } }) and the caller supplies EVM_RPC_URL.
With the viem floor at ^2.48.11, every chain currently in DEFAULT_STABLECOINS
is in viem's catalog, so the fallback is for forward-compatibility with
future SDK chains that out-pace viem.

evmRpcUrl(caip2) resolves in three tiers: EVM_RPC_URL env override -> viem
chain default -> ''. getNetworkSet(mode, evmCaip2?) accepts an optional
CAIP-2 EVM override that overlays the mode default's EVM slot from
EVM_NETWORK_CONFIGS.

Migration: CI configurations that set per-chain RPC URL env vars (e.g.,
BASE_SEPOLIA_RPC_URL) must migrate to the unified EVM_RPC_URL. The harness
selects the chain via --evm-network=<caip2> (or the mode default) and reads
RPC override from EVM_RPC_URL only. No impact on SDK consumers using the
published packages.

The complementary monorepo-wide viem floor bump is in x402-foundation#2242.
@ryanRfox ryanRfox force-pushed the chore/e2e-portable-default-stablecoins branch from 26c331b to 72d1195 Compare May 8, 2026 20:02
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.

Feature: Make the e2e harness portable to any EVM chain in DEFAULT_STABLECOINS

1 participant