Skip to content

Commit d516921

Browse files
committed
add e2e tests
1 parent 7a390d0 commit d516921

30 files changed

Lines changed: 964 additions & 696 deletions

File tree

e2e/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ Launches an interactive CLI where you can select:
5353
- **Clients** - Payment-capable HTTP clients (axios, fetch, httpx, requests, etc.)
5454
- **Extensions** - Additional features like Bazaar discovery
5555
- **Protocols** - EVM, SVM, and/or Aptos networks
56+
- **Payment schemes** (when multiple apply) - `exact`, `upto`, or `batch-settlement`
5657

5758
Every valid combination of your selections will be tested. For example, selecting 2 facilitators, 3 servers, and 2 clients will generate and run all compatible test scenarios.
5859

@@ -129,6 +130,13 @@ FACILITATOR_APTOS_PRIVATE_KEY=... # Aptos private key for facilitator (hex str
129130
FACILITATOR_STELLAR_PRIVATE_KEY=... # Stellar private key for facilitator
130131
```
131132

133+
Optional environment variables (batch-settlement scheme):
134+
135+
```bash
136+
SERVER_EVM_RECEIVER_AUTHORIZER_PRIVATE_KEY=0x... # server-side self-managed claim/refund signer
137+
CLIENT_EVM_VOUCHER_SIGNER_PRIVATE_KEY=0x... # EOA the client uses to sign vouchers
138+
```
139+
132140
### Account Setup Instructions
133141

134142
#### Stellar Testnet

e2e/clients/axios/index.ts

Lines changed: 88 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { privateKeyToAccount } from "viem/accounts";
66
import { base, baseSepolia } from "viem/chains";
77
import { ExactEvmScheme, type ExactEvmSchemeOptions } from "@x402/evm/exact/client";
88
import { UptoEvmScheme as UptoEvmClientScheme, type UptoEvmSchemeOptions } from "@x402/evm/upto/client";
9+
import { BatchSettlementEvmScheme } from "@x402/evm/batch-settlement/client";
910
import { ExactEvmSchemeV1 } from "@x402/evm/v1";
1011
import { toClientEvmSigner } from "@x402/evm";
1112
import { ExactSvmScheme } from "@x402/svm/exact/client";
@@ -49,6 +50,23 @@ const uptoSchemeOptions: UptoEvmSchemeOptions | undefined = process.env.EVM_RPC_
4950
? { rpcUrl: process.env.EVM_RPC_URL }
5051
: undefined;
5152

53+
// Batch-settlement scheme uses a per-scenario salt (CHANNEL_SALT) so concurrent
54+
// e2e runs don't collide on the same on-chain channel id. An optional voucher
55+
// signer (EVM_VOUCHER_SIGNER_PRIVATE_KEY) exercises the alt-EOA voucher branch
56+
// while deposits keep using the main client signer.
57+
const channelSalt = process.env.CHANNEL_SALT as `0x${string}` | undefined;
58+
const voucherSignerKey = process.env.EVM_VOUCHER_SIGNER_PRIVATE_KEY as
59+
| `0x${string}`
60+
| undefined;
61+
const voucherSigner = voucherSignerKey
62+
? toClientEvmSigner(privateKeyToAccount(voucherSignerKey), publicClient)
63+
: undefined;
64+
const batchSettlementOptions =
65+
channelSalt || voucherSigner
66+
? { ...(channelSalt ? { salt: channelSalt } : {}), ...(voucherSigner ? { voucherSigner } : {}) }
67+
: undefined;
68+
const batchSettlementScheme = new BatchSettlementEvmScheme(evmSigner, batchSettlementOptions);
69+
5270
// Initialize Aptos signer if key is provided
5371
let aptosAccount: Account | undefined;
5472
if (process.env.APTOS_PRIVATE_KEY) {
@@ -75,6 +93,7 @@ if (process.env.AVM_PRIVATE_KEY) {
7593
const client = new x402Client()
7694
.register("eip155:*", new ExactEvmScheme(evmSigner, evmSchemeOptions))
7795
.register("eip155:*", new UptoEvmClientScheme(evmSigner, uptoSchemeOptions))
96+
.register("eip155:*", batchSettlementScheme)
7897
.registerV1("base-sepolia", new ExactEvmSchemeV1(evmSigner))
7998
.registerV1("base", new ExactEvmSchemeV1(evmSigner))
8099
.register("solana:*", new ExactSvmScheme(svmSigner))
@@ -92,44 +111,75 @@ if (avmSigner) {
92111

93112
const axiosWithPayment = wrapAxiosWithPayment(axios.create(), client);
94113

95-
axiosWithPayment
96-
.get(url)
97-
.then(async (response) => {
98-
const data = response.data;
99-
// Check both v2 (PAYMENT-RESPONSE) and v1 (X-PAYMENT-RESPONSE) headers
100-
const paymentResponse =
101-
response.headers["payment-response"] || response.headers["x-payment-response"];
102-
103-
if (!paymentResponse) {
104-
// No payment was required
105-
const result = {
106-
success: true,
107-
data: data,
108-
status_code: response.status,
109-
};
110-
console.log(JSON.stringify(result));
111-
process.exit(0);
112-
return;
114+
// Multi-request scenarios (used by batch-settlement) issue several paid requests
115+
// against the same endpoint so the server can amortise on-chain claims, then
116+
// optionally signal a cooperative refund on the last request.
117+
const numberOfRequests = Number.parseInt(process.env.MULTI_REQUEST_COUNT ?? "1", 10);
118+
const refundOnLastRequest = process.env.REFUND_ON_LAST === "true";
119+
120+
/**
121+
* Issues a single paid request and returns the parsed result.
122+
*
123+
* @returns Structured result with response data and decoded payment-response.
124+
*/
125+
async function issueRequest(): Promise<{
126+
success: boolean;
127+
data: unknown;
128+
status_code: number;
129+
payment_response?: ReturnType<typeof decodePaymentResponseHeader>;
130+
}> {
131+
const response = await axiosWithPayment.get(url);
132+
const paymentResponseHeader =
133+
response.headers["payment-response"] || response.headers["x-payment-response"];
134+
135+
if (!paymentResponseHeader) {
136+
return { success: true, data: response.data, status_code: response.status };
137+
}
138+
139+
const decodedPaymentResponse = decodePaymentResponseHeader(paymentResponseHeader);
140+
return {
141+
success: decodedPaymentResponse.success,
142+
data: response.data,
143+
status_code: response.status,
144+
payment_response: decodedPaymentResponse,
145+
};
146+
}
147+
148+
try {
149+
const results: Awaited<ReturnType<typeof issueRequest>>[] = [];
150+
let lastChannelId: string | undefined;
151+
for (let i = 0; i < numberOfRequests; i++) {
152+
const isLast = i === numberOfRequests - 1;
153+
154+
if (isLast && refundOnLastRequest && lastChannelId) {
155+
batchSettlementScheme.requestRefund(lastChannelId);
113156
}
114157

115-
const decodedPaymentResponse = decodePaymentResponseHeader(paymentResponse);
116-
117-
const result = {
118-
success: decodedPaymentResponse.success,
119-
data: data,
120-
status_code: response.status,
121-
payment_response: decodedPaymentResponse,
122-
};
123-
124-
// Output structured result as JSON for proxy to parse
125-
console.log(JSON.stringify(result));
126-
process.exit(0);
127-
})
128-
.catch((error) => {
129-
console.error(JSON.stringify({
158+
const result = await issueRequest();
159+
results.push(result);
160+
161+
const channelId = result.payment_response?.extra?.channelId;
162+
if (typeof channelId === "string" && channelId.length > 0) {
163+
lastChannelId = channelId;
164+
}
165+
}
166+
167+
const last = results[results.length - 1]!;
168+
const aggregate =
169+
numberOfRequests > 1
170+
? { ...last, requests: results, request_count: numberOfRequests }
171+
: last;
172+
173+
console.log(JSON.stringify(aggregate));
174+
process.exit(0);
175+
} catch (error: unknown) {
176+
const err = error as { message?: string; response?: { status?: number } };
177+
console.error(
178+
JSON.stringify({
130179
success: false,
131-
error: error.message || "Request failed",
132-
status_code: error.response?.status || 500,
133-
}));
134-
process.exit(1);
135-
});
180+
error: err.message || "Request failed",
181+
status_code: err.response?.status || 500,
182+
}),
183+
);
184+
process.exit(1);
185+
}

e2e/clients/axios/test.config.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
"transferMethods": [
1818
"eip3009",
1919
"permit2",
20-
"upto"
20+
"upto",
21+
"batch-settlement"
2122
]
2223
},
2324
"extensions": [
@@ -34,7 +35,11 @@
3435
"optional": [
3536
"AVM_PRIVATE_KEY",
3637
"APTOS_PRIVATE_KEY",
37-
"STELLAR_PRIVATE_KEY"
38+
"STELLAR_PRIVATE_KEY",
39+
"CHANNEL_SALT",
40+
"MULTI_REQUEST_COUNT",
41+
"REFUND_ON_LAST",
42+
"EVM_VOUCHER_SIGNER_PRIVATE_KEY"
3843
]
3944
}
4045
}

e2e/clients/fetch/index.ts

Lines changed: 70 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { privateKeyToAccount } from "viem/accounts";
55
import { base, baseSepolia } from "viem/chains";
66
import { ExactEvmScheme, type ExactEvmSchemeOptions } from "@x402/evm/exact/client";
77
import { UptoEvmScheme as UptoEvmClientScheme, type UptoEvmSchemeOptions } from "@x402/evm/upto/client";
8+
import { BatchSettlementEvmScheme } from "@x402/evm/batch-settlement/client";
89
import { ExactEvmSchemeV1 } from "@x402/evm/v1";
910
import { toClientEvmSigner } from "@x402/evm";
1011
import { ExactSvmScheme } from "@x402/svm/exact/client";
@@ -46,6 +47,23 @@ const uptoSchemeOptions: UptoEvmSchemeOptions | undefined = process.env.EVM_RPC_
4647
? { rpcUrl: process.env.EVM_RPC_URL }
4748
: undefined;
4849

50+
// Batch-settlement scheme uses a per-scenario salt (CHANNEL_SALT) so concurrent
51+
// e2e runs don't collide on the same on-chain channel id. An optional voucher
52+
// signer (EVM_VOUCHER_SIGNER_PRIVATE_KEY) exercises the alt-EOA voucher branch
53+
// while deposits keep using the main client signer.
54+
const channelSalt = process.env.CHANNEL_SALT as `0x${string}` | undefined;
55+
const voucherSignerKey = process.env.EVM_VOUCHER_SIGNER_PRIVATE_KEY as
56+
| `0x${string}`
57+
| undefined;
58+
const voucherSigner = voucherSignerKey
59+
? toClientEvmSigner(privateKeyToAccount(voucherSignerKey), publicClient)
60+
: undefined;
61+
const batchSettlementOptions =
62+
channelSalt || voucherSigner
63+
? { ...(channelSalt ? { salt: channelSalt } : {}), ...(voucherSigner ? { voucherSigner } : {}) }
64+
: undefined;
65+
const batchSettlementScheme = new BatchSettlementEvmScheme(evmSigner, batchSettlementOptions);
66+
4967
// Initialize Aptos signer if key is provided
5068
let aptosAccount: Account | undefined;
5169
if (process.env.APTOS_PRIVATE_KEY) {
@@ -69,6 +87,7 @@ if (process.env.AVM_PRIVATE_KEY) {
6987
const client = new x402Client()
7088
.register("eip155:*", new ExactEvmScheme(evmSigner, evmSchemeOptions))
7189
.register("eip155:*", new UptoEvmClientScheme(evmSigner, uptoSchemeOptions))
90+
.register("eip155:*", batchSettlementScheme)
7291
.registerV1("base-sepolia", new ExactEvmSchemeV1(evmSigner))
7392
.registerV1("base", new ExactEvmSchemeV1(evmSigner))
7493
.register("solana:*", new ExactSvmScheme(svmSigner))
@@ -85,33 +104,64 @@ if (avmSigner) {
85104
}
86105

87106
const fetchWithPayment = wrapFetchWithPayment(fetch, client);
88-
89-
fetchWithPayment(url, {
90-
method: "GET",
91-
}).then(async response => {
107+
const httpClient = new x402HTTPClient(client);
108+
109+
// Multi-request scenarios (used by batch-settlement) issue several paid requests
110+
// against the same endpoint so the server can amortise on-chain claims, then
111+
// optionally signal a cooperative refund on the last request.
112+
const numberOfRequests = Number.parseInt(process.env.MULTI_REQUEST_COUNT ?? "1", 10);
113+
const refundOnLastRequest = process.env.REFUND_ON_LAST === "true";
114+
115+
/**
116+
* Issues a single paid request and returns the parsed result.
117+
*
118+
* @returns Structured result with response data and decoded payment-response.
119+
*/
120+
async function issueRequest(): Promise<{
121+
success: boolean;
122+
data: unknown;
123+
status_code: number;
124+
payment_response?: ReturnType<x402HTTPClient["getPaymentSettleResponse"]>;
125+
}> {
126+
const response = await fetchWithPayment(url, { method: "GET" });
92127
const data = await response.json();
93-
const paymentResponse = new x402HTTPClient(client).getPaymentSettleResponse((name) => response.headers.get(name));
128+
const paymentResponse = httpClient.getPaymentSettleResponse(name => response.headers.get(name));
94129

95130
if (!paymentResponse) {
96-
// No payment was required
97-
const result = {
98-
success: true,
99-
data: data,
100-
status_code: response.status,
101-
};
102-
console.log(JSON.stringify(result));
103-
process.exit(0);
104-
return;
131+
return { success: true, data, status_code: response.status };
105132
}
106133

107-
const result = {
134+
return {
108135
success: paymentResponse.success,
109-
data: data,
136+
data,
110137
status_code: response.status,
111138
payment_response: paymentResponse,
112139
};
140+
}
113141

114-
// Output structured result as JSON for proxy to parse
115-
console.log(JSON.stringify(result));
116-
process.exit(0);
117-
});
142+
const results: Awaited<ReturnType<typeof issueRequest>>[] = [];
143+
let lastChannelId: string | undefined;
144+
for (let i = 0; i < numberOfRequests; i++) {
145+
const isLast = i === numberOfRequests - 1;
146+
147+
if (isLast && refundOnLastRequest && lastChannelId) {
148+
batchSettlementScheme.requestRefund(lastChannelId);
149+
}
150+
151+
const result = await issueRequest();
152+
results.push(result);
153+
154+
const channelId = result.payment_response?.extra?.channelId;
155+
if (typeof channelId === "string" && channelId.length > 0) {
156+
lastChannelId = channelId;
157+
}
158+
}
159+
160+
const last = results[results.length - 1]!;
161+
const aggregate =
162+
numberOfRequests > 1
163+
? { ...last, requests: results, request_count: numberOfRequests }
164+
: last;
165+
166+
console.log(JSON.stringify(aggregate));
167+
process.exit(0);

e2e/clients/fetch/test.config.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
"transferMethods": [
1818
"eip3009",
1919
"permit2",
20-
"upto"
20+
"upto",
21+
"batch-settlement"
2122
]
2223
},
2324
"extensions": [
@@ -34,7 +35,11 @@
3435
"optional": [
3536
"AVM_PRIVATE_KEY",
3637
"APTOS_PRIVATE_KEY",
37-
"STELLAR_PRIVATE_KEY"
38+
"STELLAR_PRIVATE_KEY",
39+
"CHANNEL_SALT",
40+
"MULTI_REQUEST_COUNT",
41+
"REFUND_ON_LAST",
42+
"EVM_VOUCHER_SIGNER_PRIVATE_KEY"
3843
]
3944
}
4045
}

0 commit comments

Comments
 (0)