feat: add x402 test endpoint and payment test script
Add POST /api/x402-test — a standalone payment-gated endpoint with no auth required, for testing the x402 flow end-to-end. Add scripts/test-x402.ts using @x402/fetch to automatically handle the 402 → sign → retry cycle on Base Sepolia. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2a7071edb4
commit
74b15ba1b7
26
bun.lock
26
bun.lock
|
|
@ -10,7 +10,7 @@
|
|||
"@encryptid/sdk": "file:../encryptid-sdk",
|
||||
"@lit/reactive-element": "^2.0.4",
|
||||
"@x402/core": "^2.3.1",
|
||||
"@x402/evm": "^2.3.1",
|
||||
"@x402/evm": "^2.5.0",
|
||||
"hono": "^4.11.7",
|
||||
"imapflow": "^1.0.170",
|
||||
"mailparser": "^3.7.2",
|
||||
|
|
@ -24,8 +24,10 @@
|
|||
"@types/mailparser": "^3.4.0",
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/nodemailer": "^6.4.0",
|
||||
"@x402/fetch": "^2.5.0",
|
||||
"bun-types": "^1.1.38",
|
||||
"typescript": "^5.7.2",
|
||||
"viem": "^2.46.3",
|
||||
"vite": "^6.0.3",
|
||||
"vite-plugin-top-level-await": "^1.6.0",
|
||||
"vite-plugin-wasm": "^3.5.0",
|
||||
|
|
@ -217,9 +219,9 @@
|
|||
|
||||
"@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="],
|
||||
|
||||
"@noble/curves": ["@noble/curves@2.0.1", "", { "dependencies": { "@noble/hashes": "2.0.1" } }, "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw=="],
|
||||
"@noble/curves": ["@noble/curves@1.9.1", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA=="],
|
||||
|
||||
"@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="],
|
||||
"@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
|
||||
|
||||
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
|
||||
|
||||
|
|
@ -433,6 +435,8 @@
|
|||
|
||||
"@x402/extensions": ["@x402/extensions@2.5.0", "", { "dependencies": { "@scure/base": "^1.2.6", "@x402/core": "~2.5.0", "ajv": "^8.17.1", "siwe": "^2.3.2", "tweetnacl": "^1.0.3", "viem": "^2.43.5", "zod": "^3.24.2" } }, "sha512-e7IQShbGUM/XQmzI8DQh2tX/k2XDUGI9DNF+ij2NHUyPEqAt5/mJCwOlaxS/60FWFdfFRfWjTsqaoS7Z8WLi+A=="],
|
||||
|
||||
"@x402/fetch": ["@x402/fetch@2.5.0", "", { "dependencies": { "@x402/core": "~2.5.0", "viem": "^2.39.3", "zod": "^3.24.2" } }, "sha512-D2jH3bn0nf8w9Jg3Vxo+6reE6Z9GickzkSIw+udITJFvsrGOpfjZvhcTeflLcthCODk4Nuu9Oe8x7Q3NLUdaRQ=="],
|
||||
|
||||
"@zone-eu/mailsplit": ["@zone-eu/mailsplit@5.4.8", "", { "dependencies": { "libbase64": "1.3.0", "libmime": "5.3.7", "libqp": "2.1.1" } }, "sha512-eEyACj4JZ7sjzRvy26QhLgKEMWwQbsw1+QZnlLX+/gihcNH07lVPOcnwf5U6UAL7gkc//J3jVd76o/WS+taUiA=="],
|
||||
|
||||
"abitype": ["abitype@1.2.3", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg=="],
|
||||
|
|
@ -635,13 +639,9 @@
|
|||
|
||||
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
|
||||
|
||||
"@scure/bip32/@noble/curves": ["@noble/curves@1.9.1", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA=="],
|
||||
"@encryptid/sdk/@noble/curves": ["@noble/curves@2.0.1", "", { "dependencies": { "@noble/hashes": "2.0.1" } }, "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw=="],
|
||||
|
||||
"@scure/bip32/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
|
||||
|
||||
"@scure/bip39/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
|
||||
|
||||
"@spruceid/siwe-parser/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
|
||||
"@encryptid/sdk/@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="],
|
||||
|
||||
"ethers/@adraffy/ens-normalize": ["@adraffy/ens-normalize@1.10.1", "", {}, "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw=="],
|
||||
|
||||
|
|
@ -663,14 +663,6 @@
|
|||
|
||||
"mailparser/nodemailer": ["nodemailer@7.0.13", "", {}, "sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw=="],
|
||||
|
||||
"ox/@noble/curves": ["@noble/curves@1.9.1", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA=="],
|
||||
|
||||
"ox/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
|
||||
|
||||
"viem/@noble/curves": ["@noble/curves@1.9.1", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA=="],
|
||||
|
||||
"viem/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
|
||||
|
||||
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
||||
|
||||
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
||||
|
|
|
|||
|
|
@ -26,14 +26,16 @@
|
|||
"imapflow": "^1.0.170",
|
||||
"mailparser": "^3.7.2",
|
||||
"@x402/core": "^2.3.1",
|
||||
"@x402/evm": "^2.3.1"
|
||||
"@x402/evm": "^2.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/nodemailer": "^6.4.0",
|
||||
"@types/mailparser": "^3.4.0",
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/nodemailer": "^6.4.0",
|
||||
"@x402/fetch": "^2.5.0",
|
||||
"bun-types": "^1.1.38",
|
||||
"typescript": "^5.7.2",
|
||||
"viem": "^2.46.3",
|
||||
"vite": "^6.0.3",
|
||||
"vite-plugin-top-level-await": "^1.6.0",
|
||||
"vite-plugin-wasm": "^3.5.0"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
/**
|
||||
* test-x402.ts — End-to-end x402 payment test against rSpace rSplat.
|
||||
*
|
||||
* Tests the full 402 flow:
|
||||
* 1. POST to the gated endpoint → expect 402 with payment requirements
|
||||
* 2. Sign USDC payment on Base Sepolia using EIP-3009
|
||||
* 3. Retry with X-PAYMENT header → expect 200/201
|
||||
*
|
||||
* Usage:
|
||||
* EVM_PRIVATE_KEY=0x... bun run scripts/test-x402.ts [url]
|
||||
*
|
||||
* Defaults to https://demo.rspace.online/rsplat/api/splats
|
||||
* Wallet needs testnet USDC from https://faucet.circle.com
|
||||
*/
|
||||
|
||||
import { x402Client, wrapFetchWithPayment } from "@x402/fetch";
|
||||
import { ExactEvmScheme } from "@x402/evm/exact/client";
|
||||
import { privateKeyToAccount } from "viem/accounts";
|
||||
|
||||
const PRIVATE_KEY = process.env.EVM_PRIVATE_KEY as `0x${string}`;
|
||||
if (!PRIVATE_KEY) {
|
||||
console.error("Set EVM_PRIVATE_KEY env var (0x-prefixed hex private key)");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const TARGET_URL =
|
||||
process.argv[2] || "https://demo.rspace.online/api/x402-test";
|
||||
|
||||
async function main() {
|
||||
console.log("=== x402 Payment Test ===\n");
|
||||
console.log("Target:", TARGET_URL);
|
||||
|
||||
// Step 1: Verify the endpoint returns 402
|
||||
console.log("\n[1] Testing 402 response (no payment)...");
|
||||
const raw = await fetch(TARGET_URL, { method: "POST" });
|
||||
console.log(" Status:", raw.status);
|
||||
|
||||
if (raw.status !== 402) {
|
||||
console.log(" Expected 402, got", raw.status);
|
||||
console.log(" Body:", await raw.text());
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const body = (await raw.json()) as {
|
||||
paymentRequirements?: {
|
||||
payTo: string;
|
||||
network: string;
|
||||
maxAmountRequired: string;
|
||||
};
|
||||
};
|
||||
console.log(" Payment requirements:");
|
||||
console.log(" payTo:", body.paymentRequirements?.payTo);
|
||||
console.log(" network:", body.paymentRequirements?.network);
|
||||
console.log(" amount:", body.paymentRequirements?.maxAmountRequired);
|
||||
|
||||
// Step 2: Set up x402 client with EVM signer
|
||||
console.log("\n[2] Setting up x402 client...");
|
||||
const account = privateKeyToAccount(PRIVATE_KEY);
|
||||
console.log(" Wallet:", account.address);
|
||||
|
||||
const client = new x402Client();
|
||||
client.register("eip155:84532", new ExactEvmScheme(account));
|
||||
|
||||
const paidFetch = wrapFetchWithPayment(fetch, client);
|
||||
|
||||
// Step 3: Make the paid request
|
||||
console.log("\n[3] Making paid request (x402 client handles signing automatically)...");
|
||||
|
||||
try {
|
||||
const res = await paidFetch(TARGET_URL, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ test: true }),
|
||||
});
|
||||
|
||||
console.log(" Status:", res.status);
|
||||
const resBody = await res.text();
|
||||
try {
|
||||
console.log(
|
||||
" Response:",
|
||||
JSON.stringify(JSON.parse(resBody), null, 2),
|
||||
);
|
||||
} catch {
|
||||
console.log(" Response:", resBody.slice(0, 500));
|
||||
}
|
||||
|
||||
if (res.ok) {
|
||||
console.log("\n x402 payment successful! USDC transferred on Base Sepolia.");
|
||||
} else if (res.status === 402) {
|
||||
console.log("\n Still 402 — payment signing or verification failed.");
|
||||
console.log(
|
||||
" Check wallet has USDC on Base Sepolia (faucet: https://faucet.circle.com)",
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(" Error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -488,6 +488,21 @@ app.get("/api/modules", (c) => {
|
|||
return c.json({ modules: getModuleInfoList() });
|
||||
});
|
||||
|
||||
// ── x402 test endpoint (no auth, payment-gated only) ──
|
||||
import { setupX402FromEnv } from "../shared/x402/hono-middleware";
|
||||
const x402Test = setupX402FromEnv({ description: "x402 test endpoint", resource: "/api/x402-test" });
|
||||
app.post("/api/x402-test", async (c) => {
|
||||
if (x402Test) {
|
||||
const result = await new Promise<Response | null>((resolve) => {
|
||||
x402Test(c, async () => { resolve(null); }).then((res) => {
|
||||
if (res instanceof Response) resolve(res);
|
||||
});
|
||||
});
|
||||
if (result) return result;
|
||||
}
|
||||
return c.json({ ok: true, message: "Payment received!", timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// ── Creative tools API endpoints ──
|
||||
|
||||
const FAL_KEY = process.env.FAL_KEY || "";
|
||||
|
|
|
|||
Loading…
Reference in New Issue