Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<caip2>` 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.**
Expand Down Expand Up @@ -153,6 +173,14 @@ TONAPI_API_KEY=<tonapi-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
Expand Down
49 changes: 39 additions & 10 deletions e2e/clients/axios/index.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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));
}
Expand Down
49 changes: 39 additions & 10 deletions e2e/clients/fetch/index.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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));
}
Expand Down
2 changes: 1 addition & 1 deletion e2e/clients/mcp-typescript/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
75 changes: 48 additions & 27 deletions e2e/facilitators/typescript/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import dotenv from "dotenv";
import express from "express";
import {
createWalletClient,
defineChain,
http,
nonceManager,
publicActions,
Expand All @@ -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();
Expand All @@ -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}`);
Expand All @@ -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}`,
Expand All @@ -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<ReturnType<typeof createKeyPairSignerFromBytes>>
| 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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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)",
Expand Down
18 changes: 11 additions & 7 deletions e2e/mock-facilitator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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];

Expand All @@ -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) {
Expand All @@ -54,8 +56,10 @@ function buildSupportedResponse() {

const signers: Record<string, string[]> = {
"eip155:*": [DUMMY_EVM_SIGNER],
"solana:*": [DUMMY_SVM_SIGNER],
};
if (SVM_NETWORK) {
signers["solana:*"] = [DUMMY_SVM_SIGNER];
}
if (APTOS_NETWORK) {
signers["aptos:*"] = [DUMMY_APTOS_SIGNER];
}
Expand Down
1 change: 1 addition & 0 deletions e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions e2e/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading