Skip to content

Commit 26c331b

Browse files
committed
chore(e2e): make the harness portable to any EVM chain in DEFAULT_STABLECOINS
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 #2242.
1 parent ee7c156 commit 26c331b

18 files changed

Lines changed: 595 additions & 242 deletions

File tree

e2e/README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,27 @@ Add the `-v` flag to any command for verbose output:
8686

8787
Useful for debugging test failures or understanding the payment flow.
8888

89+
### Targeting a Specific EVM Chain
90+
91+
The harness defaults to Base Sepolia (`eip155:84532`) on `--testnet`. Pass
92+
`--evm-network=<caip2>` to target any chain in the SDK's `DEFAULT_STABLECOINS`
93+
catalog (`typescript/packages/mechanisms/evm/src/shared/defaultAssets.ts`).
94+
Combine with `--families=evm` to skip non-EVM credential requirements.
95+
96+
```bash
97+
# Run EVM-only against Mezo Testnet
98+
pnpm test --testnet --families=evm --evm-network=eip155:31611
99+
```
100+
101+
The harness derives its chain registry from `DEFAULT_STABLECOINS` at module load.
102+
Adding a new chain to the SDK propagates here after `pnpm install` — no harness
103+
source edit. Display names come from viem's chain database (`viem ^2.48.11` is
104+
the floor; SDK chains added before viem packages them fall back to `EVM <chainId>`).
105+
106+
EVM-only runs (`--families=evm`) do NOT require Solana, Aptos, Hedera, or
107+
Stellar credentials — the corresponding env vars are only consulted when their
108+
family is selected.
109+
89110
## Wallet Safety Warning
90111

91112
**Use dedicated test wallets only. Do NOT use wallets that hold real funds.**
@@ -153,6 +174,14 @@ TONAPI_API_KEY=<tonapi-key> \
153174
pnpm test --testnet --families=tvm --facilitators=python --clients=httpx,requests --servers=fastapi,flask --min -v
154175
```
155176

177+
Optional environment variables (chain selection):
178+
179+
```bash
180+
EVM_RPC_URL=https://... # Override the EVM RPC URL for the selected
181+
# chain. When unset, the harness uses
182+
# viem's default RPC for the chain.
183+
```
184+
156185
Optional environment variables (batch-settlement scheme):
157186

158187
```bash

e2e/clients/axios/index.ts

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { config } from "dotenv";
22
import axios from "axios";
33
import { wrapAxiosWithPayment, decodePaymentResponseHeader } from "@x402/axios";
4-
import { createPublicClient, http } from "viem";
4+
import { createPublicClient, defineChain, http, type Chain } from "viem";
55
import { privateKeyToAccount } from "viem/accounts";
6-
import { base, baseSepolia } from "viem/chains";
6+
import * as allViemChains from "viem/chains";
77
import { ExactEvmScheme, type ExactEvmSchemeOptions } from "@x402/evm/exact/client";
88
import {
99
UptoEvmScheme as UptoEvmClientScheme,
@@ -32,13 +32,39 @@ const baseURL = process.env.RESOURCE_SERVER_URL as string;
3232
const endpointPath = process.env.ENDPOINT_PATH as string;
3333
const url = `${baseURL}${endpointPath}`;
3434
const evmAccount = privateKeyToAccount(process.env.EVM_PRIVATE_KEY as `0x${string}`);
35-
const svmSigner = await createKeyPairSignerFromBytes(
36-
base58.decode(process.env.SVM_PRIVATE_KEY as string),
37-
);
35+
// Lazy SVM signer: only decoded when SVM_PRIVATE_KEY is set so EVM-only runs
36+
// (e.g. --families=evm) don't require a Solana key.
37+
const svmSigner = process.env.SVM_PRIVATE_KEY
38+
? await createKeyPairSignerFromBytes(
39+
base58.decode(process.env.SVM_PRIVATE_KEY),
40+
)
41+
: undefined;
3842

3943
const evmNetwork = process.env.EVM_NETWORK || "eip155:84532";
4044
const evmRpcUrl = process.env.EVM_RPC_URL;
41-
const evmChain = evmNetwork === "eip155:8453" ? base : baseSepolia;
45+
// Resolve any CAIP-2 EVM chain — viem's chain database first, with a
46+
// minimal defineChain fallback for any SDK chain that viem hasn't packaged yet.
47+
function resolveEvmChain(network: string): Chain {
48+
const [namespace, ref] = network.split(":");
49+
if (namespace !== "eip155") {
50+
throw new Error(`resolveEvmChain: not an EVM network: ${network}`);
51+
}
52+
const chainId = Number(ref);
53+
if (!Number.isInteger(chainId) || chainId <= 0) {
54+
throw new Error(`resolveEvmChain: invalid EVM chain id in ${network}`);
55+
}
56+
const known = (Object.values(allViemChains) as Chain[]).find(
57+
(c) => c && typeof c === "object" && c.id === chainId,
58+
);
59+
if (known) return known;
60+
return defineChain({
61+
id: chainId,
62+
name: `EVM ${chainId}`,
63+
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
64+
rpcUrls: { default: { http: [] } },
65+
});
66+
}
67+
const evmChain = resolveEvmChain(evmNetwork);
4268

4369
const publicClient = createPublicClient({
4470
chain: evmChain,
@@ -113,10 +139,13 @@ const client = new x402Client()
113139
.register("eip155:*", new UptoEvmClientScheme(evmSigner, uptoSchemeOptions))
114140
.register("eip155:*", batchSettlementScheme)
115141
.registerV1("base-sepolia", new ExactEvmSchemeV1(evmSigner))
116-
.registerV1("base", new ExactEvmSchemeV1(evmSigner))
117-
.register("solana:*", new ExactSvmScheme(svmSigner))
118-
.registerV1("solana-devnet", new ExactSvmSchemeV1(svmSigner))
119-
.registerV1("solana", new ExactSvmSchemeV1(svmSigner));
142+
.registerV1("base", new ExactEvmSchemeV1(evmSigner));
143+
if (svmSigner) {
144+
client
145+
.register("solana:*", new ExactSvmScheme(svmSigner))
146+
.registerV1("solana-devnet", new ExactSvmSchemeV1(svmSigner))
147+
.registerV1("solana", new ExactSvmSchemeV1(svmSigner));
148+
}
120149
if (aptosAccount) {
121150
client.register("aptos:*", new ExactAptosScheme(aptosAccount));
122151
}

e2e/clients/fetch/index.ts

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { config } from "dotenv";
22
import { wrapFetchWithPayment } from "@x402/fetch";
3-
import { createPublicClient, http } from "viem";
3+
import { createPublicClient, defineChain, http, type Chain } from "viem";
44
import { privateKeyToAccount } from "viem/accounts";
5-
import { base, baseSepolia } from "viem/chains";
5+
import * as allViemChains from "viem/chains";
66
import { ExactEvmScheme, type ExactEvmSchemeOptions } from "@x402/evm/exact/client";
77
import {
88
UptoEvmScheme as UptoEvmClientScheme,
@@ -31,13 +31,39 @@ const baseURL = process.env.RESOURCE_SERVER_URL as string;
3131
const endpointPath = process.env.ENDPOINT_PATH as string;
3232
const url = `${baseURL}${endpointPath}`;
3333
const evmAccount = privateKeyToAccount(process.env.EVM_PRIVATE_KEY as `0x${string}`);
34-
const svmSigner = await createKeyPairSignerFromBytes(
35-
base58.decode(process.env.SVM_PRIVATE_KEY as string),
36-
);
34+
// Lazy SVM signer: only decoded when SVM_PRIVATE_KEY is set so EVM-only runs
35+
// (e.g. --families=evm) don't require a Solana key.
36+
const svmSigner = process.env.SVM_PRIVATE_KEY
37+
? await createKeyPairSignerFromBytes(
38+
base58.decode(process.env.SVM_PRIVATE_KEY),
39+
)
40+
: undefined;
3741

3842
const evmNetwork = process.env.EVM_NETWORK || "eip155:84532";
3943
const evmRpcUrl = process.env.EVM_RPC_URL;
40-
const evmChain = evmNetwork === "eip155:8453" ? base : baseSepolia;
44+
// Resolve any CAIP-2 EVM chain — viem's chain database first, with a
45+
// minimal defineChain fallback for any SDK chain that viem hasn't packaged yet.
46+
function resolveEvmChain(network: string): Chain {
47+
const [namespace, ref] = network.split(":");
48+
if (namespace !== "eip155") {
49+
throw new Error(`resolveEvmChain: not an EVM network: ${network}`);
50+
}
51+
const chainId = Number(ref);
52+
if (!Number.isInteger(chainId) || chainId <= 0) {
53+
throw new Error(`resolveEvmChain: invalid EVM chain id in ${network}`);
54+
}
55+
const known = (Object.values(allViemChains) as Chain[]).find(
56+
(c) => c && typeof c === "object" && c.id === chainId,
57+
);
58+
if (known) return known;
59+
return defineChain({
60+
id: chainId,
61+
name: `EVM ${chainId}`,
62+
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
63+
rpcUrls: { default: { http: [] } },
64+
});
65+
}
66+
const evmChain = resolveEvmChain(evmNetwork);
4167

4268
const publicClient = createPublicClient({
4369
chain: evmChain,
@@ -112,10 +138,13 @@ const client = new x402Client()
112138
.register("eip155:*", new UptoEvmClientScheme(evmSigner, uptoSchemeOptions))
113139
.register("eip155:*", batchSettlementScheme)
114140
.registerV1("base-sepolia", new ExactEvmSchemeV1(evmSigner))
115-
.registerV1("base", new ExactEvmSchemeV1(evmSigner))
116-
.register("solana:*", new ExactSvmScheme(svmSigner))
117-
.registerV1("solana-devnet", new ExactSvmSchemeV1(svmSigner))
118-
.registerV1("solana", new ExactSvmSchemeV1(svmSigner));
141+
.registerV1("base", new ExactEvmSchemeV1(evmSigner));
142+
if (svmSigner) {
143+
client
144+
.register("solana:*", new ExactSvmScheme(svmSigner))
145+
.registerV1("solana-devnet", new ExactSvmSchemeV1(svmSigner))
146+
.registerV1("solana", new ExactSvmSchemeV1(svmSigner));
147+
}
119148
if (aptosAccount) {
120149
client.register("aptos:*", new ExactAptosScheme(aptosAccount));
121150
}

e2e/clients/mcp-typescript/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ interface E2EResult {
2323
const serverUrl = process.env.RESOURCE_SERVER_URL as string;
2424
const endpointPath = process.env.ENDPOINT_PATH as string; // tool name, e.g. "get_weather"
2525
const evmPrivateKey = process.env.EVM_PRIVATE_KEY as `0x${string}`;
26+
const evmNetwork = (process.env.EVM_NETWORK || "eip155:84532") as `${string}:${string}`;
2627

2728
if (!serverUrl || !endpointPath || !evmPrivateKey) {
2829
const result: E2EResult = {
@@ -42,7 +43,7 @@ async function main(): Promise<void> {
4243
const x402Mcp = createx402MCPClient({
4344
name: "x402-mcp-e2e-client",
4445
version: "1.0.0",
45-
schemes: [{ network: "eip155:84532", client: new ExactEvmScheme(evmSigner, evmSchemeOptions) }],
46+
schemes: [{ network: evmNetwork, client: new ExactEvmScheme(evmSigner, evmSchemeOptions) }],
4647
autoPayment: true,
4748
onPaymentRequested: async () => true, // Auto-approve all payments for e2e
4849
});

e2e/facilitators/typescript/index.ts

Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ import dotenv from "dotenv";
6666
import express from "express";
6767
import {
6868
createWalletClient,
69+
defineChain,
6970
http,
7071
nonceManager,
7172
publicActions,
@@ -74,7 +75,7 @@ import {
7475
recoverTransactionAddress,
7576
} from "viem";
7677
import { privateKeyToAccount } from "viem/accounts";
77-
import { baseSepolia, base } from "viem/chains";
78+
import * as allViemChains from "viem/chains";
7879
import { BazaarCatalog } from "./bazaar.js";
7980

8081
dotenv.config();
@@ -97,15 +98,29 @@ const APTOS_RPC_URL = process.env.APTOS_RPC_URL;
9798
const HEDERA_NODE_URL = process.env.HEDERA_NODE_URL;
9899
const STELLAR_RPC_URL = process.env.STELLAR_RPC_URL;
99100

100-
// Map CAIP-2 network IDs to viem chains
101+
// Resolve a CAIP-2 EVM network id to a viem `Chain`.
102+
// Uses viem's chain database when available; falls back to a minimal
103+
// defineChain so any SDK chain viem hasn't packaged yet still works
104+
// when the caller supplies their own RPC URL.
101105
function getEvmChain(network: string): Chain {
102-
switch (network) {
103-
case "eip155:8453":
104-
return base;
105-
case "eip155:84532":
106-
default:
107-
return baseSepolia;
106+
const [namespace, ref] = network.split(":");
107+
if (namespace !== "eip155") {
108+
throw new Error(`getEvmChain: not an EVM network: ${network}`);
108109
}
110+
const chainId = Number(ref);
111+
if (!Number.isInteger(chainId) || chainId <= 0) {
112+
throw new Error(`getEvmChain: invalid EVM chain id in ${network}`);
113+
}
114+
const known = (Object.values(allViemChains) as Chain[]).find(
115+
(c) => c && typeof c === "object" && c.id === chainId,
116+
);
117+
if (known) return known;
118+
return defineChain({
119+
id: chainId,
120+
name: `EVM ${chainId}`,
121+
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
122+
rpcUrls: { default: { http: [] } },
123+
});
109124
}
110125

111126
console.log(`🌐 EVM Network: ${EVM_NETWORK}`);
@@ -127,11 +142,6 @@ if (!process.env.EVM_PRIVATE_KEY) {
127142
process.exit(1);
128143
}
129144

130-
if (!process.env.SVM_PRIVATE_KEY) {
131-
console.error("❌ SVM_PRIVATE_KEY environment variable is required");
132-
process.exit(1);
133-
}
134-
135145
// Initialize the EVM account from private key
136146
const evmAccount = privateKeyToAccount(
137147
process.env.EVM_PRIVATE_KEY as `0x${string}`,
@@ -154,11 +164,17 @@ const authorizerSigner: AuthorizerSigner = {
154164
};
155165
console.info(`EVM Receiver Authorizer: ${authorizerSigner.address}`);
156166

157-
// Initialize the SVM account from private key
158-
const svmAccount = await createKeyPairSignerFromBytes(
159-
base58.decode(process.env.SVM_PRIVATE_KEY as string),
160-
);
161-
console.info(`SVM Facilitator account: ${svmAccount.address}`);
167+
// Initialize the SVM account from private key (optional)
168+
// Lazy-decoded so EVM-only runs (e.g. --families=evm) don't require an SVM key.
169+
let svmAccount:
170+
| Awaited<ReturnType<typeof createKeyPairSignerFromBytes>>
171+
| undefined;
172+
if (process.env.SVM_PRIVATE_KEY) {
173+
svmAccount = await createKeyPairSignerFromBytes(
174+
base58.decode(process.env.SVM_PRIVATE_KEY as string),
175+
);
176+
console.info(`SVM Facilitator account: ${svmAccount.address}`);
177+
}
162178

163179
// Initialize the Aptos account from private key (format to AIP-80 compliant format) if provided
164180
let aptosAccount: Account | undefined;
@@ -266,11 +282,13 @@ const evmSigner = toFacilitatorEvmSigner({
266282
});
267283

268284
// Facilitator can now handle all Solana networks with automatic RPC creation
269-
// Pass custom RPC URL if provided
270-
const svmSigner = toFacilitatorSvmSigner(
271-
svmAccount,
272-
SVM_RPC_URL ? { defaultRpcUrl: SVM_RPC_URL } : undefined,
273-
);
285+
// Pass custom RPC URL if provided. Skipped when no SVM key is configured.
286+
const svmSigner = svmAccount
287+
? toFacilitatorSvmSigner(
288+
svmAccount,
289+
SVM_RPC_URL ? { defaultRpcUrl: SVM_RPC_URL } : undefined,
290+
)
291+
: undefined;
274292

275293
// Facilitator can handle all Aptos networks with automatic RPC creation
276294
// Pass custom RPC URL if provided
@@ -422,9 +440,12 @@ facilitator
422440
EVM_NETWORK as Network,
423441
new BatchSettlementEvmScheme(evmSigner, authorizerSigner),
424442
)
425-
.registerV1(EVM_V1_NETWORKS as Network[], new ExactEvmSchemeV1(evmSigner))
426-
.register(SVM_NETWORK as Network, new ExactSvmScheme(svmSigner))
427-
.registerV1(SVM_V1_NETWORKS as Network[], new ExactSvmSchemeV1(svmSigner));
443+
.registerV1(EVM_V1_NETWORKS as Network[], new ExactEvmSchemeV1(evmSigner));
444+
if (svmSigner) {
445+
facilitator
446+
.register(SVM_NETWORK as Network, new ExactSvmScheme(svmSigner))
447+
.registerV1(SVM_V1_NETWORKS as Network[], new ExactSvmSchemeV1(svmSigner));
448+
}
428449
if (avmSigner) {
429450
facilitator.register(AVM_NETWORK as Network, new ExactAvmScheme(avmSigner));
430451
}
@@ -756,7 +777,7 @@ app.get("/health", (req, res) => {
756777
res.json({
757778
status: "ok",
758779
evmNetwork: EVM_NETWORK,
759-
svmNetwork: SVM_NETWORK,
780+
svmNetwork: svmSigner ? SVM_NETWORK : "(not configured)",
760781
avmNetwork: avmSigner ? AVM_NETWORK : "(not configured)",
761782
aptosNetwork: aptosAccount ? APTOS_NETWORK : "(not configured)",
762783
hederaNetwork: hederaSigner ? HEDERA_NETWORK : "(not configured)",

e2e/mock-facilitator/index.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import http from "node:http";
1212

1313
const PORT = parseInt(process.env.PORT || "4099", 10);
1414
const EVM_NETWORK = process.env.EVM_NETWORK || "eip155:84532";
15-
const SVM_NETWORK = process.env.SVM_NETWORK || "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1";
16-
const APTOS_NETWORK = process.env.APTOS_NETWORK || "aptos:2";
17-
const STELLAR_NETWORK = process.env.STELLAR_NETWORK || "stellar:testnet";
15+
const SVM_NETWORK = process.env.SVM_NETWORK;
16+
const APTOS_NETWORK = process.env.APTOS_NETWORK;
17+
const STELLAR_NETWORK = process.env.STELLAR_NETWORK;
1818

1919
const DUMMY_EVM_SIGNER = "0x0000000000000000000000000000000000000001";
2020
const DUMMY_SVM_SIGNER = "11111111111111111111111111111111";
@@ -23,7 +23,7 @@ const DUMMY_APTOS_SIGNER =
2323
const DUMMY_STELLAR_SIGNER = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";
2424

2525
function buildSupportedResponse() {
26-
const evmSchemes = ["exact", "upto"];
26+
const evmSchemes = ["exact", "upto", "batch-settlement"];
2727
const otherSchemes = ["exact"];
2828
const versions = [1, 2];
2929

@@ -37,8 +37,10 @@ function buildSupportedResponse() {
3737
for (const scheme of evmSchemes) {
3838
kinds.push({ x402Version: version, scheme, network: EVM_NETWORK });
3939
}
40-
for (const scheme of otherSchemes) {
41-
kinds.push({ x402Version: version, scheme, network: SVM_NETWORK });
40+
if (SVM_NETWORK) {
41+
for (const scheme of otherSchemes) {
42+
kinds.push({ x402Version: version, scheme, network: SVM_NETWORK });
43+
}
4244
}
4345
if (APTOS_NETWORK) {
4446
for (const scheme of otherSchemes) {
@@ -54,8 +56,10 @@ function buildSupportedResponse() {
5456

5557
const signers: Record<string, string[]> = {
5658
"eip155:*": [DUMMY_EVM_SIGNER],
57-
"solana:*": [DUMMY_SVM_SIGNER],
5859
};
60+
if (SVM_NETWORK) {
61+
signers["solana:*"] = [DUMMY_SVM_SIGNER];
62+
}
5963
if (APTOS_NETWORK) {
6064
signers["aptos:*"] = [DUMMY_APTOS_SIGNER];
6165
}

e2e/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"permit2:revoke": "tsx scripts/permit2-approval.ts revoke"
1717
},
1818
"dependencies": {
19+
"@x402/evm": "workspace:*",
1920
"axios": "^1.6.0",
2021
"dotenv": "^17.0.1",
2122
"express": "^4.18.0",

0 commit comments

Comments
 (0)