Skip to content

Commit 1884274

Browse files
authored
fix(evm v1): fix missing EVM network support and add explicit errors for unsupported networks (x402-foundation#818)
1 parent e61de93 commit 1884274

7 files changed

Lines changed: 149 additions & 32 deletions

File tree

typescript/packages/mechanisms/evm/src/exact/v1/client/scheme.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { authorizationTypes } from "../../../constants";
1010
import { ClientEvmSigner } from "../../../signer";
1111
import { ExactEvmPayloadV1 } from "../../../types";
1212
import { createNonce, getEvmChainId } from "../../../utils";
13+
import { EvmNetworkV1 } from "../../../v1";
1314

1415
/**
1516
* EVM client implementation for the Exact payment scheme (V1).
@@ -77,7 +78,7 @@ export class ExactEvmSchemeV1 implements SchemeNetworkClient {
7778
authorization: ExactEvmPayloadV1["authorization"],
7879
requirements: PaymentRequirementsV1,
7980
): Promise<`0x${string}`> {
80-
const chainId = getEvmChainId(requirements.network);
81+
const chainId = getEvmChainId(requirements.network as EvmNetworkV1);
8182

8283
if (!requirements.extra?.name || !requirements.extra?.version) {
8384
throw new Error(

typescript/packages/mechanisms/evm/src/exact/v1/facilitator/scheme.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { authorizationTypes, eip3009ABI } from "../../../constants";
1212
import { FacilitatorEvmSigner } from "../../../signer";
1313
import { ExactEvmPayloadV1 } from "../../../types";
1414
import { getEvmChainId } from "../../../utils";
15+
import { EvmNetworkV1 } from "../../../v1";
1516

1617
export interface ExactEvmSchemeV1Config {
1718
/**
@@ -93,7 +94,16 @@ export class ExactEvmSchemeV1 implements SchemeNetworkFacilitator {
9394
}
9495

9596
// Get chain configuration
96-
const chainId = getEvmChainId(payloadV1.network);
97+
let chainId: number;
98+
try {
99+
chainId = getEvmChainId(payloadV1.network as EvmNetworkV1);
100+
} catch {
101+
return {
102+
isValid: false,
103+
invalidReason: `invalid_network`,
104+
payer: exactEvmPayload.authorization.from,
105+
};
106+
}
97107

98108
if (!requirements.extra?.name || !requirements.extra?.version) {
99109
return {

typescript/packages/mechanisms/evm/src/utils.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,20 @@
11
import { toHex } from "viem";
2-
import { Network } from "@x402/core/types";
2+
import { EVM_NETWORK_CHAIN_ID_MAP, EvmNetworkV1 } from "./v1";
33

44
/**
55
* Extract chain ID from network string (e.g., "base-sepolia" -> 84532)
66
* Used by v1 implementations
77
*
88
* @param network - The network identifier
99
* @returns The numeric chain ID
10+
* @throws Error if the network is not supported
1011
*/
11-
export function getEvmChainId(network: Network): number {
12-
const networkMap: Record<string, number> = {
13-
base: 8453,
14-
"base-sepolia": 84532,
15-
ethereum: 1,
16-
sepolia: 11155111,
17-
polygon: 137,
18-
"polygon-amoy": 80002,
19-
};
20-
return networkMap[network] || 1;
12+
export function getEvmChainId(network: EvmNetworkV1): number {
13+
const chainId = EVM_NETWORK_CHAIN_ID_MAP[network];
14+
if (!chainId) {
15+
throw new Error(`Unsupported network: ${network}`);
16+
}
17+
return chainId;
2118
}
2219

2320
/**
Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
11
export { ExactEvmSchemeV1 } from "../exact/v1";
22

3-
export const NETWORKS: string[] = [
4-
"abstract",
5-
"abstract-testnet",
6-
"base-sepolia",
7-
"base",
8-
"avalanche-fuji",
9-
"avalanche",
10-
"iotex",
11-
"sei",
12-
"sei-testnet",
13-
"polygon",
14-
"polygon-amoy",
15-
"peaq",
16-
"story",
17-
"educhain",
18-
"skale-base-sepolia",
19-
];
3+
export const EVM_NETWORK_CHAIN_ID_MAP = {
4+
ethereum: 1,
5+
sepolia: 11155111,
6+
abstract: 2741,
7+
"abstract-testnet": 11124,
8+
"base-sepolia": 84532,
9+
base: 8453,
10+
"avalanche-fuji": 43113,
11+
avalanche: 43114,
12+
iotex: 4689,
13+
sei: 1329,
14+
"sei-testnet": 1328,
15+
polygon: 137,
16+
"polygon-amoy": 80002,
17+
peaq: 3338,
18+
story: 1514,
19+
educhain: 41923,
20+
"skale-base-sepolia": 324705682,
21+
} as const;
22+
23+
export type EvmNetworkV1 = keyof typeof EVM_NETWORK_CHAIN_ID_MAP;
24+
25+
export const NETWORKS: string[] = Object.keys(EVM_NETWORK_CHAIN_ID_MAP);

typescript/packages/mechanisms/evm/test/unit/utils.test.ts

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, it, expect } from "vitest";
22
import { getEvmChainId, createNonce } from "../../src/utils";
3+
import { EvmNetworkV1 } from "../../src/v1";
34

45
describe("EVM Utils", () => {
56
describe("getEvmChainId", () => {
@@ -27,8 +28,54 @@ describe("EVM Utils", () => {
2728
expect(getEvmChainId("polygon-amoy")).toBe(80002);
2829
});
2930

30-
it("should return default chain ID (1) for unknown networks", () => {
31-
expect(getEvmChainId("unknown-network")).toBe(1);
31+
it("should return correct chain ID for Abstract", () => {
32+
expect(getEvmChainId("abstract")).toBe(2741);
33+
});
34+
35+
it("should return correct chain ID for Abstract Testnet", () => {
36+
expect(getEvmChainId("abstract-testnet")).toBe(11124);
37+
});
38+
39+
it("should return correct chain ID for Avalanche Fuji", () => {
40+
expect(getEvmChainId("avalanche-fuji")).toBe(43113);
41+
});
42+
43+
it("should return correct chain ID for Avalanche", () => {
44+
expect(getEvmChainId("avalanche")).toBe(43114);
45+
});
46+
47+
it("should return correct chain ID for IoTeX", () => {
48+
expect(getEvmChainId("iotex")).toBe(4689);
49+
});
50+
51+
it("should return correct chain ID for Sei", () => {
52+
expect(getEvmChainId("sei")).toBe(1329);
53+
});
54+
55+
it("should return correct chain ID for Sei Testnet", () => {
56+
expect(getEvmChainId("sei-testnet")).toBe(1328);
57+
});
58+
59+
it("should return correct chain ID for Peaq", () => {
60+
expect(getEvmChainId("peaq")).toBe(3338);
61+
});
62+
63+
it("should return correct chain ID for Story", () => {
64+
expect(getEvmChainId("story")).toBe(1514);
65+
});
66+
67+
it("should return correct chain ID for Educhain", () => {
68+
expect(getEvmChainId("educhain")).toBe(41923);
69+
});
70+
71+
it("should return correct chain ID for Skale Base Sepolia", () => {
72+
expect(getEvmChainId("skale-base-sepolia")).toBe(324705682);
73+
});
74+
75+
it("should throw for unsupported network", () => {
76+
expect(() => getEvmChainId("unknown-network" as EvmNetworkV1)).toThrow(
77+
"Unsupported network: unknown-network",
78+
);
3279
});
3380
});
3481

typescript/packages/mechanisms/evm/test/unit/v1/client.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,5 +184,26 @@ describe("ExactEvmSchemeV1", () => {
184184
expect(callArgs.domain.chainId).toBe(84532); // Base Sepolia
185185
expect(callArgs.primaryType).toBe("TransferWithAuthorization");
186186
});
187+
188+
it("should throw for unsupported network", async () => {
189+
const client = new ExactEvmSchemeV1(mockSigner);
190+
191+
const requirements: PaymentRequirementsV1 = {
192+
scheme: "exact",
193+
network: "unknown-network",
194+
asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
195+
maxAmountRequired: "100000",
196+
payTo: "0x9876543210987654321098765432109876543210",
197+
maxTimeoutSeconds: 3600,
198+
extra: {
199+
name: "USDC",
200+
version: "2",
201+
},
202+
};
203+
204+
await expect(client.createPaymentPayload(1, requirements as never)).rejects.toThrow(
205+
"Unsupported network: unknown-network",
206+
);
207+
});
187208
});
188209
});

typescript/packages/mechanisms/evm/test/unit/v1/facilitator.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,41 @@ describe("ExactEvmSchemeV1", () => {
245245
expect(result.isValid).toBe(false);
246246
expect(result.invalidReason).toBe("invalid_exact_evm_payload_recipient_mismatch");
247247
});
248+
249+
it("should reject if network not supported", async () => {
250+
const facilitator = new ExactEvmSchemeV1(mockSigner);
251+
252+
const payload: PaymentPayloadV1 = {
253+
x402Version: 1,
254+
scheme: "exact",
255+
network: "unknown-network",
256+
payload: {
257+
authorization: {
258+
from: "0x1234567890123456789012345678901234567890",
259+
to: "0x9876543210987654321098765432109876543210",
260+
value: "100000",
261+
validAfter: "0",
262+
validBefore: "999999999999",
263+
nonce: "0x00",
264+
},
265+
},
266+
};
267+
268+
const requirements: PaymentRequirementsV1 = {
269+
scheme: "exact",
270+
network: "unknown-network",
271+
asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
272+
maxAmountRequired: "100000",
273+
payTo: "0x9876543210987654321098765432109876543210",
274+
maxTimeoutSeconds: 3600,
275+
extra: {},
276+
};
277+
278+
const result = await facilitator.verify(payload as never, requirements as never);
279+
280+
expect(result.isValid).toBe(false);
281+
expect(result.invalidReason).toBe("invalid_network");
282+
});
248283
});
249284

250285
describe("settle", () => {

0 commit comments

Comments
 (0)