/** * x402 Hono middleware — reusable payment gate for rSpace modules. * * Supports two payment schemes: * - "exact": On-chain USDC via x402 facilitator (original) * - "crdt": Local cUSDC balance via CRDT token ledger (bridge) * * When X402_PAY_TO env is set, protects routes with x402 micro-transactions. * When not set, acts as a no-op passthrough. */ import type { Context, Next, MiddlewareHandler } from "hono"; import type { CrdtPaymentPayload } from "./crdt-scheme"; export interface CrdtPaymentConfig { tokenId: string; amount: number; payToDid: string; payToLabel: string; // Injected to avoid shared/ → server/ import getBalance: (doc: any, did: string) => number; getTokenDoc: (tokenId: string) => any; transferTokens: ( tokenId: string, fromDid: string, fromLabel: string, toDid: string, toLabel: string, amount: number, memo: string, issuedBy: string, ) => boolean; } export interface X402Config { payTo: string; network: string; amount: string; facilitatorUrl: string; resource?: string; description?: string; /** Called after on-chain payment is verified (Direction 1: on-chain → CRDT mint) */ onPaymentSettled?: (params: { paymentHeader: string; context: Context }) => Promise; /** CRDT payment config (Direction 2: CRDT balance as payment) */ crdtPayment?: CrdtPaymentConfig; } /** * Create x402 payment middleware for Hono routes. */ export function createX402Middleware(config: X402Config): MiddlewareHandler { // Lazy-load crdt-scheme to avoid circular imports let verifyCrdtPayment: typeof import("./crdt-scheme").verifyCrdtPayment | null = null; return async (c: Context, next: Next) => { const paymentHeader = c.req.header("X-PAYMENT"); if (!paymentHeader) { // Build payment requirements array with available schemes const requirements: any[] = [ { scheme: "exact", network: config.network, maxAmountRequired: config.amount, resource: config.resource || c.req.url, description: config.description || "Payment required", payTo: config.payTo, maxTimeoutSeconds: 300, }, ]; // Add CRDT option if configured if (config.crdtPayment) { requirements.push({ scheme: "crdt", network: "rspace:local", tokenId: config.crdtPayment.tokenId, maxAmountRequired: String(config.crdtPayment.amount), resource: config.resource || c.req.url, description: config.description || "Payment required", payTo: config.crdtPayment.payToDid, }); } return c.json( { error: "Payment Required", x402Version: 1, paymentRequirements: requirements, }, 402, { "X-PAYMENT-REQUIREMENTS": JSON.stringify(requirements) } ); } // Decode payment header to detect scheme let decoded: any; try { decoded = JSON.parse( typeof atob === "function" ? atob(paymentHeader) : Buffer.from(paymentHeader, "base64").toString("utf-8") ); } catch { // Not base64 JSON — treat as legacy exact scheme decoded = { scheme: "exact" }; } // ── CRDT scheme ── if (decoded.scheme === "crdt" && config.crdtPayment) { try { // Lazy-load verifier if (!verifyCrdtPayment) { const mod = await import("./crdt-scheme"); verifyCrdtPayment = mod.verifyCrdtPayment; } const payload = decoded as CrdtPaymentPayload; const result = await verifyCrdtPayment( payload, config.crdtPayment.amount, config.crdtPayment.tokenId, ); if (!result.valid) { return c.json({ error: "CRDT payment invalid", details: result.error }, 402); } // Check balance const doc = config.crdtPayment.getTokenDoc(config.crdtPayment.tokenId); if (!doc) { return c.json({ error: "Token ledger not found" }, 500); } const balance = config.crdtPayment.getBalance(doc, result.fromDid!); if (balance < config.crdtPayment.amount) { return c.json({ error: "Insufficient cUSDC balance", required: config.crdtPayment.amount, available: balance, }, 402); } // Execute transfer: payer → treasury const transferred = config.crdtPayment.transferTokens( config.crdtPayment.tokenId, result.fromDid!, result.fromLabel || result.fromDid!, config.crdtPayment.payToDid, config.crdtPayment.payToLabel, config.crdtPayment.amount, `x402 CRDT payment for ${config.resource || c.req.url}`, 'x402-crdt', ); if (!transferred) { return c.json({ error: "CRDT transfer failed" }, 500); } c.set("x402Payment", paymentHeader); c.set("x402Scheme", "crdt"); await next(); return; } catch (e) { console.error("[x402] CRDT verification error:", e); return c.json({ error: "CRDT payment verification failed" }, 500); } } // ── Exact (on-chain) scheme ── try { const verifyRes = await fetch(`${config.facilitatorUrl}/verify`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ payment: paymentHeader, requirements: { scheme: "exact", network: config.network, maxAmountRequired: config.amount, payTo: config.payTo, }, }), }); if (!verifyRes.ok) { const err = await verifyRes.text(); return c.json({ error: "Payment verification failed", details: err }, 402); } const result = await verifyRes.json() as { valid?: boolean }; if (!result.valid) { return c.json({ error: "Payment invalid or insufficient" }, 402); } // Payment valid — store tx info for downstream handlers c.set("x402Payment", paymentHeader); c.set("x402Scheme", "exact"); // Direction 1: fire onPaymentSettled hook (mint cUSDC from on-chain) if (config.onPaymentSettled) { try { await config.onPaymentSettled({ paymentHeader, context: c }); } catch (e) { console.warn("[x402] onPaymentSettled hook error (non-fatal):", e); } } await next(); } catch (e) { console.error("[x402] Verification error:", e); return c.json({ error: "Payment verification service unavailable" }, 503); } }; } /** * Initialize x402 from environment variables. * Returns middleware or null if disabled. */ export function setupX402FromEnv(overrides?: Partial): MiddlewareHandler | null { const payTo = process.env.X402_PAY_TO; if (!payTo) { console.log("[x402] Disabled — X402_PAY_TO not set"); return null; } const config: X402Config = { payTo, network: process.env.X402_NETWORK || "eip155:84532", amount: process.env.X402_UPLOAD_PRICE || "0.01", facilitatorUrl: process.env.X402_FACILITATOR_URL || "https://www.x402.org/facilitator", ...overrides, }; console.log(`[x402] Enabled — payTo=${payTo}, network=${config.network}, amount=${config.amount}`); return createX402Middleware(config); }