diff --git a/e2e/README.md b/e2e/README.md index bc4f44dfd7..c854f1a1fd 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -86,6 +86,26 @@ Add the `-v` flag to any command for verbose output: Useful for debugging test failures or understanding the payment flow. +### Targeting a Specific EVM Chain + +The harness defaults to Base Sepolia (`eip155:84532`) on `--testnet`. Pass +`--evm-network=` to target any chain in the SDK's `DEFAULT_STABLECOINS` +catalog (`typescript/packages/mechanisms/evm/src/shared/defaultAssets.ts`). +Combine with `--families=evm` to skip non-EVM credential requirements. + +```bash +# Run EVM-only against Mezo Testnet +pnpm test --testnet --families=evm --evm-network=eip155:31611 +``` + +The harness derives its chain registry from `DEFAULT_STABLECOINS` at module load. +Adding a new chain to the SDK propagates here after `pnpm install` — no harness +source edit. Display names come from viem's chain database. + +EVM-only runs (`--families=evm`) do NOT require Solana, Aptos, Hedera, or +Stellar credentials — the corresponding env vars are only consulted when their +family is selected. + ## Wallet Safety Warning **Use dedicated test wallets only. Do NOT use wallets that hold real funds.** @@ -153,6 +173,14 @@ TONAPI_API_KEY= \ pnpm test --testnet --families=tvm --facilitators=python --clients=httpx,requests --servers=fastapi,flask --min -v ``` +Optional environment variables (chain selection): + +```bash +EVM_RPC_URL=https://... # Override the EVM RPC URL for the selected + # chain. When unset, the harness uses + # viem's default RPC for the chain. +``` + Optional environment variables (batch-settlement scheme): ```bash diff --git a/e2e/clients/axios/index.ts b/e2e/clients/axios/index.ts index 5b16a67e9f..45031ab092 100644 --- a/e2e/clients/axios/index.ts +++ b/e2e/clients/axios/index.ts @@ -1,9 +1,9 @@ import { config } from "dotenv"; import axios from "axios"; import { wrapAxiosWithPayment, decodePaymentResponseHeader } from "@x402/axios"; -import { createPublicClient, http } from "viem"; +import { createPublicClient, defineChain, http, type Chain } from "viem"; import { privateKeyToAccount } from "viem/accounts"; -import { base, baseSepolia } from "viem/chains"; +import * as allViemChains from "viem/chains"; import { ExactEvmScheme, type ExactEvmSchemeOptions } from "@x402/evm/exact/client"; import { UptoEvmScheme as UptoEvmClientScheme, @@ -32,13 +32,39 @@ const baseURL = process.env.RESOURCE_SERVER_URL as string; const endpointPath = process.env.ENDPOINT_PATH as string; const url = `${baseURL}${endpointPath}`; const evmAccount = privateKeyToAccount(process.env.EVM_PRIVATE_KEY as `0x${string}`); -const svmSigner = await createKeyPairSignerFromBytes( - base58.decode(process.env.SVM_PRIVATE_KEY as string), -); +// Lazy SVM signer: only decoded when SVM_PRIVATE_KEY is set so EVM-only runs +// (e.g. --families=evm) don't require a Solana key. +const svmSigner = process.env.SVM_PRIVATE_KEY + ? await createKeyPairSignerFromBytes( + base58.decode(process.env.SVM_PRIVATE_KEY), + ) + : undefined; const evmNetwork = process.env.EVM_NETWORK || "eip155:84532"; const evmRpcUrl = process.env.EVM_RPC_URL; -const evmChain = evmNetwork === "eip155:8453" ? base : baseSepolia; +// Resolve any CAIP-2 EVM chain — viem's chain database first, with a +// minimal defineChain fallback for any SDK chain that viem hasn't packaged yet. +function resolveEvmChain(network: string): Chain { + const [namespace, ref] = network.split(":"); + if (namespace !== "eip155") { + throw new Error(`resolveEvmChain: not an EVM network: ${network}`); + } + const chainId = Number(ref); + if (!Number.isInteger(chainId) || chainId <= 0) { + throw new Error(`resolveEvmChain: invalid EVM chain id in ${network}`); + } + const known = (Object.values(allViemChains) as Chain[]).find( + (c) => c && typeof c === "object" && c.id === chainId, + ); + if (known) return known; + return defineChain({ + id: chainId, + name: `EVM ${chainId}`, + nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, + rpcUrls: { default: { http: [] } }, + }); +} +const evmChain = resolveEvmChain(evmNetwork); const publicClient = createPublicClient({ chain: evmChain, @@ -113,10 +139,13 @@ const client = new x402Client() .register("eip155:*", new UptoEvmClientScheme(evmSigner, uptoSchemeOptions)) .register("eip155:*", batchSettlementScheme) .registerV1("base-sepolia", new ExactEvmSchemeV1(evmSigner)) - .registerV1("base", new ExactEvmSchemeV1(evmSigner)) - .register("solana:*", new ExactSvmScheme(svmSigner)) - .registerV1("solana-devnet", new ExactSvmSchemeV1(svmSigner)) - .registerV1("solana", new ExactSvmSchemeV1(svmSigner)); + .registerV1("base", new ExactEvmSchemeV1(evmSigner)); +if (svmSigner) { + client + .register("solana:*", new ExactSvmScheme(svmSigner)) + .registerV1("solana-devnet", new ExactSvmSchemeV1(svmSigner)) + .registerV1("solana", new ExactSvmSchemeV1(svmSigner)); +} if (aptosAccount) { client.register("aptos:*", new ExactAptosScheme(aptosAccount)); } diff --git a/e2e/clients/fetch/index.ts b/e2e/clients/fetch/index.ts index da3164a42f..5da5bbdcf4 100644 --- a/e2e/clients/fetch/index.ts +++ b/e2e/clients/fetch/index.ts @@ -1,8 +1,8 @@ import { config } from "dotenv"; import { wrapFetchWithPayment } from "@x402/fetch"; -import { createPublicClient, http } from "viem"; +import { createPublicClient, defineChain, http, type Chain } from "viem"; import { privateKeyToAccount } from "viem/accounts"; -import { base, baseSepolia } from "viem/chains"; +import * as allViemChains from "viem/chains"; import { ExactEvmScheme, type ExactEvmSchemeOptions } from "@x402/evm/exact/client"; import { UptoEvmScheme as UptoEvmClientScheme, @@ -31,13 +31,39 @@ const baseURL = process.env.RESOURCE_SERVER_URL as string; const endpointPath = process.env.ENDPOINT_PATH as string; const url = `${baseURL}${endpointPath}`; const evmAccount = privateKeyToAccount(process.env.EVM_PRIVATE_KEY as `0x${string}`); -const svmSigner = await createKeyPairSignerFromBytes( - base58.decode(process.env.SVM_PRIVATE_KEY as string), -); +// Lazy SVM signer: only decoded when SVM_PRIVATE_KEY is set so EVM-only runs +// (e.g. --families=evm) don't require a Solana key. +const svmSigner = process.env.SVM_PRIVATE_KEY + ? await createKeyPairSignerFromBytes( + base58.decode(process.env.SVM_PRIVATE_KEY), + ) + : undefined; const evmNetwork = process.env.EVM_NETWORK || "eip155:84532"; const evmRpcUrl = process.env.EVM_RPC_URL; -const evmChain = evmNetwork === "eip155:8453" ? base : baseSepolia; +// Resolve any CAIP-2 EVM chain — viem's chain database first, with a +// minimal defineChain fallback for any SDK chain that viem hasn't packaged yet. +function resolveEvmChain(network: string): Chain { + const [namespace, ref] = network.split(":"); + if (namespace !== "eip155") { + throw new Error(`resolveEvmChain: not an EVM network: ${network}`); + } + const chainId = Number(ref); + if (!Number.isInteger(chainId) || chainId <= 0) { + throw new Error(`resolveEvmChain: invalid EVM chain id in ${network}`); + } + const known = (Object.values(allViemChains) as Chain[]).find( + (c) => c && typeof c === "object" && c.id === chainId, + ); + if (known) return known; + return defineChain({ + id: chainId, + name: `EVM ${chainId}`, + nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, + rpcUrls: { default: { http: [] } }, + }); +} +const evmChain = resolveEvmChain(evmNetwork); const publicClient = createPublicClient({ chain: evmChain, @@ -112,10 +138,13 @@ const client = new x402Client() .register("eip155:*", new UptoEvmClientScheme(evmSigner, uptoSchemeOptions)) .register("eip155:*", batchSettlementScheme) .registerV1("base-sepolia", new ExactEvmSchemeV1(evmSigner)) - .registerV1("base", new ExactEvmSchemeV1(evmSigner)) - .register("solana:*", new ExactSvmScheme(svmSigner)) - .registerV1("solana-devnet", new ExactSvmSchemeV1(svmSigner)) - .registerV1("solana", new ExactSvmSchemeV1(svmSigner)); + .registerV1("base", new ExactEvmSchemeV1(evmSigner)); +if (svmSigner) { + client + .register("solana:*", new ExactSvmScheme(svmSigner)) + .registerV1("solana-devnet", new ExactSvmSchemeV1(svmSigner)) + .registerV1("solana", new ExactSvmSchemeV1(svmSigner)); +} if (aptosAccount) { client.register("aptos:*", new ExactAptosScheme(aptosAccount)); } diff --git a/e2e/clients/mcp-typescript/index.ts b/e2e/clients/mcp-typescript/index.ts index eb832cf0c4..21c914458c 100644 --- a/e2e/clients/mcp-typescript/index.ts +++ b/e2e/clients/mcp-typescript/index.ts @@ -39,7 +39,7 @@ interface RequestResult { const serverUrl = process.env.RESOURCE_SERVER_URL as string; const endpointPath = process.env.ENDPOINT_PATH as string; // tool name, e.g. "get_weather" const evmPrivateKey = process.env.EVM_PRIVATE_KEY as `0x${string}`; -const evmNetwork = process.env.EVM_NETWORK || "eip155:84532"; +const evmNetwork = (process.env.EVM_NETWORK || "eip155:84532") as `${string}:${string}`; const evmChain = evmNetwork === "eip155:8453" ? base : baseSepolia; const channelSalt = process.env.CHANNEL_SALT as `0x${string}` | undefined; const batchSettlementPhase = process.env.BATCH_SETTLEMENT_PHASE as diff --git a/e2e/facilitators/typescript/index.ts b/e2e/facilitators/typescript/index.ts index e714cbeb1c..75b3276c3e 100644 --- a/e2e/facilitators/typescript/index.ts +++ b/e2e/facilitators/typescript/index.ts @@ -66,6 +66,7 @@ import dotenv from "dotenv"; import express from "express"; import { createWalletClient, + defineChain, http, nonceManager, publicActions, @@ -74,7 +75,7 @@ import { recoverTransactionAddress, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; -import { baseSepolia, base } from "viem/chains"; +import * as allViemChains from "viem/chains"; import { BazaarCatalog } from "./bazaar.js"; dotenv.config(); @@ -97,15 +98,29 @@ const APTOS_RPC_URL = process.env.APTOS_RPC_URL; const HEDERA_NODE_URL = process.env.HEDERA_NODE_URL; const STELLAR_RPC_URL = process.env.STELLAR_RPC_URL; -// Map CAIP-2 network IDs to viem chains +// Resolve a CAIP-2 EVM network id to a viem `Chain`. +// Uses viem's chain database when available; falls back to a minimal +// defineChain so any SDK chain viem hasn't packaged yet still works +// when the caller supplies their own RPC URL. function getEvmChain(network: string): Chain { - switch (network) { - case "eip155:8453": - return base; - case "eip155:84532": - default: - return baseSepolia; + const [namespace, ref] = network.split(":"); + if (namespace !== "eip155") { + throw new Error(`getEvmChain: not an EVM network: ${network}`); } + const chainId = Number(ref); + if (!Number.isInteger(chainId) || chainId <= 0) { + throw new Error(`getEvmChain: invalid EVM chain id in ${network}`); + } + const known = (Object.values(allViemChains) as Chain[]).find( + (c) => c && typeof c === "object" && c.id === chainId, + ); + if (known) return known; + return defineChain({ + id: chainId, + name: `EVM ${chainId}`, + nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, + rpcUrls: { default: { http: [] } }, + }); } console.log(`🌐 EVM Network: ${EVM_NETWORK}`); @@ -127,11 +142,6 @@ if (!process.env.EVM_PRIVATE_KEY) { process.exit(1); } -if (!process.env.SVM_PRIVATE_KEY) { - console.error("āŒ SVM_PRIVATE_KEY environment variable is required"); - process.exit(1); -} - // Initialize the EVM account from private key const evmAccount = privateKeyToAccount( process.env.EVM_PRIVATE_KEY as `0x${string}`, @@ -154,11 +164,17 @@ const authorizerSigner: AuthorizerSigner = { }; console.info(`EVM Receiver Authorizer: ${authorizerSigner.address}`); -// Initialize the SVM account from private key -const svmAccount = await createKeyPairSignerFromBytes( - base58.decode(process.env.SVM_PRIVATE_KEY as string), -); -console.info(`SVM Facilitator account: ${svmAccount.address}`); +// Initialize the SVM account from private key (optional) +// Lazy-decoded so EVM-only runs (e.g. --families=evm) don't require an SVM key. +let svmAccount: + | Awaited> + | undefined; +if (process.env.SVM_PRIVATE_KEY) { + svmAccount = await createKeyPairSignerFromBytes( + base58.decode(process.env.SVM_PRIVATE_KEY as string), + ); + console.info(`SVM Facilitator account: ${svmAccount.address}`); +} // Initialize the Aptos account from private key (format to AIP-80 compliant format) if provided let aptosAccount: Account | undefined; @@ -266,11 +282,13 @@ const evmSigner = toFacilitatorEvmSigner({ }); // Facilitator can now handle all Solana networks with automatic RPC creation -// Pass custom RPC URL if provided -const svmSigner = toFacilitatorSvmSigner( - svmAccount, - SVM_RPC_URL ? { defaultRpcUrl: SVM_RPC_URL } : undefined, -); +// Pass custom RPC URL if provided. Skipped when no SVM key is configured. +const svmSigner = svmAccount + ? toFacilitatorSvmSigner( + svmAccount, + SVM_RPC_URL ? { defaultRpcUrl: SVM_RPC_URL } : undefined, + ) + : undefined; // Facilitator can handle all Aptos networks with automatic RPC creation // Pass custom RPC URL if provided @@ -422,9 +440,12 @@ facilitator EVM_NETWORK as Network, new BatchSettlementEvmScheme(evmSigner, authorizerSigner), ) - .registerV1(EVM_V1_NETWORKS as Network[], new ExactEvmSchemeV1(evmSigner)) - .register(SVM_NETWORK as Network, new ExactSvmScheme(svmSigner)) - .registerV1(SVM_V1_NETWORKS as Network[], new ExactSvmSchemeV1(svmSigner)); + .registerV1(EVM_V1_NETWORKS as Network[], new ExactEvmSchemeV1(evmSigner)); +if (svmSigner) { + facilitator + .register(SVM_NETWORK as Network, new ExactSvmScheme(svmSigner)) + .registerV1(SVM_V1_NETWORKS as Network[], new ExactSvmSchemeV1(svmSigner)); +} if (avmSigner) { facilitator.register(AVM_NETWORK as Network, new ExactAvmScheme(avmSigner)); } @@ -756,7 +777,7 @@ app.get("/health", (req, res) => { res.json({ status: "ok", evmNetwork: EVM_NETWORK, - svmNetwork: SVM_NETWORK, + svmNetwork: svmSigner ? SVM_NETWORK : "(not configured)", avmNetwork: avmSigner ? AVM_NETWORK : "(not configured)", aptosNetwork: aptosAccount ? APTOS_NETWORK : "(not configured)", hederaNetwork: hederaSigner ? HEDERA_NETWORK : "(not configured)", diff --git a/e2e/mock-facilitator/index.ts b/e2e/mock-facilitator/index.ts index 024cdd896f..04ba2324b8 100644 --- a/e2e/mock-facilitator/index.ts +++ b/e2e/mock-facilitator/index.ts @@ -12,9 +12,9 @@ import http from "node:http"; const PORT = parseInt(process.env.PORT || "4099", 10); const EVM_NETWORK = process.env.EVM_NETWORK || "eip155:84532"; -const SVM_NETWORK = process.env.SVM_NETWORK || "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"; -const APTOS_NETWORK = process.env.APTOS_NETWORK || "aptos:2"; -const STELLAR_NETWORK = process.env.STELLAR_NETWORK || "stellar:testnet"; +const SVM_NETWORK = process.env.SVM_NETWORK; +const APTOS_NETWORK = process.env.APTOS_NETWORK; +const STELLAR_NETWORK = process.env.STELLAR_NETWORK; const DUMMY_EVM_SIGNER = "0x0000000000000000000000000000000000000001"; const DUMMY_SVM_SIGNER = "11111111111111111111111111111111"; @@ -23,7 +23,7 @@ const DUMMY_APTOS_SIGNER = const DUMMY_STELLAR_SIGNER = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"; function buildSupportedResponse() { - const evmSchemes = ["exact", "upto"]; + const evmSchemes = ["exact", "upto", "batch-settlement"]; const otherSchemes = ["exact"]; const versions = [1, 2]; @@ -37,8 +37,10 @@ function buildSupportedResponse() { for (const scheme of evmSchemes) { kinds.push({ x402Version: version, scheme, network: EVM_NETWORK }); } - for (const scheme of otherSchemes) { - kinds.push({ x402Version: version, scheme, network: SVM_NETWORK }); + if (SVM_NETWORK) { + for (const scheme of otherSchemes) { + kinds.push({ x402Version: version, scheme, network: SVM_NETWORK }); + } } if (APTOS_NETWORK) { for (const scheme of otherSchemes) { @@ -54,8 +56,10 @@ function buildSupportedResponse() { const signers: Record = { "eip155:*": [DUMMY_EVM_SIGNER], - "solana:*": [DUMMY_SVM_SIGNER], }; + if (SVM_NETWORK) { + signers["solana:*"] = [DUMMY_SVM_SIGNER]; + } if (APTOS_NETWORK) { signers["aptos:*"] = [DUMMY_APTOS_SIGNER]; } diff --git a/e2e/package.json b/e2e/package.json index 00a5b7ff86..6e46df2ede 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -17,6 +17,7 @@ "permit2:revoke": "tsx scripts/permit2-approval.ts revoke" }, "dependencies": { + "@x402/evm": "workspace:*", "dotenv": "^17.0.1", "prompts": "^2.4.2", "tsx": "^4.21.0", diff --git a/e2e/pnpm-lock.yaml b/e2e/pnpm-lock.yaml index 91def4e6a6..01c2ee200d 100644 --- a/e2e/pnpm-lock.yaml +++ b/e2e/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@x402/evm': + specifier: workspace:* + version: link:../typescript/packages/mechanisms/evm dotenv: specifier: ^17.0.1 version: 17.0.1 diff --git a/e2e/scripts/permit2-approval.ts b/e2e/scripts/permit2-approval.ts index 9e3ef2e6f0..107476a418 100644 --- a/e2e/scripts/permit2-approval.ts +++ b/e2e/scripts/permit2-approval.ts @@ -18,13 +18,16 @@ import { config } from 'dotenv'; import { createWalletClient, createPublicClient, + defineChain, http, parseAbi, formatUnits, getAddress, + type Chain, } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; -import { base, baseSepolia } from 'viem/chains'; +import * as allViemChains from 'viem/chains'; +import { DEFAULT_STABLECOINS } from '@x402/evm'; config(); @@ -33,41 +36,58 @@ const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3'; const evmNetwork = process.env.EVM_NETWORK || 'eip155:84532'; const evmRpcUrl = process.env.EVM_RPC_URL; -const evmChain = evmNetwork === 'eip155:8453' ? base : baseSepolia; -const isMainnet = evmNetwork === 'eip155:8453'; - -const TOKENS_BY_NETWORK: Record> = { - 'eip155:84532': { - USDC: { - address: '0x036CbD53842c5426634e7929541eC2318f3dCF7e', - decimals: 6, - name: 'USDC', - }, - MockERC20: { - address: '0xeED520980fC7C7B4eB379B96d61CEdea2423005a', - decimals: 6, - name: 'MockERC20', - }, - }, - 'eip155:8453': { - USDC: { - address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', - decimals: 6, - name: 'USDC', - }, - }, -}; - -const TOKENS = TOKENS_BY_NETWORK[evmNetwork] || TOKENS_BY_NETWORK['eip155:84532']; + +// Resolve any CAIP-2 EVM chain — viem's chain database first, with a +// minimal defineChain fallback for any SDK chain that viem hasn't packaged yet. +function resolveEvmChain(network: string): Chain { + const [namespace, ref] = network.split(':'); + if (namespace !== 'eip155') { + throw new Error(`resolveEvmChain: not an EVM network: ${network}`); + } + const chainId = Number(ref); + if (!Number.isInteger(chainId) || chainId <= 0) { + throw new Error(`resolveEvmChain: invalid EVM chain id in ${network}`); + } + const known = (Object.values(allViemChains) as Chain[]).find( + (c) => c && typeof c === 'object' && c.id === chainId, + ); + if (known) return known; + return defineChain({ + id: chainId, + name: `EVM ${chainId}`, + nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, + rpcUrls: { default: { http: [] } }, + }); +} +const evmChain = resolveEvmChain(evmNetwork); + +// Token list is driven by EVM_PERMIT2_ASSET (canonical Permit2 target for the +// configured chain). Pass an explicit `[tokenAddress]` CLI arg to operate on a +// non-default token (e.g. MockERC20 on Base Sepolia). +// +// Decimals + display name flow from `DEFAULT_STABLECOINS[evmNetwork]` when the +// chain is in the SDK's catalog (the canonical case). Custom tokens supplied +// via the CLI override read decimals from the contract's `decimals()` view. +const permit2AssetEnv = process.env.EVM_PERMIT2_ASSET; +const sdkAsset = DEFAULT_STABLECOINS[evmNetwork]; +const TOKENS: Record = {}; +if (permit2AssetEnv) { + TOKENS.PRIMARY = { + address: getAddress(permit2AssetEnv) as `0x${string}`, + decimals: sdkAsset?.decimals ?? 6, + name: sdkAsset?.name ?? 'TOKEN', + }; +} // Maximum uint256 for unlimited approval const MAX_UINT256 = 2n ** 256n - 1n; -// ERC20 ABI for approve and allowance +// ERC20 ABI for approve, allowance, balanceOf, and decimals const erc20Abi = parseAbi([ 'function approve(address spender, uint256 amount) returns (bool)', 'function allowance(address owner, address spender) view returns (uint256)', 'function balanceOf(address account) view returns (uint256)', + 'function decimals() view returns (uint8)', ]); async function main() { @@ -83,10 +103,15 @@ Usage: pnpm tsx scripts/permit2-approval.ts approve [tokenAddress] pnpm tsx scripts/permit2-approval.ts revoke [tokenAddress] -If tokenAddress is not provided, processes all known tokens (USDC and MockERC20). +If tokenAddress is not provided, processes the chain's primary Permit2 asset +(EVM_PERMIT2_ASSET). Pass an explicit address to operate on a different token. Environment variables required: CLIENT_EVM_PRIVATE_KEY - Private key of the client wallet + EVM_NETWORK - CAIP-2 EVM chain id (e.g. eip155:84532) + EVM_PERMIT2_ASSET - Permit2 target token address for EVM_NETWORK + (optional when [tokenAddress] is provided) + EVM_RPC_URL - Optional RPC override; falls back to viem chain default `); process.exit(1); } @@ -97,6 +122,13 @@ Environment variables required: process.exit(1); } + if (!filterAddress && Object.keys(TOKENS).length === 0) { + console.error( + 'āŒ EVM_PERMIT2_ASSET environment variable is required when no tokenAddress is provided', + ); + process.exit(1); + } + const account = privateKeyToAccount(privateKey as `0x${string}`); const publicClient = createPublicClient({ @@ -110,6 +142,23 @@ Environment variables required: transport: http(evmRpcUrl), }); + // For a CLI-override token not already in TOKENS, read its decimals on-chain. + if ( + filterAddress && + !Object.values(TOKENS).some((t) => getAddress(t.address) === filterAddress) + ) { + const decimals = await publicClient.readContract({ + address: filterAddress, + abi: erc20Abi, + functionName: 'decimals', + }); + TOKENS[filterAddress] = { + address: filterAddress, + decimals: Number(decimals), + name: filterAddress, + }; + } + console.log(`\nšŸ”‘ Wallet: ${account.address}`); console.log(`šŸ“ Network: ${evmChain.name} (${evmNetwork})`); console.log(`šŸ” Permit2: ${PERMIT2_ADDRESS}\n`); diff --git a/e2e/servers/express/index.ts b/e2e/servers/express/index.ts index ad1915b305..e37eaad174 100644 --- a/e2e/servers/express/index.ts +++ b/e2e/servers/express/index.ts @@ -35,8 +35,17 @@ const APTOS_NETWORK = (process.env.APTOS_NETWORK || "aptos:2") as `${string}:${s const HEDERA_NETWORK = (process.env.HEDERA_NETWORK || "hedera:testnet") as `${string}:${string}`; const STELLAR_NETWORK = (process.env.STELLAR_NETWORK || "stellar:testnet") as `${string}:${string}`; const EVM_PAYEE_ADDRESS = process.env.EVM_PAYEE_ADDRESS as `0x${string}`; -const SVM_PAYEE_ADDRESS = process.env.SVM_PAYEE_ADDRESS as string; +const SVM_PAYEE_ADDRESS = process.env.SVM_PAYEE_ADDRESS as string | undefined; const EVM_PERMIT2_ASSET = process.env.EVM_PERMIT2_ASSET as `0x${string}`; + +// Token name/version for permit2 EIP-712 domain. Defaults sourced from canonical +// USDC issuance per chain; falls back to "USDC" for chains we haven't enumerated. +const EVM_PERMIT2_ASSET_NAMES: Record = { + "eip155:84532": "USDC", + "eip155:8453": "USD Coin", +}; +const evmPermit2AssetName = EVM_PERMIT2_ASSET_NAMES[EVM_NETWORK] ?? "USDC"; + const AVM_PAYEE_ADDRESS = process.env.AVM_PAYEE_ADDRESS as string; const APTOS_PAYEE_ADDRESS = process.env.APTOS_PAYEE_ADDRESS as string; const HEDERA_PAYEE_ADDRESS = process.env.HEDERA_PAYEE_ADDRESS as string | undefined; @@ -50,11 +59,6 @@ if (!EVM_PAYEE_ADDRESS) { process.exit(1); } -if (!SVM_PAYEE_ADDRESS) { - console.error("āŒ SVM_PAYEE_ADDRESS environment variable is required"); - process.exit(1); -} - if (!facilitatorUrl) { console.error("āŒ FACILITATOR_URL environment variable is required"); process.exit(1); @@ -94,7 +98,9 @@ server.register( ...(receiverAuthorizerSigner ? { receiverAuthorizerSigner } : {}), }), ); -server.register("solana:*", new ExactSvmScheme()); +if (SVM_PAYEE_ADDRESS) { + server.register("solana:*", new ExactSvmScheme()); +} if (APTOS_PAYEE_ADDRESS) { server.register("aptos:*", new ExactAptosScheme()); } @@ -127,6 +133,20 @@ app.get("/exact/avm", (req, res, next) => { next(); }); +/** + * Pre-middleware guard for optional SVM endpoint + * Returns 501 Not Implemented if SVM is not configured + */ +app.get("/exact/svm", (req, res, next) => { + if (!SVM_PAYEE_ADDRESS) { + return res.status(501).json({ + error: "SVM payments not configured", + message: "SVM_PAYEE_ADDRESS environment variable is not set", + }); + } + next(); +}); + /** * Pre-middleware guard for optional Aptos endpoint * Returns 501 Not Implemented if Aptos is not configured @@ -226,7 +246,7 @@ app.use( asset: EVM_PERMIT2_ASSET, extra: { assetTransferMethod: "permit2", - name: EVM_NETWORK == "eip155:84532" ? "USDC" : "USD Coin", + name: evmPermit2AssetName, version: "2", }, }, @@ -286,31 +306,35 @@ app.use( }), }, }, - "GET /exact/svm": { - accepts: { - payTo: SVM_PAYEE_ADDRESS, - scheme: "exact", - price: "$0.001", - network: SVM_NETWORK, - }, - extensions: { - ...declareDiscoveryExtension({ - output: { - example: { - message: "Protected endpoint accessed successfully", - timestamp: "2024-01-01T00:00:00Z", - }, - schema: { - properties: { - message: { type: "string" }, - timestamp: { type: "string" }, + ...(SVM_PAYEE_ADDRESS + ? { + "GET /exact/svm": { + accepts: { + payTo: SVM_PAYEE_ADDRESS, + scheme: "exact", + price: "$0.001", + network: SVM_NETWORK, + }, + extensions: { + ...declareDiscoveryExtension({ + output: { + example: { + message: "Protected endpoint accessed successfully", + timestamp: "2024-01-01T00:00:00Z", + }, + schema: { + properties: { + message: { type: "string" }, + timestamp: { type: "string" }, + }, + required: ["message", "timestamp"], + }, }, - required: ["message", "timestamp"], - }, + }), }, - }), - }, - }, + }, + } + : {}), ...(APTOS_PAYEE_ADDRESS ? { "GET /exact/aptos": { @@ -401,7 +425,7 @@ app.use( asset: EVM_PERMIT2_ASSET, extra: { assetTransferMethod: "permit2", - name: EVM_NETWORK == "eip155:84532" ? "USDC" : "USD Coin", + name: evmPermit2AssetName, version: "2", }, }, @@ -468,7 +492,7 @@ app.use( asset: EVM_PERMIT2_ASSET, extra: { assetTransferMethod: "permit2", - name: EVM_NETWORK == "eip155:84532" ? "USDC" : "USD Coin", + name: evmPermit2AssetName, version: "2", }, }, @@ -486,7 +510,7 @@ app.use( asset: EVM_PERMIT2_ASSET, extra: { assetTransferMethod: "permit2", - name: EVM_NETWORK == "eip155:84532" ? "USDC" : "USD Coin", + name: evmPermit2AssetName, version: "2", }, }, @@ -780,7 +804,7 @@ app.listen(parseInt(PORT), () => { ā•‘ Stellar Network: ${STELLAR_NETWORK}ā•‘ ā•‘ AVM Payee: ${AVM_PAYEE_ADDRESS || "(not configured)"} ā•‘ EVM Payee: ${EVM_PAYEE_ADDRESS} ā•‘ -ā•‘ SVM Payee: ${SVM_PAYEE_ADDRESS} ā•‘ +ā•‘ SVM Payee: ${SVM_PAYEE_ADDRESS || "(not configured)"} ā•‘ Aptos Payee: ${APTOS_PAYEE_ADDRESS || "(not configured)"} ā•‘ Hedera Payee: ${HEDERA_PAYEE_ADDRESS || "(not configured)"} ā•‘ Stellar Payee: ${STELLAR_PAYEE_ADDRESS || "(not configured)"} diff --git a/e2e/servers/fastify/index.ts b/e2e/servers/fastify/index.ts index 0b9863a9c7..720b373c8b 100644 --- a/e2e/servers/fastify/index.ts +++ b/e2e/servers/fastify/index.ts @@ -35,8 +35,17 @@ const HEDERA_NETWORK = (process.env.HEDERA_NETWORK || "hedera:testnet") as `${st const AVM_NETWORK = (process.env.AVM_NETWORK || "algorand:SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=") as `${string}:${string}`; const STELLAR_NETWORK = (process.env.STELLAR_NETWORK || "stellar:testnet") as `${string}:${string}`; const EVM_PAYEE_ADDRESS = process.env.EVM_PAYEE_ADDRESS as `0x${string}`; -const SVM_PAYEE_ADDRESS = process.env.SVM_PAYEE_ADDRESS as string; +const SVM_PAYEE_ADDRESS = process.env.SVM_PAYEE_ADDRESS as string | undefined; const EVM_PERMIT2_ASSET = process.env.EVM_PERMIT2_ASSET as `0x${string}`; + +// Token name/version for permit2 EIP-712 domain. Defaults are sourced from the +// canonical USDC issuance per chain; falls back to "USDC"/"2" for chains we +// haven't yet enumerated. Add entries when new chains land in EVM_NETWORK_CONFIGS. +const EVM_PERMIT2_ASSET_NAMES: Record = { + "eip155:84532": "USDC", + "eip155:8453": "USD Coin", +}; +const evmPermit2AssetName = EVM_PERMIT2_ASSET_NAMES[EVM_NETWORK] ?? "USDC"; const AVM_PAYEE_ADDRESS = process.env.AVM_PAYEE_ADDRESS as string; const APTOS_PAYEE_ADDRESS = process.env.APTOS_PAYEE_ADDRESS as string; const HEDERA_PAYEE_ADDRESS = process.env.HEDERA_PAYEE_ADDRESS as string | undefined; @@ -50,11 +59,6 @@ if (!EVM_PAYEE_ADDRESS) { process.exit(1); } -if (!SVM_PAYEE_ADDRESS) { - console.error("āŒ SVM_PAYEE_ADDRESS environment variable is required"); - process.exit(1); -} - if (!facilitatorUrl) { console.error("āŒ FACILITATOR_URL environment variable is required"); process.exit(1); @@ -90,7 +94,9 @@ server.register( ...(receiverAuthorizerSigner ? { receiverAuthorizerSigner } : {}), }), ); -server.register("solana:*", new ExactSvmScheme()); +if (SVM_PAYEE_ADDRESS) { + server.register("solana:*", new ExactSvmScheme()); +} if (APTOS_PAYEE_ADDRESS) { server.register("aptos:*", new ExactAptosScheme()); } @@ -121,6 +127,12 @@ app.addHook("onRequest", async (request, reply) => { message: "AVM_PAYEE_ADDRESS environment variable is not set", }); } + if (path === "/exact/svm" && !SVM_PAYEE_ADDRESS) { + return reply.status(501).send({ + error: "SVM payments not configured", + message: "SVM_PAYEE_ADDRESS environment variable is not set", + }); + } if (path === "/exact/aptos" && !APTOS_PAYEE_ADDRESS) { return reply.status(501).send({ error: "Aptos payments not configured", @@ -198,7 +210,7 @@ paymentMiddleware( asset: EVM_PERMIT2_ASSET, extra: { assetTransferMethod: "permit2", - name: EVM_NETWORK == "eip155:84532" ? "USDC" : "USD Coin", + name: evmPermit2AssetName, version: "2", }, }, @@ -258,31 +270,35 @@ paymentMiddleware( }), }, }, - "GET /exact/svm": { - accepts: { - payTo: SVM_PAYEE_ADDRESS, - scheme: "exact", - price: "$0.001", - network: SVM_NETWORK, - }, - extensions: { - ...declareDiscoveryExtension({ - output: { - example: { - message: "Protected endpoint accessed successfully", - timestamp: "2024-01-01T00:00:00Z", - }, - schema: { - properties: { - message: { type: "string" }, - timestamp: { type: "string" }, + ...(SVM_PAYEE_ADDRESS + ? { + "GET /exact/svm": { + accepts: { + payTo: SVM_PAYEE_ADDRESS, + scheme: "exact", + price: "$0.001", + network: SVM_NETWORK, + }, + extensions: { + ...declareDiscoveryExtension({ + output: { + example: { + message: "Protected endpoint accessed successfully", + timestamp: "2024-01-01T00:00:00Z", + }, + schema: { + properties: { + message: { type: "string" }, + timestamp: { type: "string" }, + }, + required: ["message", "timestamp"], + }, }, - required: ["message", "timestamp"], - }, + }), }, - }), - }, - }, + }, + } + : {}), ...(HEDERA_PAYEE_ADDRESS ? { "GET /exact/hedera": { @@ -355,7 +371,7 @@ paymentMiddleware( asset: EVM_PERMIT2_ASSET, extra: { assetTransferMethod: "permit2", - name: EVM_NETWORK == "eip155:84532" ? "USDC" : "USD Coin", + name: evmPermit2AssetName, version: "2", }, }, @@ -439,7 +455,7 @@ paymentMiddleware( asset: EVM_PERMIT2_ASSET, extra: { assetTransferMethod: "permit2", - name: EVM_NETWORK == "eip155:84532" ? "USDC" : "USD Coin", + name: evmPermit2AssetName, version: "2", }, }, @@ -456,7 +472,7 @@ paymentMiddleware( asset: EVM_PERMIT2_ASSET, extra: { assetTransferMethod: "permit2", - name: EVM_NETWORK == "eip155:84532" ? "USDC" : "USD Coin", + name: evmPermit2AssetName, version: "2", }, }, @@ -751,7 +767,7 @@ app.listen({ port: parseInt(PORT) }, (err, address) => { ā•‘ Stellar Network: ${STELLAR_NETWORK}ā•‘ ā•‘ AVM Payee: ${AVM_PAYEE_ADDRESS || "(not configured)"} ā•‘ EVM Payee: ${EVM_PAYEE_ADDRESS} ā•‘ -ā•‘ SVM Payee: ${SVM_PAYEE_ADDRESS} ā•‘ +ā•‘ SVM Payee: ${SVM_PAYEE_ADDRESS || "(not configured)"} ā•‘ Aptos Payee: ${APTOS_PAYEE_ADDRESS || "(not configured)"} ā•‘ Hedera Payee: ${HEDERA_PAYEE_ADDRESS || "(not configured)"} ā•‘ Stellar Payee: ${STELLAR_PAYEE_ADDRESS || "(not configured)"} diff --git a/e2e/servers/hono/index.ts b/e2e/servers/hono/index.ts index 8a31c20004..20c9a5f628 100644 --- a/e2e/servers/hono/index.ts +++ b/e2e/servers/hono/index.ts @@ -36,7 +36,13 @@ const HEDERA_NETWORK = (process.env.HEDERA_NETWORK || "hedera:testnet") as `${st const AVM_NETWORK = (process.env.AVM_NETWORK || "algorand:SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=") as `${string}:${string}`; const STELLAR_NETWORK = (process.env.STELLAR_NETWORK || "stellar:testnet") as `${string}:${string}`; const EVM_PAYEE_ADDRESS = process.env.EVM_PAYEE_ADDRESS as `0x${string}`; -const SVM_PAYEE_ADDRESS = process.env.SVM_PAYEE_ADDRESS as string; +const SVM_PAYEE_ADDRESS = process.env.SVM_PAYEE_ADDRESS as string | undefined; +// Token name/version for permit2 EIP-712 domain. Defaults sourced from canonical +// USDC issuance per chain; falls back to "USDC" for chains we haven't enumerated. +const EVM_PERMIT2_ASSET_NAMES: Record = { + "eip155:84532": "USDC", + "eip155:8453": "USD Coin", +}; const APTOS_PAYEE_ADDRESS = process.env.APTOS_PAYEE_ADDRESS as string; const HEDERA_PAYEE_ADDRESS = process.env.HEDERA_PAYEE_ADDRESS as string | undefined; const AVM_PAYEE_ADDRESS = process.env.AVM_PAYEE_ADDRESS as string; @@ -51,11 +57,6 @@ if (!EVM_PAYEE_ADDRESS) { process.exit(1); } -if (!SVM_PAYEE_ADDRESS) { - console.error("āŒ SVM_PAYEE_ADDRESS environment variable is required"); - process.exit(1); -} - if (!facilitatorUrl) { console.error("āŒ FACILITATOR_URL environment variable is required"); process.exit(1); @@ -95,7 +96,9 @@ x402Server.register( ...(receiverAuthorizerSigner ? { receiverAuthorizerSigner } : {}), }), ); -x402Server.register("solana:*", new ExactSvmScheme()); +if (SVM_PAYEE_ADDRESS) { + x402Server.register("solana:*", new ExactSvmScheme()); +} if (APTOS_PAYEE_ADDRESS) { x402Server.register("aptos:*", new ExactAptosScheme()); } @@ -131,6 +134,23 @@ app.use("/exact/avm", async (c, next) => { await next(); }); +/** + * Pre-middleware guard for optional SVM endpoint + * Returns 501 Not Implemented if SVM is not configured + */ +app.use("/exact/svm", async (c, next) => { + if (!SVM_PAYEE_ADDRESS) { + return c.json( + { + error: "SVM payments not configured", + message: "SVM_PAYEE_ADDRESS environment variable is not set", + }, + 501, + ); + } + await next(); +}); + /** * Pre-middleware guard for optional Aptos endpoint * Returns 501 Not Implemented if Aptos is not configured @@ -237,7 +257,7 @@ app.use( asset: EVM_PERMIT2_ASSET, extra: { assetTransferMethod: "permit2", - name: EVM_NETWORK == "eip155:84532" ? "USDC" : "USD Coin", + name: EVM_PERMIT2_ASSET_NAMES[EVM_NETWORK] ?? "USDC", version: "2", }, }, @@ -297,31 +317,35 @@ app.use( }), }, }, - "GET /exact/svm": { - accepts: { - payTo: SVM_PAYEE_ADDRESS, - scheme: "exact", - price: "$0.001", - network: SVM_NETWORK, - }, - extensions: { - ...declareDiscoveryExtension({ - output: { - example: { - message: "Protected endpoint accessed successfully", - timestamp: "2024-01-01T00:00:00Z", - }, - schema: { - properties: { - message: { type: "string" }, - timestamp: { type: "string" }, + ...(SVM_PAYEE_ADDRESS + ? { + "GET /exact/svm": { + accepts: { + payTo: SVM_PAYEE_ADDRESS, + scheme: "exact", + price: "$0.001", + network: SVM_NETWORK, + }, + extensions: { + ...declareDiscoveryExtension({ + output: { + example: { + message: "Protected endpoint accessed successfully", + timestamp: "2024-01-01T00:00:00Z", + }, + schema: { + properties: { + message: { type: "string" }, + timestamp: { type: "string" }, + }, + required: ["message", "timestamp"], + }, }, - required: ["message", "timestamp"], - }, + }), }, - }), - }, - }, + }, + } + : {}), ...(APTOS_PAYEE_ADDRESS ? { "GET /exact/aptos": { @@ -393,7 +417,7 @@ app.use( asset: EVM_PERMIT2_ASSET, extra: { assetTransferMethod: "permit2", - name: EVM_NETWORK == "eip155:84532" ? "USDC" : "USD Coin", + name: EVM_PERMIT2_ASSET_NAMES[EVM_NETWORK] ?? "USDC", version: "2", }, }, @@ -477,7 +501,7 @@ app.use( asset: EVM_PERMIT2_ASSET, extra: { assetTransferMethod: "permit2", - name: EVM_NETWORK == "eip155:84532" ? "USDC" : "USD Coin", + name: EVM_PERMIT2_ASSET_NAMES[EVM_NETWORK] ?? "USDC", version: "2", }, }, @@ -495,7 +519,7 @@ app.use( asset: EVM_PERMIT2_ASSET, extra: { assetTransferMethod: "permit2", - name: EVM_NETWORK == "eip155:84532" ? "USDC" : "USD Coin", + name: EVM_PERMIT2_ASSET_NAMES[EVM_NETWORK] ?? "USDC", version: "2", }, }, @@ -796,7 +820,7 @@ console.log(` ā•‘ Stellar Network: ${STELLAR_NETWORK} ā•‘ ā•‘ AVM Payee: ${AVM_PAYEE_ADDRESS || "(not configured)"} ā•‘ EVM Payee: ${EVM_PAYEE_ADDRESS} ā•‘ -ā•‘ SVM Payee: ${SVM_PAYEE_ADDRESS} ā•‘ +ā•‘ SVM Payee: ${SVM_PAYEE_ADDRESS || "(not configured)"} ā•‘ Aptos Payee: ${APTOS_PAYEE_ADDRESS || "(not configured)"} ā•‘ Hedera Payee: ${HEDERA_PAYEE_ADDRESS || "(not configured)"} ā•‘ Stellar Payee: ${STELLAR_PAYEE_ADDRESS || "(not configured)"} diff --git a/e2e/servers/next/proxy.ts b/e2e/servers/next/proxy.ts index 0f16f53857..4c8b99a5c0 100644 --- a/e2e/servers/next/proxy.ts +++ b/e2e/servers/next/proxy.ts @@ -16,7 +16,7 @@ import { } from "@x402/extensions"; export const EVM_PAYEE_ADDRESS = process.env.EVM_PAYEE_ADDRESS as `0x${string}`; -export const SVM_PAYEE_ADDRESS = process.env.SVM_PAYEE_ADDRESS as string; +export const SVM_PAYEE_ADDRESS = process.env.SVM_PAYEE_ADDRESS as string | undefined; export const AVM_PAYEE_ADDRESS = process.env.AVM_PAYEE_ADDRESS as string; export const APTOS_PAYEE_ADDRESS = process.env.APTOS_PAYEE_ADDRESS as string; export const HEDERA_PAYEE_ADDRESS = process.env.HEDERA_PAYEE_ADDRESS as string | undefined; @@ -34,6 +34,14 @@ export const STELLAR_NETWORK = (process.env.STELLAR_NETWORK || const EVM_PERMIT2_ASSET = process.env.EVM_PERMIT2_ASSET as `0x${string}`; const facilitatorUrl = process.env.FACILITATOR_URL; +// Token name/version for permit2 EIP-712 domain. Defaults sourced from canonical +// USDC issuance per chain; falls back to "USDC" for chains we haven't enumerated. +const EVM_PERMIT2_ASSET_NAMES: Record = { + "eip155:84532": "USDC", + "eip155:8453": "USD Coin", +}; +const evmPermit2AssetName = EVM_PERMIT2_ASSET_NAMES[EVM_NETWORK] ?? "USDC"; + if (!facilitatorUrl) { console.error("āŒ FACILITATOR_URL environment variable is required"); process.exit(1); @@ -70,7 +78,9 @@ server.register( ...(receiverAuthorizerSigner ? { receiverAuthorizerSigner } : {}), }), ); -server.register("solana:*", new ExactSvmScheme()); +if (SVM_PAYEE_ADDRESS) { + server.register("solana:*", new ExactSvmScheme()); +} if (APTOS_PAYEE_ADDRESS) { server.register("aptos:*", new ExactAptosScheme()); } @@ -106,7 +116,7 @@ export const proxy = paymentProxy( asset: EVM_PERMIT2_ASSET, extra: { assetTransferMethod: "permit2", - name: EVM_NETWORK == "eip155:84532" ? "USDC" : "USD Coin", + name: evmPermit2AssetName, version: "2", }, }, @@ -166,31 +176,35 @@ export const proxy = paymentProxy( }), }, }, - "/api/exact/svm": { - accepts: { - payTo: SVM_PAYEE_ADDRESS, - scheme: "exact", - price: "$0.001", - network: SVM_NETWORK, - }, - extensions: { - ...declareDiscoveryExtension({ - output: { - example: { - message: "Protected endpoint accessed successfully", - timestamp: "2024-01-01T00:00:00Z", - }, - schema: { - properties: { - message: { type: "string" }, - timestamp: { type: "string" }, + ...(SVM_PAYEE_ADDRESS + ? { + "/api/exact/svm": { + accepts: { + payTo: SVM_PAYEE_ADDRESS, + scheme: "exact", + price: "$0.001", + network: SVM_NETWORK, + }, + extensions: { + ...declareDiscoveryExtension({ + output: { + example: { + message: "Protected endpoint accessed successfully", + timestamp: "2024-01-01T00:00:00Z", + }, + schema: { + properties: { + message: { type: "string" }, + timestamp: { type: "string" }, + }, + required: ["message", "timestamp"], + }, }, - required: ["message", "timestamp"], - }, + }), }, - }), - }, - }, + }, + } + : {}), ...(AVM_PAYEE_ADDRESS ? { "/api/exact/avm": { @@ -399,7 +413,7 @@ export const proxy = paymentProxy( asset: EVM_PERMIT2_ASSET, extra: { assetTransferMethod: "permit2", - name: EVM_NETWORK == "eip155:84532" ? "USDC" : "USD Coin", + name: evmPermit2AssetName, version: "2", }, }, @@ -415,7 +429,7 @@ export const proxy = paymentProxy( asset: EVM_PERMIT2_ASSET, extra: { assetTransferMethod: "permit2", - name: EVM_NETWORK == "eip155:84532" ? "USDC" : "USD Coin", + name: evmPermit2AssetName, version: "2", }, }, diff --git a/e2e/src/cli/args.ts b/e2e/src/cli/args.ts index 847f1bfcec..b5155b2aaa 100644 --- a/e2e/src/cli/args.ts +++ b/e2e/src/cli/args.ts @@ -14,6 +14,7 @@ export interface ParsedArgs { showHelp: boolean; minimize: boolean; networkMode?: NetworkMode; // undefined = prompt user, set = skip prompt + evmNetwork?: string; // CAIP-2 EVM override (e.g. eip155:31611), overlays mode default parallel: boolean; concurrency: number; endpoints?: string[]; @@ -87,6 +88,9 @@ export function parseArgs(): ParsedArgs { networkMode = 'testnet'; } + // Parse EVM network override (CAIP-2, e.g. eip155:31611 for Mezo Testnet) + const evmNetwork = args.find(arg => arg.startsWith('--evm-network='))?.split('=')[1]; + // Parse filters (comma-separated lists) const transports = parseListArg(args, '--transport'); const facilitators = parseListArg(args, '--facilitators'); @@ -117,6 +121,7 @@ export function parseArgs(): ParsedArgs { showHelp: false, minimize, networkMode, + evmNetwork, parallel, concurrency, endpoints, @@ -140,6 +145,7 @@ export function printHelp(): void { console.log('Network Selection:'); console.log(' --testnet Use testnet networks'); console.log(' --mainnet Use mainnet networks āš ļø Real funds!'); + console.log(' --evm-network= Override EVM slot (e.g. eip155:31611 for Mezo Testnet)'); console.log(' (If not specified, will prompt in interactive mode)'); console.log(''); console.log('Programmatic Mode (for CI/workflows):'); diff --git a/e2e/src/networks/networks.ts b/e2e/src/networks/networks.ts index 472151990d..92d63679fa 100644 --- a/e2e/src/networks/networks.ts +++ b/e2e/src/networks/networks.ts @@ -1,10 +1,15 @@ /** * Network configuration for E2E tests - * + * * This is the single source of truth for all network configs. - * Use getNetworkSet() to get configs for testnet or mainnet mode. + * Use getNetworkSet() to get configs for testnet or mainnet mode (Base-only shortcut). + * Use getEvmNetworkConfig() for any EVM chain in DEFAULT_STABLECOINS by CAIP-2 id. */ +import { DEFAULT_STABLECOINS } from '@x402/evm'; +import { type Chain, defineChain } from 'viem'; +import * as allChains from 'viem/chains'; + export type NetworkMode = 'testnet' | 'mainnet'; export type ProtocolFamily = 'evm' | 'svm' | 'avm' | 'aptos' | 'hedera' | 'stellar' | 'tvm'; @@ -25,6 +30,20 @@ export type NetworkSet = { tvm: NetworkConfig; }; +/** + * Resolve the EVM RPC URL for a chain. Single-knob configuration: + * 1. `EVM_RPC_URL` override (matches the harness-wide convention used by + * e2e/test.ts, e2e/facilitators/*, and e2e/clients/*). + * 2. viem's chain default (covers public chains shipped by viem). + * 3. Empty string when neither applies — preserves the prior + * `process.env.X || ''` semantics so module load never throws. + */ +function evmRpcUrl(caip2: string): string { + const override = process.env.EVM_RPC_URL?.trim(); + if (override) return override; + return resolveViemChain(caip2).rpcUrls.default?.http?.[0] ?? ''; +} + /** * All supported networks, organized by mode and protocol family */ @@ -33,7 +52,7 @@ const NETWORK_SETS: Record = { evm: { name: 'Base Sepolia', caip2: 'eip155:84532', - rpcUrl: process.env.BASE_SEPOLIA_RPC_URL || 'https://sepolia.base.org', + rpcUrl: evmRpcUrl('eip155:84532'), permit2Asset: '0x036CbD53842c5426634e7929541eC2318f3dCF7e', }, svm: { @@ -71,7 +90,7 @@ const NETWORK_SETS: Record = { evm: { name: 'Base', caip2: 'eip155:8453', - rpcUrl: process.env.BASE_RPC_URL || 'https://mainnet.base.org', + rpcUrl: evmRpcUrl('eip155:8453'), permit2Asset: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', }, svm: { @@ -108,13 +127,20 @@ const NETWORK_SETS: Record = { }; /** - * Get the network set for a given mode - * + * Get the network set for a given mode, optionally overriding the EVM slot. + * * @param mode - 'testnet' or 'mainnet' + * @param evmCaip2 - Optional CAIP-2 EVM identifier; when provided the `evm` + * slot is overlaid from {@link EVM_NETWORK_CONFIGS} so the harness can + * target chains beyond the mode's default (e.g. Mezo Testnet on a `--testnet` run). * @returns NetworkSet containing configured protocol network configs */ -export function getNetworkSet(mode: NetworkMode): NetworkSet { - return NETWORK_SETS[mode]; +export function getNetworkSet(mode: NetworkMode, evmCaip2?: string): NetworkSet { + const base = NETWORK_SETS[mode]; + if (!evmCaip2 || evmCaip2 === base.evm.caip2) { + return base; + } + return { ...base, evm: getEvmNetworkConfig(evmCaip2) }; } /** @@ -150,7 +176,7 @@ export function getNetworkForProtocol( /** * Get display string for a network mode - * + * * @param mode - 'testnet' or 'mainnet' * @returns Human-readable description of the networks */ @@ -159,3 +185,76 @@ export function getNetworkModeDescription(mode: NetworkMode): string { const networks = [set.evm.name, set.svm.name, set.avm.name, set.aptos.name, set.hedera.name, set.stellar.name, set.tvm.name]; return networks.join(' + '); } + +/** + * Per-chain EVM network configurations indexed by CAIP-2 identifier. + * Derived at module load from {@link DEFAULT_STABLECOINS} (the SDK's + * canonical chain catalog) so adding a chain there propagates here + * automatically — no parallel hand-curated table to keep in sync. + * + * Display name comes from viem's chain database, falling back to + * `EVM ${chainId}` for chains viem hasn't shipped. + * + * RPC URLs resolve via {@link evmRpcUrl}: `EVM_RPC_URL` overrides everything, + * otherwise viem's chain default is used (empty string when viem ships no + * default). + * `permit2Asset` is the chain's default stablecoin (also the Permit2 target for + * tests that exercise the permit2 / EIP-2612 path). + */ +export const EVM_NETWORK_CONFIGS: Record = Object.fromEntries( + Object.keys(DEFAULT_STABLECOINS).map(caip2 => [ + caip2, + { + name: resolveViemChain(caip2).name, + caip2: caip2 as `${string}:${string}`, + rpcUrl: evmRpcUrl(caip2), + permit2Asset: DEFAULT_STABLECOINS[caip2].address, + }, + ]), +); + +/** + * Get NetworkConfig for an EVM chain by CAIP-2 identifier. + * + * @param caip2 - CAIP-2 EVM identifier (e.g. "eip155:84532") + * @returns NetworkConfig for the chain + * @throws If the network is not in the configured list + */ +export function getEvmNetworkConfig(caip2: string): NetworkConfig { + const config = EVM_NETWORK_CONFIGS[caip2]; + if (!config) { + throw new Error( + `No EVM network config for ${caip2}. Supported: ${Object.keys(EVM_NETWORK_CONFIGS).join(', ')}`, + ); + } + return config; +} + +/** + * Map a CAIP-2 EVM identifier to a viem `Chain`. + * + * Looks up viem's chain database; falls back to a minimal `defineChain` so + * that EVM networks viem hasn't yet packaged still work for callers supplying + * their own `EVM_RPC_URL`. + * + * @param caip2 - CAIP-2 EVM identifier (e.g. "eip155:84532") + * @returns viem Chain object suitable for createPublicClient/createWalletClient + */ +export function resolveViemChain(caip2: string): Chain { + const [namespace, ref] = caip2.split(':'); + if (namespace !== 'eip155') { + throw new Error(`resolveViemChain: not an EVM network: ${caip2}`); + } + const chainId = Number(ref); + if (!Number.isInteger(chainId) || chainId <= 0) { + throw new Error(`resolveViemChain: invalid EVM chain id in ${caip2}`); + } + const known = (Object.values(allChains) as Chain[]).find(c => c.id === chainId); + if (known) return known; + return defineChain({ + id: chainId, + name: `EVM ${chainId}`, + nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, + rpcUrls: { default: { http: [] } }, + }); +} diff --git a/e2e/src/servers/generic-server.ts b/e2e/src/servers/generic-server.ts index 3d1d63ca0b..afc672f9b5 100644 --- a/e2e/src/servers/generic-server.ts +++ b/e2e/src/servers/generic-server.ts @@ -258,10 +258,20 @@ export class GenericServerProxy extends BaseProxy implements ServerProxy { } /** - * Translates v2 CAIP-2 network format to v1 simple format for legacy servers - * + * Translates v2 CAIP-2 network format to v1 simple format for legacy servers. + * + * Activated only when {@link this.directory} contains `legacy/` (see + * `isV1Server` gate in this file). For v2 (non-legacy) servers this function + * is dead code: callers pass the v2 CAIP-2 string straight through unchanged. + * + * The hardcoded map covers the v1 chains the legacy harness was built around + * (Base / Base Sepolia, Solana mainnet / devnet); any other CAIP-2 input + * falls through unchanged. When `e2e/legacy/*` is retired this entire + * translator can be deleted. + * * @param network - Network in CAIP-2 format (e.g., "eip155:84532") - * @returns Network in v1 format (e.g., "base-sepolia") + * @returns Network in v1 format (e.g., "base-sepolia") for known mappings, + * otherwise the input is returned unchanged. */ function translateNetworkForV1(network: string): string { const networkMap: Record = { diff --git a/e2e/test.ts b/e2e/test.ts index f278753bfd..4e0ee541a3 100644 --- a/e2e/test.ts +++ b/e2e/test.ts @@ -4,7 +4,6 @@ import { writeFileSync } from 'fs'; import { join } from 'path'; import { createWalletClient, createPublicClient, http, parseEther, formatEther, toHex } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; -import { base, baseSepolia } from 'viem/chains'; import { TestDiscovery } from './src/discovery'; import { ClientConfig, ScenarioResult, ServerConfig, TestScenario, endpointAssetTransferMethod, endpointPaymentScheme, endpointUsesBatchSettlement } from './src/types'; import { config as loggerConfig, log, verboseLog, errorLog, close as closeLogger, createComboLogger } from './src/logger'; @@ -13,7 +12,7 @@ import { parseArgs, printHelp } from './src/cli/args'; import { runInteractiveMode } from './src/cli/interactive'; import { filterScenarios, TestFilters, shouldShowExtensionOutput } from './src/cli/filters'; import { minimizeScenarios } from './src/sampling'; -import { getNetworkSet, NetworkMode, getNetworkModeDescription, resolveEvmPermit2Asset } from './src/networks/networks'; +import { getNetworkSet, NetworkMode, getNetworkModeDescription, resolveEvmPermit2Asset, resolveViemChain } from './src/networks/networks'; import { GenericServerProxy } from './src/servers/generic-server'; import { Semaphore, FacilitatorLock } from './src/concurrency'; import { FacilitatorManager } from './src/facilitators/facilitator-manager'; @@ -139,7 +138,7 @@ async function revokePermit2Approval(tokenAddress?: string): Promise { function getEvmClients() { const evmNetwork = process.env.EVM_NETWORK || 'eip155:84532'; const evmRpcUrl = process.env.EVM_RPC_URL; - const evmChain = evmNetwork === 'eip155:8453' ? base : baseSepolia; + const evmChain = resolveViemChain(evmNetwork); const facilitatorKey = process.env.FACILITATOR_EVM_PRIVATE_KEY; const clientKey = process.env.CLIENT_EVM_PRIVATE_KEY; @@ -511,11 +510,6 @@ async function runTest() { const facilitatorStellarPrivateKey = process.env.FACILITATOR_STELLAR_PRIVATE_KEY; const facilitatorTvmPrivateKey = process.env.FACILITATOR_TVM_PRIVATE_KEY; const batchSettlementRecovery = envFlagDefaultTrue(process.env.BATCH_SETTLEMENT_RECOVERY); - if (!serverEvmAddress || !serverSvmAddress || !clientEvmPrivateKey || !clientSvmPrivateKey || !facilitatorEvmPrivateKey || !facilitatorSvmPrivateKey) { - errorLog('āŒ Missing required environment variables:'); - errorLog(' SERVER_EVM_ADDRESS, SERVER_SVM_ADDRESS, CLIENT_EVM_PRIVATE_KEY, CLIENT_SVM_PRIVATE_KEY, FACILITATOR_EVM_PRIVATE_KEY, and FACILITATOR_SVM_PRIVATE_KEY must be set'); - process.exit(1); - } // Discover all servers, clients, and facilitators (always include legacy) const discovery = new TestDiscovery('.', true); // Always discover legacy @@ -580,8 +574,8 @@ async function runTest() { } } - // Get network configuration based on selected mode - const networks = getNetworkSet(networkMode); + // Get network configuration based on selected mode, with optional CLI EVM override + const networks = getNetworkSet(networkMode, parsedArgs.evmNetwork); const evmPermit2Asset = resolveEvmPermit2Asset(networks); const permit2AssetSource = process.env.EVM_PERMIT2_ASSET?.trim()