diff --git a/src/lib/choices/framework.ts b/src/lib/choices/framework.ts index ea3791a..21ab441 100644 --- a/src/lib/choices/framework.ts +++ b/src/lib/choices/framework.ts @@ -1,7 +1,7 @@ import { Framework } from '../types/framework'; export const FRAMEWORK_OPTIONS = ( - Object.keys({ nextjs: null, 'vite-react': null } as Record< + Object.keys({ nextjs: null, 'vite-react': null, node: null } as Record< Framework, null >) as Framework[] diff --git a/src/lib/choices/templates.ts b/src/lib/choices/templates.ts index 2486347..50427b9 100644 --- a/src/lib/choices/templates.ts +++ b/src/lib/choices/templates.ts @@ -49,7 +49,19 @@ const VITE_REACT_TEMPLATE_OPTIONS: ITemplate[] = [ }, ]; +const NODE_TEMPLATE_OPTIONS: ITemplate[] = [ + { + name: 'x402 Server (ERC-7710)', + value: 'x402-server', + description: + 'A Node.js Express server with x402 payment middleware and ERC-7710 support', + framework: 'node', + isWeb3AuthSupported: false, + }, +]; + export const TEMPLATE_OPTIONS: ITemplate[] = [ ...NEXTJS_TEMPLATE_OPTIONS, ...VITE_REACT_TEMPLATE_OPTIONS, + ...NODE_TEMPLATE_OPTIONS, ]; diff --git a/src/lib/types/framework.ts b/src/lib/types/framework.ts index c79eeb2..85bae12 100644 --- a/src/lib/types/framework.ts +++ b/src/lib/types/framework.ts @@ -1 +1 @@ -export type Framework = 'nextjs' | 'vite-react'; +export type Framework = 'nextjs' | 'vite-react' | 'node'; diff --git a/templates/node/x402-server/.env.example b/templates/node/x402-server/.env.example new file mode 100644 index 0000000..98bbbeb --- /dev/null +++ b/templates/node/x402-server/.env.example @@ -0,0 +1,5 @@ +# Address that receives payment for the protected resource +PAY_TO_ADDRESS=0x... + +# Override the default MetaMask Sentinel facilitator URL +FACILITATOR_URL=https://tx-sentinel-base-mainnet.dev-api.cx.metamask.io/platform/v2/x402 diff --git a/templates/node/x402-server/.gitignore b/templates/node/x402-server/.gitignore new file mode 100644 index 0000000..589f366 --- /dev/null +++ b/templates/node/x402-server/.gitignore @@ -0,0 +1,22 @@ +# dependencies +node_modules/ + +# build output +dist/ + +# env files +.env + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# typescript +*.tsbuildinfo +.npmrc diff --git a/templates/node/x402-server/package.json b/templates/node/x402-server/package.json new file mode 100644 index 0000000..5c5a976 --- /dev/null +++ b/templates/node/x402-server/package.json @@ -0,0 +1,26 @@ +{ + "name": "x402-server", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc", + "dev": "tsx watch src/index.ts", + "start": "node dist/index.js" + }, + "dependencies": { + "@x402/core": "^2.12.0", + "@x402/evm": "^2.12.0", + "@x402/express": "^2.12.0", + "cors": "^2.8.5", + "dotenv": "^16.4.7", + "express": "^4.21.2" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^5.0.2", + "@types/node": "^22.15.3", + "tsx": "^4.19.4", + "typescript": "^5.8.3" + } +} diff --git a/templates/node/x402-server/src/index.ts b/templates/node/x402-server/src/index.ts new file mode 100644 index 0000000..ca118d4 --- /dev/null +++ b/templates/node/x402-server/src/index.ts @@ -0,0 +1,62 @@ +import { config } from "dotenv"; +import express, { type Request, type Response } from "express"; +import cors from "cors"; +import { paymentMiddleware } from "@x402/express"; +import { x402ResourceServer, HTTPFacilitatorClient } from "@x402/core/server"; +import { Erc7710ExactEvmScheme } from "./scheme.js"; + +config(); + +const NETWORK_ID = "eip155:8453"; +const PORT = 4402; + +const payToAddress = process.env.PAY_TO_ADDRESS as string; +if (!payToAddress) { + console.error("PAY_TO_ADDRESS environment variable is required"); + process.exit(1); +} + +const facilitatorUrl = process.env.FACILITATOR_URL as string; +if (!facilitatorUrl) { + console.error("FACILITATOR_URL environment variable is required"); + process.exit(1); +} + +const facilitatorClient = new HTTPFacilitatorClient({ url: facilitatorUrl }); + +const app = express(); +app.use(cors({ exposedHeaders: ["PAYMENT-REQUIRED", "PAYMENT-RESPONSE"] })); + +app.use( + paymentMiddleware( + { + "GET /api/hello": { + accepts: [ + { + scheme: "exact", + price: "$0.01", + network: NETWORK_ID, + payTo: payToAddress, + }, + ], + description: "Access to protected resource", + mimeType: "application/json", + }, + }, + new x402ResourceServer(facilitatorClient).register( + NETWORK_ID, + new Erc7710ExactEvmScheme(facilitatorClient), + ), + ), +); + +app.get("/api/hello", (_req: Request, res: Response) => { + res.json({ message: "Hello!" }); +}); + +app.listen(PORT, () => { + console.log(`[seller] Server running on http://localhost:${PORT}`); + console.log(`[seller] Pay-to address: ${payToAddress}`); + console.log(`[seller] Facilitator URL: ${facilitatorUrl}`); + console.log(`[seller] Network: ${NETWORK_ID}`); +}); diff --git a/templates/node/x402-server/src/scheme.ts b/templates/node/x402-server/src/scheme.ts new file mode 100644 index 0000000..900b3eb --- /dev/null +++ b/templates/node/x402-server/src/scheme.ts @@ -0,0 +1,39 @@ +import { ExactEvmScheme } from "@x402/evm/exact/server"; +import type { + PaymentRequirements, + SupportedKind, +} from "@x402/core/types"; +import type { FacilitatorClient } from "@x402/core/server"; + +export class Erc7710ExactEvmScheme extends ExactEvmScheme { + constructor(private readonly facilitatorClient: FacilitatorClient) { + super(); + } + + async enhancePaymentRequirements( + paymentRequirements: PaymentRequirements, + supportedKind: SupportedKind, + facilitatorExtensions: string[], + ): Promise { + const enhanced = await super.enhancePaymentRequirements( + paymentRequirements, + supportedKind, + facilitatorExtensions, + ); + + const supported = await this.facilitatorClient.getSupported(); + const facilitators = [ + ...(supported.signers[paymentRequirements.network] ?? []), + ...(supported.signers["eip155:*"] ?? []) + ]; + + return { + ...enhanced, + extra: { + ...enhanced.extra, + assetTransferMethod: "erc7710", + facilitators, + }, + }; + } +} diff --git a/templates/node/x402-server/tsconfig.json b/templates/node/x402-server/tsconfig.json new file mode 100644 index 0000000..e710f00 --- /dev/null +++ b/templates/node/x402-server/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "strict": true, + "outDir": "dist", + "rootDir": "src", + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true + }, + "include": ["src"] +}