diff --git a/bun.lock b/bun.lock index 97bde00..ea55549 100644 --- a/bun.lock +++ b/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=="], diff --git a/package.json b/package.json index f07b4b2..cedbe7c 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/scripts/test-x402.ts b/scripts/test-x402.ts new file mode 100644 index 0000000..f995a8c --- /dev/null +++ b/scripts/test-x402.ts @@ -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(); diff --git a/server/index.ts b/server/index.ts index e71441b..b0a50f1 100644 --- a/server/index.ts +++ b/server/index.ts @@ -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((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 || "";