From 0387aa00f945a5ea2486c106c7ee4ea8c4b06e0f Mon Sep 17 00:00:00 2001 From: KAMIYO Date: Thu, 5 Feb 2026 12:36:47 +0100 Subject: [PATCH 1/7] Add x402 payment protocol support --- examples/x402-payment.ts | 116 ++++++++ src/index.ts | 20 ++ src/x402.ts | 590 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 726 insertions(+) create mode 100644 examples/x402-payment.ts create mode 100644 src/x402.ts diff --git a/examples/x402-payment.ts b/examples/x402-payment.ts new file mode 100644 index 0000000..846427f --- /dev/null +++ b/examples/x402-payment.ts @@ -0,0 +1,116 @@ +/** + * x402 Payment Protocol examples. + * + * Shows both client-side (paying for APIs) and server-side (monetizing APIs) + * usage with ShadowWire private transfers. + */ + +import { + ShadowWireClient, + X402Client, + x402Paywall, + createDiscoveryDocument, +} from '@radr/shadowwire'; + +// --------------------------------------------------------------------------- +// Client: Paying for a paid API endpoint +// --------------------------------------------------------------------------- + +async function clientExample() { + const client = new ShadowWireClient(); + + // Use @solana/wallet-adapter in real apps + const wallet = { + signMessage: async (message: Uint8Array) => { + throw new Error('Implement wallet signing'); + }, + }; + + const x402 = new X402Client({ + client, + wallet, + senderWallet: 'YOUR_WALLET_ADDRESS', + defaultToken: 'USDC', + // 'external' = sender anonymous, amount visible + // 'internal' = both parties private, amount hidden + defaultTransferType: 'external', + }); + + // Automatic flow: request -> detect 402 -> pay -> retry + const result = await x402.request('https://api.example.com/data'); + + if (result.success) { + console.log('Data:', result.data); + if (result.payment) { + console.log('Paid:', result.payment.transfer.tx_signature); + console.log('Amount hidden:', result.payment.transfer.amount_hidden); + } + } + + // Manual flow: parse 402 and pay individually + const response = await fetch('https://api.example.com/premium'); + if (response.status === 402) { + const body = await response.json(); + const requirements = X402Client.parseRequirements(body); + if (requirements) { + const req = requirements.accepts[0]; + const payment = await x402.pay(req); + if (payment.success) { + // Retry with payment header + await fetch('https://api.example.com/premium', { + headers: { 'X-Payment': payment.paymentHeader! }, + }); + } + } + } +} + +// --------------------------------------------------------------------------- +// Server: Monetizing API endpoints with Express +// --------------------------------------------------------------------------- + +async function serverExample() { + // Requires: npm install express + const express = require('express'); + const app = express(); + + // Protect an endpoint: requests without payment get a 402 response. + // ShadowWire payments are verified automatically. + app.get( + '/api/data', + x402Paywall({ + payTo: 'YOUR_MERCHANT_WALLET', + amount: 0.01, // $0.01 USDC + asset: 'USDC', + description: 'Premium data endpoint', + // Optional: also accept payments via PayAI facilitator + facilitatorUrl: 'https://facilitator.payai.network', + onPayment: (info) => { + console.log(`Payment from ${info.payer}: ${info.signature}`); + }, + }), + (req: any, res: any) => { + // req.x402 contains payment details + res.json({ + data: 'premium content', + payment: req.x402, + }); + } + ); + + // Discovery document for agents to find your paid endpoints + app.get('/.well-known/x402', (_req: any, res: any) => { + res.json(createDiscoveryDocument( + 'My API', + 'YOUR_MERCHANT_WALLET', + [ + { path: '/api/data', method: 'GET', price: 0.01, description: 'Premium data' }, + { path: '/api/signals', method: 'GET', price: 0.05, description: 'Trading signals' }, + ], + )); + }); + + app.listen(3000); +} + +clientExample().catch(console.error); diff --git a/src/index.ts b/src/index.ts index 6bd79f2..110678a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -61,3 +61,23 @@ export type { } from './types'; export { TOKEN_FEES, TOKEN_MINIMUMS, TOKEN_MINTS, TOKEN_DECIMALS } from './constants'; + +export { + X402Client, + x402Paywall, + createPaymentRequired, + verifyPayment, + settlePayment, + createDiscoveryDocument, +} from './x402'; +export type { + X402PaymentRequirement, + X402Response, + X402PaymentResult, + X402RequestResult, + X402ClientConfig, + X402VerifyResult, + X402MiddlewareConfig, + X402PaymentProof, + X402DiscoveryResource, +} from './x402'; diff --git a/src/x402.ts b/src/x402.ts new file mode 100644 index 0000000..ae16787 --- /dev/null +++ b/src/x402.ts @@ -0,0 +1,590 @@ +/** + * x402 Payment Protocol integration for ShadowWire. + * + * Implements the x402 (HTTP 402 Payment Required) spec with ShadowWire + * private transfers. Includes both client-side (paying for APIs) and + * server-side (protecting endpoints with paywalls) components. + * + * Payment flow: + * 1. Client requests paid endpoint + * 2. Server returns 402 with payment requirements (including shadowwire scheme) + * 3. Client pays via ShadowWire (amount hidden via Bulletproofs) + * 4. Client retries request with X-Payment header containing transfer proof + * 5. Server verifies the ShadowWire transfer via facilitator and serves the resource + * + * Spec: https://github.com/coinbase/x402 + */ + +import { ShadowWireClient } from './client'; +import { TokenSymbol, WalletAdapter, TransferResponse, PoolBalance } from './types'; +import { NetworkError } from './errors'; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export interface X402PaymentRequirement { + scheme: string; + network: string; + amount: string; + asset: string; + payTo: string; + resource: string; + description?: string; + maxTimeoutSeconds?: number; + extra?: Record; +} + +export interface X402Response { + x402Version: number; + accepts: X402PaymentRequirement[]; + error?: string; + facilitator?: string; + resource?: { + url: string; + description?: string; + mimeType?: string; + }; + [key: string]: unknown; +} + +export interface X402PaymentResult { + success: boolean; + transfer?: TransferResponse; + paymentHeader?: string; + error?: string; +} + +export interface X402RequestResult { + success: boolean; + data?: T; + payment?: { + transfer: TransferResponse; + requirement: X402PaymentRequirement; + }; + error?: string; + statusCode: number; +} + +export interface X402ClientConfig { + client: ShadowWireClient; + wallet: WalletAdapter; + senderWallet: string; + defaultToken?: TokenSymbol; + defaultTransferType?: 'internal' | 'external'; + maxRetries?: number; + headers?: Record; +} + +export interface X402VerifyResult { + valid: boolean; + payer?: string; + signature?: string; + amountHidden?: boolean; + error?: string; +} + +export interface X402MiddlewareConfig { + payTo: string; + amount: number; + asset?: TokenSymbol; + description?: string; + maxTimeoutSeconds?: number; + /** x402 facilitator URL for payment verification and settlement */ + facilitatorUrl: string; + /** Additional accepted payment schemes alongside shadowwire */ + additionalSchemes?: X402PaymentRequirement[]; + /** Called after successful payment verification */ + onPayment?: (info: { payer: string; amount: number; signature: string; resource: string }) => void; +} + +export interface X402PaymentProof { + x402Version: number; + scheme: string; + network: string; + payload: { + signature: string; + amountHidden: boolean; + resource: string; + payTo: string; + sender?: string; + }; +} + +// --------------------------------------------------------------------------- +// Client (payer side) +// --------------------------------------------------------------------------- + +export class X402Client { + private client: ShadowWireClient; + private wallet: WalletAdapter; + private senderWallet: string; + private defaultToken: TokenSymbol; + private defaultTransferType: 'internal' | 'external'; + private maxRetries: number; + private headers: Record; + + constructor(config: X402ClientConfig) { + this.client = config.client; + this.wallet = config.wallet; + this.senderWallet = config.senderWallet; + this.defaultToken = config.defaultToken || 'USDC'; + this.defaultTransferType = config.defaultTransferType || 'external'; + this.maxRetries = config.maxRetries ?? 1; + this.headers = config.headers || {}; + } + + /** + * Make a request to a URL that may require x402 payment. + * Handles the full flow: request -> 402 -> pay -> retry. + */ + async request(url: string, options?: RequestInit): Promise> { + const mergedHeaders: Record = { + ...this.headers, + ...(options?.headers as Record || {}), + }; + + const response = await this.doFetch(url, { ...options, headers: mergedHeaders }); + + if (response.status !== 402) { + if (!response.ok) { + return { + success: false, + error: `HTTP ${response.status}: ${response.statusText}`, + statusCode: response.status, + }; + } + const data = await response.json() as T; + return { success: true, data, statusCode: response.status }; + } + + const x402Body = await response.json() as X402Response; + if (!x402Body.accepts || x402Body.accepts.length === 0) { + return { success: false, error: 'No accepted payment methods in 402 response', statusCode: 402 }; + } + + const requirement = this.findCompatibleRequirement(x402Body.accepts); + if (!requirement) { + return { success: false, error: 'No compatible payment option (need Solana or ShadowWire)', statusCode: 402 }; + } + + const payResult = await this.pay(requirement); + if (!payResult.success || !payResult.transfer || !payResult.paymentHeader) { + return { success: false, error: payResult.error || 'Payment failed', statusCode: 402 }; + } + + for (let attempt = 0; attempt <= this.maxRetries; attempt++) { + const retryResponse = await this.doFetch(url, { + ...options, + headers: { ...mergedHeaders, 'X-Payment': payResult.paymentHeader }, + }); + + if (retryResponse.ok) { + const data = await retryResponse.json() as T; + return { + success: true, + data, + payment: { transfer: payResult.transfer, requirement }, + statusCode: retryResponse.status, + }; + } + + if (retryResponse.status === 402) { + return { success: false, error: 'Payment not accepted by server', statusCode: 402 }; + } + + if (attempt === this.maxRetries) { + return { + success: false, + error: `HTTP ${retryResponse.status} after payment`, + statusCode: retryResponse.status, + }; + } + } + + return { success: false, error: 'Unexpected error', statusCode: 500 }; + } + + /** + * Pay a specific x402 requirement via ShadowWire. + */ + async pay(requirement: X402PaymentRequirement): Promise { + const amount = this.parseAmount(requirement.amount, requirement.asset); + if (amount <= 0) { + return { success: false, error: 'Invalid payment amount' }; + } + + try { + const transfer = await this.client.transfer({ + sender: this.senderWallet, + recipient: requirement.payTo, + amount, + token: this.resolveToken(requirement.asset), + type: this.defaultTransferType, + wallet: this.wallet, + }); + + if (!transfer.success) { + return { success: false, error: 'ShadowWire transfer failed' }; + } + + const paymentHeader = X402Client.encodePaymentHeader({ + x402Version: 2, + scheme: 'shadowwire', + network: 'solana:mainnet', + payload: { + signature: transfer.tx_signature, + amountHidden: transfer.amount_hidden, + resource: requirement.resource, + payTo: requirement.payTo, + sender: this.senderWallet, + }, + }); + + return { success: true, transfer, paymentHeader }; + } catch (err) { + const message = err instanceof Error ? err.message : 'Unknown error'; + return { success: false, error: message }; + } + } + + /** + * Check sender's shielded balance for a given token. + */ + async getBalance(token?: TokenSymbol): Promise { + return this.client.getBalance(this.senderWallet, token || this.defaultToken); + } + + /** + * Estimate the fee for a payment amount. + */ + estimateFee(amount: number, token?: TokenSymbol): { fee: number; feePercentage: number; netAmount: number } { + return this.client.calculateFee(amount, token || this.defaultToken); + } + + // --- Static helpers --- + + static parseRequirements(body: unknown): X402Response | null { + if (!body || typeof body !== 'object') return null; + const obj = body as Record; + if (!Array.isArray(obj.accepts)) return null; + return obj as unknown as X402Response; + } + + static is402(status: number, body: unknown): boolean { + return status === 402 && X402Client.parseRequirements(body) !== null; + } + + static encodePaymentHeader(proof: X402PaymentProof): string { + return Buffer.from(JSON.stringify(proof)).toString('base64'); + } + + static decodePaymentHeader(header: string): X402PaymentProof | null { + try { + const decoded = JSON.parse(Buffer.from(header, 'base64').toString()); + if (!decoded.scheme || !decoded.payload?.signature) return null; + return decoded as X402PaymentProof; + } catch { + return null; + } + } + + // --- Private --- + + private findCompatibleRequirement(accepts: X402PaymentRequirement[]): X402PaymentRequirement | null { + const shadowReq = accepts.find((r) => + r.scheme === 'shadowwire' || r.scheme === 'shadow' + ); + if (shadowReq) return shadowReq; + + return accepts.find((r) => r.network?.includes('solana')) || null; + } + + private parseAmount(amountStr: string, asset: string): number { + const raw = parseInt(amountStr, 10); + if (isNaN(raw) || raw <= 0) return 0; + + const token = this.resolveToken(asset); + try { + const { TokenUtils } = require('./tokens'); + return TokenUtils.fromSmallestUnit(raw, token); + } catch { + return raw / 1_000_000; + } + } + + private resolveToken(asset: string): TokenSymbol { + const upper = asset.toUpperCase(); + try { + const { TokenUtils } = require('./tokens'); + if (TokenUtils.isValidToken(upper)) return upper as TokenSymbol; + } catch {} + return this.defaultToken; + } + + private async doFetch(url: string, options?: RequestInit): Promise { + try { + return await (globalThis as any).fetch(url, options); + } catch (err) { + throw new NetworkError( + err instanceof Error ? `x402 request failed: ${err.message}` : 'x402 request failed' + ); + } + } +} + +// --------------------------------------------------------------------------- +// Server middleware (payee side) +// --------------------------------------------------------------------------- + +/** + * Create a 402 payment requirement response body. + */ +export function createPaymentRequired( + resource: string, + config: X402MiddlewareConfig +): X402Response { + const amount = Math.floor(config.amount * 1_000_000).toString(); + + const accepts: X402PaymentRequirement[] = [ + { + scheme: 'shadowwire', + network: 'solana:mainnet', + amount, + asset: config.asset || 'USDC', + payTo: config.payTo, + resource, + description: config.description, + maxTimeoutSeconds: config.maxTimeoutSeconds || 60, + extra: { + transferTypes: ['internal', 'external'], + amountHidden: true, + }, + }, + ]; + + if (config.additionalSchemes) { + accepts.push(...config.additionalSchemes); + } + + return { + x402Version: 2, + accepts, + error: 'Payment Required', + facilitator: config.facilitatorUrl, + resource: { + url: resource, + description: config.description, + mimeType: 'application/json', + }, + }; +} + +/** + * Verify a payment via the configured x402 facilitator. + * The facilitator handles ShadowWire transfer verification, + * settlement, escrow, and dispute resolution. + */ +export async function verifyPayment( + paymentHeader: string, + requirement: X402PaymentRequirement, + facilitatorUrl: string +): Promise { + try { + const response = await (globalThis as any).fetch(`${facilitatorUrl}/verify`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + x402Version: 2, + paymentHeader, + paymentRequirements: requirement, + }), + }); + + if (!response.ok) { + return { valid: false, error: `Facilitator returned ${response.status}` }; + } + + const data = await response.json() as { + isValid?: boolean; + payer?: string; + signature?: string; + amountHidden?: boolean; + invalidReason?: string; + }; + + return { + valid: !!data.isValid, + payer: data.payer, + signature: data.signature, + amountHidden: data.amountHidden, + error: data.invalidReason, + }; + } catch (err) { + const message = err instanceof Error ? err.message : 'Unknown error'; + return { valid: false, error: `Facilitator error: ${message}` }; + } +} + +/** + * Settle a verified payment via the facilitator. + */ +export async function settlePayment( + paymentHeader: string, + requirement: X402PaymentRequirement, + facilitatorUrl: string +): Promise<{ success: boolean; tx?: string; error?: string }> { + try { + const response = await (globalThis as any).fetch(`${facilitatorUrl}/settle`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + x402Version: 2, + paymentHeader, + paymentRequirements: requirement, + }), + }); + + if (!response.ok) { + return { success: false, error: `Settlement returned ${response.status}` }; + } + + const data = await response.json() as { + success?: boolean; + transaction?: string; + error?: string; + }; + + return { + success: !!data.success, + tx: data.transaction, + error: data.error, + }; + } catch (err) { + const message = err instanceof Error ? err.message : 'Unknown error'; + return { success: false, error: `Settlement error: ${message}` }; + } +} + +/** + * Express-compatible middleware factory. + * + * Protects routes with x402 payment requirements. Verification and + * settlement are delegated to the configured facilitator. + * + * Usage: + * app.get('/api/data', x402Paywall({ + * payTo: 'WALLET', + * amount: 0.01, + * facilitatorUrl: 'https://x402.kamiyo.ai', + * }), handler); + */ +export function x402Paywall(config: X402MiddlewareConfig) { + return async (req: any, res: any, next: any) => { + const resource = req.path || req.url || '/'; + const paymentHeader = req.headers?.['x-payment'] as string | undefined; + + if (!paymentHeader) { + const body = createPaymentRequired(resource, config); + res.setHeader?.('WWW-Authenticate', 'X402'); + res.setHeader?.('X-Payment-Schemes', 'shadowwire'); + return res.status(402).json(body); + } + + const amount = Math.floor(config.amount * 1_000_000).toString(); + const requirement: X402PaymentRequirement = { + scheme: 'shadowwire', + network: 'solana:mainnet', + amount, + asset: config.asset || 'USDC', + payTo: config.payTo, + resource, + description: config.description, + maxTimeoutSeconds: config.maxTimeoutSeconds || 60, + }; + + const verifyResult = await verifyPayment(paymentHeader, requirement, config.facilitatorUrl); + + if (!verifyResult.valid) { + const body = createPaymentRequired(resource, config); + (body as any).verifyError = verifyResult.error; + res.setHeader?.('WWW-Authenticate', 'X402'); + return res.status(402).json(body); + } + + const settleResult = await settlePayment(paymentHeader, requirement, config.facilitatorUrl); + if (!settleResult.success) { + const body = createPaymentRequired(resource, config); + (body as any).settleError = settleResult.error; + res.setHeader?.('WWW-Authenticate', 'X402'); + return res.status(402).json(body); + } + + req.x402 = { + payer: verifyResult.payer, + signature: verifyResult.signature, + amountHidden: verifyResult.amountHidden, + tx: settleResult.tx, + scheme: 'shadowwire', + }; + + if (config.onPayment && verifyResult.payer && verifyResult.signature) { + config.onPayment({ + payer: verifyResult.payer, + amount: config.amount, + signature: verifyResult.signature, + resource, + }); + } + + next(); + }; +} + +// --------------------------------------------------------------------------- +// Discovery document helper +// --------------------------------------------------------------------------- + +export interface X402DiscoveryResource { + path: string; + method: string; + price: number; + asset?: string; + description?: string; +} + +/** + * Generate a .well-known/x402 discovery document. + */ +export function createDiscoveryDocument( + name: string, + payTo: string, + resources: X402DiscoveryResource[], + options?: { + description?: string; + facilitatorUrl?: string; + } +): Record { + return { + version: '2.0', + name, + description: options?.description, + payTo, + schemes: ['shadowwire', 'exact'], + networks: ['solana:mainnet'], + facilitator: options?.facilitatorUrl, + resources: resources.map((r) => ({ + path: r.path, + method: r.method, + price: r.price, + asset: r.asset || 'USDC', + description: r.description, + schemes: ['shadowwire'], + })), + capabilities: { + privatePayments: true, + amountHiding: true, + bulletproofs: true, + }, + }; +} From 2cec755719b05f2edfd29e32bbfbff725eaf0a71 Mon Sep 17 00:00:00 2001 From: KAMIYO Date: Thu, 5 Feb 2026 12:44:39 +0100 Subject: [PATCH 2/7] Add timeouts, header limits, and browser base64 support --- examples/x402-payment.ts | 1 + src/x402.ts | 131 +++++++++++++++++++++++++++++++++++---- 2 files changed, 120 insertions(+), 12 deletions(-) diff --git a/examples/x402-payment.ts b/examples/x402-payment.ts index 846427f..7bf28e4 100644 --- a/examples/x402-payment.ts +++ b/examples/x402-payment.ts @@ -34,6 +34,7 @@ async function clientExample() { // 'external' = sender anonymous, amount visible // 'internal' = both parties private, amount hidden defaultTransferType: 'external', + requestTimeoutMs: 20_000, }); // Automatic flow: request -> detect 402 -> pay -> retry diff --git a/src/x402.ts b/src/x402.ts index ae16787..435402f 100644 --- a/src/x402.ts +++ b/src/x402.ts @@ -19,6 +19,13 @@ import { ShadowWireClient } from './client'; import { TokenSymbol, WalletAdapter, TransferResponse, PoolBalance } from './types'; import { NetworkError } from './errors'; +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +const DEFAULT_TIMEOUT_MS = 15_000; +const MAX_PAYMENT_HEADER_BYTES = 16_384; + // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- @@ -74,6 +81,8 @@ export interface X402ClientConfig { defaultTransferType?: 'internal' | 'external'; maxRetries?: number; headers?: Record; + /** Timeout for HTTP requests in milliseconds. */ + requestTimeoutMs?: number; } export interface X402VerifyResult { @@ -111,6 +120,35 @@ export interface X402PaymentProof { }; } +// --------------------------------------------------------------------------- +// Encoding helpers (cross-platform: Node + browser) +// --------------------------------------------------------------------------- + +function toBase64(input: string): string { + if (typeof Buffer !== 'undefined') { + return Buffer.from(input, 'utf-8').toString('base64'); + } + return btoa(input); +} + +function fromBase64(encoded: string): string { + if (typeof Buffer !== 'undefined') { + return Buffer.from(encoded, 'base64').toString('utf-8'); + } + return atob(encoded); +} + +function byteLength(str: string): number { + if (typeof Buffer !== 'undefined') { + return Buffer.byteLength(str, 'utf-8'); + } + return new TextEncoder().encode(str).length; +} + +function isShadowwire(scheme: string): boolean { + return scheme === 'shadowwire' || scheme === 'shadow'; +} + // --------------------------------------------------------------------------- // Client (payer side) // --------------------------------------------------------------------------- @@ -123,6 +161,7 @@ export class X402Client { private defaultTransferType: 'internal' | 'external'; private maxRetries: number; private headers: Record; + private timeoutMs: number; constructor(config: X402ClientConfig) { this.client = config.client; @@ -132,6 +171,7 @@ export class X402Client { this.defaultTransferType = config.defaultTransferType || 'external'; this.maxRetries = config.maxRetries ?? 1; this.headers = config.headers || {}; + this.timeoutMs = config.requestTimeoutMs ?? DEFAULT_TIMEOUT_MS; } /** @@ -154,12 +194,12 @@ export class X402Client { statusCode: response.status, }; } - const data = await response.json() as T; + const data = await safeParseBody(response); return { success: true, data, statusCode: response.status }; } - const x402Body = await response.json() as X402Response; - if (!x402Body.accepts || x402Body.accepts.length === 0) { + const x402Body = await safeParseBody(response); + if (!x402Body?.accepts || x402Body.accepts.length === 0) { return { success: false, error: 'No accepted payment methods in 402 response', statusCode: 402 }; } @@ -180,7 +220,7 @@ export class X402Client { }); if (retryResponse.ok) { - const data = await retryResponse.json() as T; + const data = await safeParseBody(retryResponse); return { success: true, data, @@ -209,6 +249,10 @@ export class X402Client { * Pay a specific x402 requirement via ShadowWire. */ async pay(requirement: X402PaymentRequirement): Promise { + if (!isShadowwire(requirement.scheme)) { + return { success: false, error: `Unsupported scheme: ${requirement.scheme}` }; + } + const amount = this.parseAmount(requirement.amount, requirement.asset); if (amount <= 0) { return { success: false, error: 'Invalid payment amount' }; @@ -276,12 +320,13 @@ export class X402Client { } static encodePaymentHeader(proof: X402PaymentProof): string { - return Buffer.from(JSON.stringify(proof)).toString('base64'); + return toBase64(JSON.stringify(proof)); } static decodePaymentHeader(header: string): X402PaymentProof | null { try { - const decoded = JSON.parse(Buffer.from(header, 'base64').toString()); + if (byteLength(header) > MAX_PAYMENT_HEADER_BYTES) return null; + const decoded = JSON.parse(fromBase64(header)); if (!decoded.scheme || !decoded.payload?.signature) return null; return decoded as X402PaymentProof; } catch { @@ -292,9 +337,7 @@ export class X402Client { // --- Private --- private findCompatibleRequirement(accepts: X402PaymentRequirement[]): X402PaymentRequirement | null { - const shadowReq = accepts.find((r) => - r.scheme === 'shadowwire' || r.scheme === 'shadow' - ); + const shadowReq = accepts.find((r) => isShadowwire(r.scheme)); if (shadowReq) return shadowReq; return accepts.find((r) => r.network?.includes('solana')) || null; @@ -323,12 +366,22 @@ export class X402Client { } private async doFetch(url: string, options?: RequestInit): Promise { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), this.timeoutMs); try { - return await (globalThis as any).fetch(url, options); + return await (globalThis as any).fetch(url, { + ...options, + signal: controller.signal, + }); } catch (err) { + if (err instanceof DOMException && err.name === 'AbortError') { + throw new NetworkError(`x402 request timed out after ${this.timeoutMs}ms`); + } throw new NetworkError( err instanceof Error ? `x402 request failed: ${err.message}` : 'x402 request failed' ); + } finally { + clearTimeout(timer); } } } @@ -337,6 +390,38 @@ export class X402Client { // Server middleware (payee side) // --------------------------------------------------------------------------- +/** + * Safely parse JSON from a response, checking content-type first. + */ +async function safeParseBody(response: Response): Promise { + const ct = response.headers?.get?.('content-type') || ''; + if (!ct.includes('application/json') && !ct.includes('text/json')) { + try { + return await response.json() as T; + } catch { + return undefined; + } + } + return response.json() as Promise; +} + +/** + * Timed fetch to an external service (facilitator). + */ +async function timedFetch( + url: string, + init: RequestInit, + timeoutMs: number = DEFAULT_TIMEOUT_MS +): Promise { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + try { + return await (globalThis as any).fetch(url, { ...init, signal: controller.signal }); + } finally { + clearTimeout(timer); + } +} + /** * Create a 402 payment requirement response body. */ @@ -390,8 +475,12 @@ export async function verifyPayment( requirement: X402PaymentRequirement, facilitatorUrl: string ): Promise { + if (byteLength(paymentHeader) > MAX_PAYMENT_HEADER_BYTES) { + return { valid: false, error: 'Payment header exceeds size limit' }; + } + try { - const response = await (globalThis as any).fetch(`${facilitatorUrl}/verify`, { + const response = await timedFetch(`${facilitatorUrl}/verify`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -435,7 +524,7 @@ export async function settlePayment( facilitatorUrl: string ): Promise<{ success: boolean; tx?: string; error?: string }> { try { - const response = await (globalThis as any).fetch(`${facilitatorUrl}/settle`, { + const response = await timedFetch(`${facilitatorUrl}/settle`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -488,6 +577,22 @@ export function x402Paywall(config: X402MiddlewareConfig) { const body = createPaymentRequired(resource, config); res.setHeader?.('WWW-Authenticate', 'X402'); res.setHeader?.('X-Payment-Schemes', 'shadowwire'); + res.setHeader?.('Cache-Control', 'no-store'); + return res.status(402).json(body); + } + + // Reject oversized headers before hitting the facilitator. + if (byteLength(paymentHeader) > MAX_PAYMENT_HEADER_BYTES) { + return res.status(400).json({ error: 'Payment header too large' }); + } + + // Decode and validate the proof structure early. + const proof = X402Client.decodePaymentHeader(paymentHeader); + if (!proof || !isShadowwire(proof.scheme)) { + const body = createPaymentRequired(resource, config); + (body as any).verifyError = 'Invalid or unsupported payment proof'; + res.setHeader?.('WWW-Authenticate', 'X402'); + res.setHeader?.('Cache-Control', 'no-store'); return res.status(402).json(body); } @@ -509,6 +614,7 @@ export function x402Paywall(config: X402MiddlewareConfig) { const body = createPaymentRequired(resource, config); (body as any).verifyError = verifyResult.error; res.setHeader?.('WWW-Authenticate', 'X402'); + res.setHeader?.('Cache-Control', 'no-store'); return res.status(402).json(body); } @@ -517,6 +623,7 @@ export function x402Paywall(config: X402MiddlewareConfig) { const body = createPaymentRequired(resource, config); (body as any).settleError = settleResult.error; res.setHeader?.('WWW-Authenticate', 'X402'); + res.setHeader?.('Cache-Control', 'no-store'); return res.status(402).json(body); } From bb031f732bc20f828d9acb75fde9a8db76182264 Mon Sep 17 00:00:00 2001 From: KAMIYO Date: Thu, 5 Feb 2026 12:49:33 +0100 Subject: [PATCH 3/7] Clean up comments and match project style --- examples/x402-payment.ts | 34 +------------- src/x402.ts | 98 +--------------------------------------- 2 files changed, 4 insertions(+), 128 deletions(-) diff --git a/examples/x402-payment.ts b/examples/x402-payment.ts index 7bf28e4..ee2837d 100644 --- a/examples/x402-payment.ts +++ b/examples/x402-payment.ts @@ -1,10 +1,3 @@ -/** - * x402 Payment Protocol examples. - * - * Shows both client-side (paying for APIs) and server-side (monetizing APIs) - * usage with ShadowWire private transfers. - */ - import { ShadowWireClient, X402Client, @@ -12,14 +5,9 @@ import { createDiscoveryDocument, } from '@radr/shadowwire'; -// --------------------------------------------------------------------------- -// Client: Paying for a paid API endpoint -// --------------------------------------------------------------------------- - async function clientExample() { const client = new ShadowWireClient(); - // Use @solana/wallet-adapter in real apps const wallet = { signMessage: async (message: Uint8Array) => { throw new Error('Implement wallet signing'); @@ -31,13 +19,10 @@ async function clientExample() { wallet, senderWallet: 'YOUR_WALLET_ADDRESS', defaultToken: 'USDC', - // 'external' = sender anonymous, amount visible - // 'internal' = both parties private, amount hidden defaultTransferType: 'external', requestTimeoutMs: 20_000, }); - // Automatic flow: request -> detect 402 -> pay -> retry const result = await x402.request('https://api.example.com/data'); if (result.success) { @@ -48,7 +33,6 @@ async function clientExample() { } } - // Manual flow: parse 402 and pay individually const response = await fetch('https://api.example.com/premium'); if (response.status === 402) { const body = await response.json(); @@ -57,7 +41,6 @@ async function clientExample() { const req = requirements.accepts[0]; const payment = await x402.pay(req); if (payment.success) { - // Retry with payment header await fetch('https://api.example.com/premium', { headers: { 'X-Payment': payment.paymentHeader! }, }); @@ -66,40 +49,27 @@ async function clientExample() { } } -// --------------------------------------------------------------------------- -// Server: Monetizing API endpoints with Express -// --------------------------------------------------------------------------- - async function serverExample() { - // Requires: npm install express const express = require('express'); const app = express(); - // Protect an endpoint: requests without payment get a 402 response. - // ShadowWire payments are verified automatically. app.get( '/api/data', x402Paywall({ payTo: 'YOUR_MERCHANT_WALLET', - amount: 0.01, // $0.01 USDC + amount: 0.01, asset: 'USDC', description: 'Premium data endpoint', - // Optional: also accept payments via PayAI facilitator facilitatorUrl: 'https://facilitator.payai.network', onPayment: (info) => { console.log(`Payment from ${info.payer}: ${info.signature}`); }, }), (req: any, res: any) => { - // req.x402 contains payment details - res.json({ - data: 'premium content', - payment: req.x402, - }); + res.json({ data: 'premium content', payment: req.x402 }); } ); - // Discovery document for agents to find your paid endpoints app.get('/.well-known/x402', (_req: any, res: any) => { res.json(createDiscoveryDocument( 'My API', diff --git a/src/x402.ts b/src/x402.ts index 435402f..367fc3f 100644 --- a/src/x402.ts +++ b/src/x402.ts @@ -1,35 +1,13 @@ -/** - * x402 Payment Protocol integration for ShadowWire. - * - * Implements the x402 (HTTP 402 Payment Required) spec with ShadowWire - * private transfers. Includes both client-side (paying for APIs) and - * server-side (protecting endpoints with paywalls) components. - * - * Payment flow: - * 1. Client requests paid endpoint - * 2. Server returns 402 with payment requirements (including shadowwire scheme) - * 3. Client pays via ShadowWire (amount hidden via Bulletproofs) - * 4. Client retries request with X-Payment header containing transfer proof - * 5. Server verifies the ShadowWire transfer via facilitator and serves the resource - * - * Spec: https://github.com/coinbase/x402 - */ +// x402 (HTTP 402) payment protocol for ShadowWire. +// Spec: https://github.com/coinbase/x402 import { ShadowWireClient } from './client'; import { TokenSymbol, WalletAdapter, TransferResponse, PoolBalance } from './types'; import { NetworkError } from './errors'; -// --------------------------------------------------------------------------- -// Constants -// --------------------------------------------------------------------------- - const DEFAULT_TIMEOUT_MS = 15_000; const MAX_PAYMENT_HEADER_BYTES = 16_384; -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - export interface X402PaymentRequirement { scheme: string; network: string; @@ -81,7 +59,6 @@ export interface X402ClientConfig { defaultTransferType?: 'internal' | 'external'; maxRetries?: number; headers?: Record; - /** Timeout for HTTP requests in milliseconds. */ requestTimeoutMs?: number; } @@ -99,11 +76,8 @@ export interface X402MiddlewareConfig { asset?: TokenSymbol; description?: string; maxTimeoutSeconds?: number; - /** x402 facilitator URL for payment verification and settlement */ facilitatorUrl: string; - /** Additional accepted payment schemes alongside shadowwire */ additionalSchemes?: X402PaymentRequirement[]; - /** Called after successful payment verification */ onPayment?: (info: { payer: string; amount: number; signature: string; resource: string }) => void; } @@ -120,10 +94,6 @@ export interface X402PaymentProof { }; } -// --------------------------------------------------------------------------- -// Encoding helpers (cross-platform: Node + browser) -// --------------------------------------------------------------------------- - function toBase64(input: string): string { if (typeof Buffer !== 'undefined') { return Buffer.from(input, 'utf-8').toString('base64'); @@ -149,10 +119,6 @@ function isShadowwire(scheme: string): boolean { return scheme === 'shadowwire' || scheme === 'shadow'; } -// --------------------------------------------------------------------------- -// Client (payer side) -// --------------------------------------------------------------------------- - export class X402Client { private client: ShadowWireClient; private wallet: WalletAdapter; @@ -174,10 +140,6 @@ export class X402Client { this.timeoutMs = config.requestTimeoutMs ?? DEFAULT_TIMEOUT_MS; } - /** - * Make a request to a URL that may require x402 payment. - * Handles the full flow: request -> 402 -> pay -> retry. - */ async request(url: string, options?: RequestInit): Promise> { const mergedHeaders: Record = { ...this.headers, @@ -245,9 +207,6 @@ export class X402Client { return { success: false, error: 'Unexpected error', statusCode: 500 }; } - /** - * Pay a specific x402 requirement via ShadowWire. - */ async pay(requirement: X402PaymentRequirement): Promise { if (!isShadowwire(requirement.scheme)) { return { success: false, error: `Unsupported scheme: ${requirement.scheme}` }; @@ -292,22 +251,14 @@ export class X402Client { } } - /** - * Check sender's shielded balance for a given token. - */ async getBalance(token?: TokenSymbol): Promise { return this.client.getBalance(this.senderWallet, token || this.defaultToken); } - /** - * Estimate the fee for a payment amount. - */ estimateFee(amount: number, token?: TokenSymbol): { fee: number; feePercentage: number; netAmount: number } { return this.client.calculateFee(amount, token || this.defaultToken); } - // --- Static helpers --- - static parseRequirements(body: unknown): X402Response | null { if (!body || typeof body !== 'object') return null; const obj = body as Record; @@ -334,8 +285,6 @@ export class X402Client { } } - // --- Private --- - private findCompatibleRequirement(accepts: X402PaymentRequirement[]): X402PaymentRequirement | null { const shadowReq = accepts.find((r) => isShadowwire(r.scheme)); if (shadowReq) return shadowReq; @@ -386,13 +335,6 @@ export class X402Client { } } -// --------------------------------------------------------------------------- -// Server middleware (payee side) -// --------------------------------------------------------------------------- - -/** - * Safely parse JSON from a response, checking content-type first. - */ async function safeParseBody(response: Response): Promise { const ct = response.headers?.get?.('content-type') || ''; if (!ct.includes('application/json') && !ct.includes('text/json')) { @@ -405,9 +347,6 @@ async function safeParseBody(response: Response): Promise { return response.json() as Promise; } -/** - * Timed fetch to an external service (facilitator). - */ async function timedFetch( url: string, init: RequestInit, @@ -422,9 +361,6 @@ async function timedFetch( } } -/** - * Create a 402 payment requirement response body. - */ export function createPaymentRequired( resource: string, config: X402MiddlewareConfig @@ -465,11 +401,6 @@ export function createPaymentRequired( }; } -/** - * Verify a payment via the configured x402 facilitator. - * The facilitator handles ShadowWire transfer verification, - * settlement, escrow, and dispute resolution. - */ export async function verifyPayment( paymentHeader: string, requirement: X402PaymentRequirement, @@ -515,9 +446,6 @@ export async function verifyPayment( } } -/** - * Settle a verified payment via the facilitator. - */ export async function settlePayment( paymentHeader: string, requirement: X402PaymentRequirement, @@ -555,19 +483,6 @@ export async function settlePayment( } } -/** - * Express-compatible middleware factory. - * - * Protects routes with x402 payment requirements. Verification and - * settlement are delegated to the configured facilitator. - * - * Usage: - * app.get('/api/data', x402Paywall({ - * payTo: 'WALLET', - * amount: 0.01, - * facilitatorUrl: 'https://x402.kamiyo.ai', - * }), handler); - */ export function x402Paywall(config: X402MiddlewareConfig) { return async (req: any, res: any, next: any) => { const resource = req.path || req.url || '/'; @@ -581,12 +496,10 @@ export function x402Paywall(config: X402MiddlewareConfig) { return res.status(402).json(body); } - // Reject oversized headers before hitting the facilitator. if (byteLength(paymentHeader) > MAX_PAYMENT_HEADER_BYTES) { return res.status(400).json({ error: 'Payment header too large' }); } - // Decode and validate the proof structure early. const proof = X402Client.decodePaymentHeader(paymentHeader); if (!proof || !isShadowwire(proof.scheme)) { const body = createPaymentRequired(resource, config); @@ -648,10 +561,6 @@ export function x402Paywall(config: X402MiddlewareConfig) { }; } -// --------------------------------------------------------------------------- -// Discovery document helper -// --------------------------------------------------------------------------- - export interface X402DiscoveryResource { path: string; method: string; @@ -660,9 +569,6 @@ export interface X402DiscoveryResource { description?: string; } -/** - * Generate a .well-known/x402 discovery document. - */ export function createDiscoveryDocument( name: string, payTo: string, From 62fd19028a19565bfe32cb2bb24ab0334120beef Mon Sep 17 00:00:00 2001 From: KAMIYO Date: Thu, 5 Feb 2026 15:32:13 +0100 Subject: [PATCH 4/7] Align x402 client with KAMIYO facilitator API contract Verify endpoint: send {paymentHeader, resource, maxAmount} instead of {paymentRequirements}, read `valid` not `isValid` from response. Settle endpoint: send {paymentHeader, merchantWallet, amount, asset} instead of {paymentRequirements}, read `txHash` not `transaction`. Add X-API-Key header support (required by facilitator auth middleware). Update example facilitator URL to x402.kamiyo.ai. --- examples/x402-payment.ts | 3 +- src/x402.ts | 86 ++++++++++++++++++++++++++-------------- 2 files changed, 58 insertions(+), 31 deletions(-) diff --git a/examples/x402-payment.ts b/examples/x402-payment.ts index ee2837d..439f990 100644 --- a/examples/x402-payment.ts +++ b/examples/x402-payment.ts @@ -60,7 +60,8 @@ async function serverExample() { amount: 0.01, asset: 'USDC', description: 'Premium data endpoint', - facilitatorUrl: 'https://facilitator.payai.network', + facilitatorUrl: 'https://x402.kamiyo.ai', + apiKey: 'YOUR_API_KEY', onPayment: (info) => { console.log(`Payment from ${info.payer}: ${info.signature}`); }, diff --git a/src/x402.ts b/src/x402.ts index 367fc3f..6c0fdb2 100644 --- a/src/x402.ts +++ b/src/x402.ts @@ -65,8 +65,10 @@ export interface X402ClientConfig { export interface X402VerifyResult { valid: boolean; payer?: string; - signature?: string; - amountHidden?: boolean; + amount?: string; + resource?: string; + balance?: number; + sufficient?: boolean; error?: string; } @@ -77,6 +79,7 @@ export interface X402MiddlewareConfig { description?: string; maxTimeoutSeconds?: number; facilitatorUrl: string; + apiKey: string; additionalSchemes?: X402PaymentRequirement[]; onPayment?: (info: { payer: string; amount: number; signature: string; resource: string }) => void; } @@ -404,41 +407,46 @@ export function createPaymentRequired( export async function verifyPayment( paymentHeader: string, requirement: X402PaymentRequirement, - facilitatorUrl: string + facilitatorUrl: string, + apiKey?: string ): Promise { if (byteLength(paymentHeader) > MAX_PAYMENT_HEADER_BYTES) { return { valid: false, error: 'Payment header exceeds size limit' }; } try { + const headers: Record = { 'Content-Type': 'application/json' }; + if (apiKey) headers['X-API-Key'] = apiKey; + const response = await timedFetch(`${facilitatorUrl}/verify`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers, body: JSON.stringify({ - x402Version: 2, paymentHeader, - paymentRequirements: requirement, + resource: requirement.resource, + maxAmount: requirement.amount ? parseInt(requirement.amount, 10) / 1_000_000 : undefined, }), }); if (!response.ok) { - return { valid: false, error: `Facilitator returned ${response.status}` }; + const errBody = await safeParseBody<{ error?: string }>(response); + return { valid: false, error: errBody?.error || `Facilitator returned ${response.status}` }; } const data = await response.json() as { - isValid?: boolean; + valid?: boolean; payer?: string; - signature?: string; - amountHidden?: boolean; - invalidReason?: string; + amount?: string; + resource?: string; + balance?: number; + sufficient?: boolean; + error?: string; }; return { - valid: !!data.isValid, + valid: !!data.valid, payer: data.payer, - signature: data.signature, - amountHidden: data.amountHidden, - error: data.invalidReason, + error: data.error, }; } catch (err) { const message = err instanceof Error ? err.message : 'Unknown error'; @@ -449,32 +457,48 @@ export async function verifyPayment( export async function settlePayment( paymentHeader: string, requirement: X402PaymentRequirement, - facilitatorUrl: string -): Promise<{ success: boolean; tx?: string; error?: string }> { + facilitatorUrl: string, + apiKey?: string +): Promise<{ success: boolean; txHash?: string; amount?: number; fee?: number; net?: number; network?: string; error?: string }> { try { + const headers: Record = { 'Content-Type': 'application/json' }; + if (apiKey) headers['X-API-Key'] = apiKey; + + const amount = parseInt(requirement.amount, 10) / 1_000_000; + const response = await timedFetch(`${facilitatorUrl}/settle`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers, body: JSON.stringify({ - x402Version: 2, paymentHeader, - paymentRequirements: requirement, + merchantWallet: requirement.payTo, + amount, + asset: requirement.asset || 'USDC', }), }); if (!response.ok) { - return { success: false, error: `Settlement returned ${response.status}` }; + const errBody = await safeParseBody<{ error?: string }>(response); + return { success: false, error: errBody?.error || `Settlement returned ${response.status}` }; } const data = await response.json() as { success?: boolean; - transaction?: string; + txHash?: string; + amount?: number; + fee?: number; + net?: number; + network?: string; error?: string; }; return { success: !!data.success, - tx: data.transaction, + txHash: data.txHash, + amount: data.amount, + fee: data.fee, + net: data.net, + network: data.network, error: data.error, }; } catch (err) { @@ -521,7 +545,7 @@ export function x402Paywall(config: X402MiddlewareConfig) { maxTimeoutSeconds: config.maxTimeoutSeconds || 60, }; - const verifyResult = await verifyPayment(paymentHeader, requirement, config.facilitatorUrl); + const verifyResult = await verifyPayment(paymentHeader, requirement, config.facilitatorUrl, config.apiKey); if (!verifyResult.valid) { const body = createPaymentRequired(resource, config); @@ -531,7 +555,7 @@ export function x402Paywall(config: X402MiddlewareConfig) { return res.status(402).json(body); } - const settleResult = await settlePayment(paymentHeader, requirement, config.facilitatorUrl); + const settleResult = await settlePayment(paymentHeader, requirement, config.facilitatorUrl, config.apiKey); if (!settleResult.success) { const body = createPaymentRequired(resource, config); (body as any).settleError = settleResult.error; @@ -542,17 +566,19 @@ export function x402Paywall(config: X402MiddlewareConfig) { req.x402 = { payer: verifyResult.payer, - signature: verifyResult.signature, - amountHidden: verifyResult.amountHidden, - tx: settleResult.tx, + txHash: settleResult.txHash, + amount: settleResult.amount, + fee: settleResult.fee, + net: settleResult.net, + network: settleResult.network, scheme: 'shadowwire', }; - if (config.onPayment && verifyResult.payer && verifyResult.signature) { + if (config.onPayment && verifyResult.payer && settleResult.txHash) { config.onPayment({ payer: verifyResult.payer, amount: config.amount, - signature: verifyResult.signature, + signature: settleResult.txHash, resource, }); } From 0ebdf0d0857cc694cfc338aa03ed028f7f252842 Mon Sep 17 00:00:00 2001 From: KAMIYO Date: Thu, 5 Feb 2026 15:48:57 +0100 Subject: [PATCH 5/7] Harden x402 protocol implementation Fix inverted content-type check in safeParseBody. Restrict findCompatibleRequirement to shadowwire-only schemes. Add retry backoff on 5xx after payment. Add Vary: X-Payment header to prevent cache poisoning. Reject proof resource mismatch before calling facilitator. Improve abort detection for non-DOM runtimes. Remove unadvertised 'exact' scheme from discovery document. --- examples/x402-payment.ts | 39 +++---- src/x402.ts | 227 ++++++++++++++------------------------- 2 files changed, 98 insertions(+), 168 deletions(-) diff --git a/examples/x402-payment.ts b/examples/x402-payment.ts index 439f990..5086f7b 100644 --- a/examples/x402-payment.ts +++ b/examples/x402-payment.ts @@ -9,7 +9,7 @@ async function clientExample() { const client = new ShadowWireClient(); const wallet = { - signMessage: async (message: Uint8Array) => { + signMessage: async (_message: Uint8Array) => { throw new Error('Implement wallet signing'); }, }; @@ -23,27 +23,24 @@ async function clientExample() { requestTimeoutMs: 20_000, }); - const result = await x402.request('https://api.example.com/data'); - - if (result.success) { - console.log('Data:', result.data); - if (result.payment) { - console.log('Paid:', result.payment.transfer.tx_signature); - console.log('Amount hidden:', result.payment.transfer.amount_hidden); + const res1 = await x402.request('https://api.example.com/data'); + if (res1.success) { + console.log('Data:', res1.data); + if (res1.payment) { + console.log('Paid:', res1.payment.transfer.tx_signature); + console.log('Amount hidden:', res1.payment.transfer.amount_hidden); } } - const response = await fetch('https://api.example.com/premium'); - if (response.status === 402) { - const body = await response.json(); + const probe = await fetch('https://api.example.com/premium'); + if (probe.status === 402) { + const body = await probe.json(); const requirements = X402Client.parseRequirements(body); if (requirements) { const req = requirements.accepts[0]; const payment = await x402.pay(req); - if (payment.success) { - await fetch('https://api.example.com/premium', { - headers: { 'X-Payment': payment.paymentHeader! }, - }); + if (payment.success && payment.paymentHeader) { + await fetch('https://api.example.com/premium', { headers: { 'X-Payment': payment.paymentHeader } }); } } } @@ -62,7 +59,7 @@ async function serverExample() { description: 'Premium data endpoint', facilitatorUrl: 'https://x402.kamiyo.ai', apiKey: 'YOUR_API_KEY', - onPayment: (info) => { + onPayment: (info: { payer: string; amount: number; signature: string; resource: string }) => { console.log(`Payment from ${info.payer}: ${info.signature}`); }, }), @@ -72,14 +69,12 @@ async function serverExample() { ); app.get('/.well-known/x402', (_req: any, res: any) => { - res.json(createDiscoveryDocument( - 'My API', - 'YOUR_MERCHANT_WALLET', - [ + res.json( + createDiscoveryDocument('My API', 'YOUR_MERCHANT_WALLET', [ { path: '/api/data', method: 'GET', price: 0.01, description: 'Premium data' }, { path: '/api/signals', method: 'GET', price: 0.05, description: 'Trading signals' }, - ], - )); + ]) + ); }); app.listen(3000); diff --git a/src/x402.ts b/src/x402.ts index 6c0fdb2..7b2d40e 100644 --- a/src/x402.ts +++ b/src/x402.ts @@ -1,12 +1,10 @@ -// x402 (HTTP 402) payment protocol for ShadowWire. -// Spec: https://github.com/coinbase/x402 - import { ShadowWireClient } from './client'; import { TokenSymbol, WalletAdapter, TransferResponse, PoolBalance } from './types'; import { NetworkError } from './errors'; const DEFAULT_TIMEOUT_MS = 15_000; const MAX_PAYMENT_HEADER_BYTES = 16_384; +const PAYMENT_HEADER_NAME = 'X-Payment'; export interface X402PaymentRequirement { scheme: string; @@ -98,23 +96,17 @@ export interface X402PaymentProof { } function toBase64(input: string): string { - if (typeof Buffer !== 'undefined') { - return Buffer.from(input, 'utf-8').toString('base64'); - } + if (typeof Buffer !== 'undefined') return Buffer.from(input, 'utf-8').toString('base64'); return btoa(input); } function fromBase64(encoded: string): string { - if (typeof Buffer !== 'undefined') { - return Buffer.from(encoded, 'base64').toString('utf-8'); - } + if (typeof Buffer !== 'undefined') return Buffer.from(encoded, 'base64').toString('utf-8'); return atob(encoded); } function byteLength(str: string): number { - if (typeof Buffer !== 'undefined') { - return Buffer.byteLength(str, 'utf-8'); - } + if (typeof Buffer !== 'undefined') return Buffer.byteLength(str, 'utf-8'); return new TextEncoder().encode(str).length; } @@ -122,6 +114,10 @@ function isShadowwire(scheme: string): boolean { return scheme === 'shadowwire' || scheme === 'shadow'; } +function sleep(ms: number) { + return new Promise((r) => setTimeout(r, ms)); +} + export class X402Client { private client: ShadowWireClient; private wallet: WalletAdapter; @@ -146,31 +142,27 @@ export class X402Client { async request(url: string, options?: RequestInit): Promise> { const mergedHeaders: Record = { ...this.headers, - ...(options?.headers as Record || {}), + ...((options?.headers as Record) || {}), }; - const response = await this.doFetch(url, { ...options, headers: mergedHeaders }); + const initial = await this.doFetch(url, { ...options, headers: mergedHeaders }); - if (response.status !== 402) { - if (!response.ok) { - return { - success: false, - error: `HTTP ${response.status}: ${response.statusText}`, - statusCode: response.status, - }; + if (initial.status !== 402) { + if (!initial.ok) { + return { success: false, error: `HTTP ${initial.status}: ${initial.statusText}`, statusCode: initial.status }; } - const data = await safeParseBody(response); - return { success: true, data, statusCode: response.status }; + const parsed = await safeParseBody(initial); + return { success: true, data: parsed, statusCode: initial.status }; } - const x402Body = await safeParseBody(response); - if (!x402Body?.accepts || x402Body.accepts.length === 0) { + const x402Body = await safeParseBody(initial); + if (!x402Body?.accepts?.length) { return { success: false, error: 'No accepted payment methods in 402 response', statusCode: 402 }; } const requirement = this.findCompatibleRequirement(x402Body.accepts); if (!requirement) { - return { success: false, error: 'No compatible payment option (need Solana or ShadowWire)', statusCode: 402 }; + return { success: false, error: 'No compatible payment option (need ShadowWire)', statusCode: 402 }; } const payResult = await this.pay(requirement); @@ -179,46 +171,36 @@ export class X402Client { } for (let attempt = 0; attempt <= this.maxRetries; attempt++) { - const retryResponse = await this.doFetch(url, { + const res = await this.doFetch(url, { ...options, - headers: { ...mergedHeaders, 'X-Payment': payResult.paymentHeader }, + headers: { ...mergedHeaders, [PAYMENT_HEADER_NAME]: payResult.paymentHeader }, }); - if (retryResponse.ok) { - const data = await safeParseBody(retryResponse); - return { - success: true, - data, - payment: { transfer: payResult.transfer, requirement }, - statusCode: retryResponse.status, - }; + if (res.ok) { + const parsed = await safeParseBody(res); + return { success: true, data: parsed, payment: { transfer: payResult.transfer, requirement }, statusCode: res.status }; } - if (retryResponse.status === 402) { + if (res.status === 402) { return { success: false, error: 'Payment not accepted by server', statusCode: 402 }; } - if (attempt === this.maxRetries) { - return { - success: false, - error: `HTTP ${retryResponse.status} after payment`, - statusCode: retryResponse.status, - }; + if (attempt < this.maxRetries && res.status >= 500) { + await sleep(Math.min(250 * 2 ** attempt, 1000)); + continue; } + + return { success: false, error: `HTTP ${res.status} after payment`, statusCode: res.status }; } return { success: false, error: 'Unexpected error', statusCode: 500 }; } async pay(requirement: X402PaymentRequirement): Promise { - if (!isShadowwire(requirement.scheme)) { - return { success: false, error: `Unsupported scheme: ${requirement.scheme}` }; - } + if (!isShadowwire(requirement.scheme)) return { success: false, error: `Unsupported scheme: ${requirement.scheme}` }; const amount = this.parseAmount(requirement.amount, requirement.asset); - if (amount <= 0) { - return { success: false, error: 'Invalid payment amount' }; - } + if (amount <= 0) return { success: false, error: 'Invalid payment amount' }; try { const transfer = await this.client.transfer({ @@ -230,9 +212,7 @@ export class X402Client { wallet: this.wallet, }); - if (!transfer.success) { - return { success: false, error: 'ShadowWire transfer failed' }; - } + if (!transfer.success) return { success: false, error: 'ShadowWire transfer failed' }; const paymentHeader = X402Client.encodePaymentHeader({ x402Version: 2, @@ -289,16 +269,12 @@ export class X402Client { } private findCompatibleRequirement(accepts: X402PaymentRequirement[]): X402PaymentRequirement | null { - const shadowReq = accepts.find((r) => isShadowwire(r.scheme)); - if (shadowReq) return shadowReq; - - return accepts.find((r) => r.network?.includes('solana')) || null; + return accepts.find((r) => isShadowwire(r.scheme)) || null; } private parseAmount(amountStr: string, asset: string): number { const raw = parseInt(amountStr, 10); if (isNaN(raw) || raw <= 0) return 0; - const token = this.resolveToken(asset); try { const { TokenUtils } = require('./tokens'); @@ -309,10 +285,10 @@ export class X402Client { } private resolveToken(asset: string): TokenSymbol { - const upper = asset.toUpperCase(); + const upper = asset?.toUpperCase?.() || ''; try { const { TokenUtils } = require('./tokens'); - if (TokenUtils.isValidToken(upper)) return upper as TokenSymbol; + if (upper && TokenUtils.isValidToken(upper)) return upper as TokenSymbol; } catch {} return this.defaultToken; } @@ -321,17 +297,12 @@ export class X402Client { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), this.timeoutMs); try { - return await (globalThis as any).fetch(url, { - ...options, - signal: controller.signal, - }); - } catch (err) { - if (err instanceof DOMException && err.name === 'AbortError') { + return await (globalThis as any).fetch(url, { ...options, signal: controller.signal }); + } catch (err: any) { + if (err && (err.name === 'AbortError')) { throw new NetworkError(`x402 request timed out after ${this.timeoutMs}ms`); } - throw new NetworkError( - err instanceof Error ? `x402 request failed: ${err.message}` : 'x402 request failed' - ); + throw new NetworkError(err instanceof Error ? `x402 request failed: ${err.message}` : 'x402 request failed'); } finally { clearTimeout(timer); } @@ -339,22 +310,14 @@ export class X402Client { } async function safeParseBody(response: Response): Promise { - const ct = response.headers?.get?.('content-type') || ''; - if (!ct.includes('application/json') && !ct.includes('text/json')) { - try { - return await response.json() as T; - } catch { - return undefined; - } + try { + return (await response.json()) as T; + } catch { + return undefined; } - return response.json() as Promise; } -async function timedFetch( - url: string, - init: RequestInit, - timeoutMs: number = DEFAULT_TIMEOUT_MS -): Promise { +async function timedFetch(url: string, init: RequestInit, timeoutMs: number = DEFAULT_TIMEOUT_MS): Promise { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { @@ -364,10 +327,7 @@ async function timedFetch( } } -export function createPaymentRequired( - resource: string, - config: X402MiddlewareConfig -): X402Response { +export function createPaymentRequired(resource: string, config: X402MiddlewareConfig): X402Response { const amount = Math.floor(config.amount * 1_000_000).toString(); const accepts: X402PaymentRequirement[] = [ @@ -380,27 +340,18 @@ export function createPaymentRequired( resource, description: config.description, maxTimeoutSeconds: config.maxTimeoutSeconds || 60, - extra: { - transferTypes: ['internal', 'external'], - amountHidden: true, - }, + extra: { transferTypes: ['internal', 'external'], amountHidden: true }, }, ]; - if (config.additionalSchemes) { - accepts.push(...config.additionalSchemes); - } + if (config.additionalSchemes) accepts.push(...config.additionalSchemes); return { x402Version: 2, accepts, error: 'Payment Required', facilitator: config.facilitatorUrl, - resource: { - url: resource, - description: config.description, - mimeType: 'application/json', - }, + resource: { url: resource, description: config.description, mimeType: 'application/json' }, }; } @@ -410,15 +361,13 @@ export async function verifyPayment( facilitatorUrl: string, apiKey?: string ): Promise { - if (byteLength(paymentHeader) > MAX_PAYMENT_HEADER_BYTES) { - return { valid: false, error: 'Payment header exceeds size limit' }; - } + if (byteLength(paymentHeader) > MAX_PAYMENT_HEADER_BYTES) return { valid: false, error: 'Payment header exceeds size limit' }; try { const headers: Record = { 'Content-Type': 'application/json' }; if (apiKey) headers['X-API-Key'] = apiKey; - const response = await timedFetch(`${facilitatorUrl}/verify`, { + const res = await timedFetch(`${facilitatorUrl}/verify`, { method: 'POST', headers, body: JSON.stringify({ @@ -428,24 +377,19 @@ export async function verifyPayment( }), }); - if (!response.ok) { - const errBody = await safeParseBody<{ error?: string }>(response); - return { valid: false, error: errBody?.error || `Facilitator returned ${response.status}` }; + if (!res.ok) { + const errBody = await safeParseBody<{ error?: string }>(res); + return { valid: false, error: errBody?.error || `Facilitator returned ${res.status}` }; } - const data = await response.json() as { - valid?: boolean; - payer?: string; - amount?: string; - resource?: string; - balance?: number; - sufficient?: boolean; - error?: string; - }; - + const data = (await safeParseBody(res)) || {}; return { valid: !!data.valid, payer: data.payer, + amount: data.amount, + resource: data.resource, + balance: typeof data.balance === 'number' ? data.balance : undefined, + sufficient: typeof data.sufficient === 'boolean' ? data.sufficient : undefined, error: data.error, }; } catch (err) { @@ -466,32 +410,18 @@ export async function settlePayment( const amount = parseInt(requirement.amount, 10) / 1_000_000; - const response = await timedFetch(`${facilitatorUrl}/settle`, { + const res = await timedFetch(`${facilitatorUrl}/settle`, { method: 'POST', headers, - body: JSON.stringify({ - paymentHeader, - merchantWallet: requirement.payTo, - amount, - asset: requirement.asset || 'USDC', - }), + body: JSON.stringify({ paymentHeader, merchantWallet: requirement.payTo, amount, asset: requirement.asset || 'USDC' }), }); - if (!response.ok) { - const errBody = await safeParseBody<{ error?: string }>(response); - return { success: false, error: errBody?.error || `Settlement returned ${response.status}` }; + if (!res.ok) { + const errBody = await safeParseBody<{ error?: string }>(res); + return { success: false, error: errBody?.error || `Settlement returned ${res.status}` }; } - const data = await response.json() as { - success?: boolean; - txHash?: string; - amount?: number; - fee?: number; - net?: number; - network?: string; - error?: string; - }; - + const data = (await safeParseBody(res)) || {}; return { success: !!data.success, txHash: data.txHash, @@ -510,12 +440,13 @@ export async function settlePayment( export function x402Paywall(config: X402MiddlewareConfig) { return async (req: any, res: any, next: any) => { const resource = req.path || req.url || '/'; - const paymentHeader = req.headers?.['x-payment'] as string | undefined; + const paymentHeader = (req.headers && (req.headers['x-payment'] || req.headers['X-Payment'])) as string | undefined; if (!paymentHeader) { const body = createPaymentRequired(resource, config); res.setHeader?.('WWW-Authenticate', 'X402'); res.setHeader?.('X-Payment-Schemes', 'shadowwire'); + res.setHeader?.('Vary', PAYMENT_HEADER_NAME); res.setHeader?.('Cache-Control', 'no-store'); return res.status(402).json(body); } @@ -529,6 +460,7 @@ export function x402Paywall(config: X402MiddlewareConfig) { const body = createPaymentRequired(resource, config); (body as any).verifyError = 'Invalid or unsupported payment proof'; res.setHeader?.('WWW-Authenticate', 'X402'); + res.setHeader?.('Vary', PAYMENT_HEADER_NAME); res.setHeader?.('Cache-Control', 'no-store'); return res.status(402).json(body); } @@ -545,12 +477,22 @@ export function x402Paywall(config: X402MiddlewareConfig) { maxTimeoutSeconds: config.maxTimeoutSeconds || 60, }; + if (proof.payload?.resource && proof.payload.resource !== resource) { + const body = createPaymentRequired(resource, config); + (body as any).verifyError = 'Payment proof resource mismatch'; + res.setHeader?.('WWW-Authenticate', 'X402'); + res.setHeader?.('Vary', PAYMENT_HEADER_NAME); + res.setHeader?.('Cache-Control', 'no-store'); + return res.status(402).json(body); + } + const verifyResult = await verifyPayment(paymentHeader, requirement, config.facilitatorUrl, config.apiKey); if (!verifyResult.valid) { const body = createPaymentRequired(resource, config); (body as any).verifyError = verifyResult.error; res.setHeader?.('WWW-Authenticate', 'X402'); + res.setHeader?.('Vary', PAYMENT_HEADER_NAME); res.setHeader?.('Cache-Control', 'no-store'); return res.status(402).json(body); } @@ -560,6 +502,7 @@ export function x402Paywall(config: X402MiddlewareConfig) { const body = createPaymentRequired(resource, config); (body as any).settleError = settleResult.error; res.setHeader?.('WWW-Authenticate', 'X402'); + res.setHeader?.('Vary', PAYMENT_HEADER_NAME); res.setHeader?.('Cache-Control', 'no-store'); return res.status(402).json(body); } @@ -575,12 +518,7 @@ export function x402Paywall(config: X402MiddlewareConfig) { }; if (config.onPayment && verifyResult.payer && settleResult.txHash) { - config.onPayment({ - payer: verifyResult.payer, - amount: config.amount, - signature: settleResult.txHash, - resource, - }); + config.onPayment({ payer: verifyResult.payer, amount: config.amount, signature: settleResult.txHash, resource }); } next(); @@ -599,17 +537,14 @@ export function createDiscoveryDocument( name: string, payTo: string, resources: X402DiscoveryResource[], - options?: { - description?: string; - facilitatorUrl?: string; - } + options?: { description?: string; facilitatorUrl?: string } ): Record { return { version: '2.0', name, description: options?.description, payTo, - schemes: ['shadowwire', 'exact'], + schemes: ['shadowwire'], networks: ['solana:mainnet'], facilitator: options?.facilitatorUrl, resources: resources.map((r) => ({ From f8fe9e441faae61d8843b9573a9ef4584f253478 Mon Sep 17 00:00:00 2001 From: KAMIYO Date: Fri, 6 Feb 2026 13:21:45 +0100 Subject: [PATCH 6/7] Add custom errors, tests, and examples for x402 protocol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add X402InvalidSchemeError, X402HeaderTooLargeError, X402FacilitatorError - Replace generic error strings with typed errors in x402.ts - Add JSDoc to public x402 exports - Add vitest with 35 test cases covering encode/decode, request flow, middleware, verify/settle, and discovery document - Fix Unicode-unsafe browser base64 (btoa/atob → TextEncoder path) - Add x402Version validation in decodePaymentHeader - Use URL API for facilitator endpoint composition - Wrap onPayment callback in try/catch to prevent post-settlement crashes - Split x402-payment.ts into focused x402-server.ts and x402-client.ts --- examples/x402-client.ts | 97 +++ examples/x402-payment.ts | 83 -- examples/x402-server.ts | 58 ++ package-lock.json | 1563 +++++++++++++++++++++++++++++++++++++- package.json | 5 +- src/errors.ts | 24 + src/index.ts | 3 + src/x402.test.ts | 477 ++++++++++++ src/x402.ts | 54 +- tsconfig.json | 2 +- 10 files changed, 2265 insertions(+), 101 deletions(-) create mode 100644 examples/x402-client.ts delete mode 100644 examples/x402-payment.ts create mode 100644 examples/x402-server.ts create mode 100644 src/x402.test.ts diff --git a/examples/x402-client.ts b/examples/x402-client.ts new file mode 100644 index 0000000..dbb99bc --- /dev/null +++ b/examples/x402-client.ts @@ -0,0 +1,97 @@ +/** + * x402 Client Example + * + * Demonstrates both the automatic request() flow and a manual + * step-by-step payment flow against an x402-protected server. + * + * Environment variables: + * WALLET_ADDRESS – Sender's Solana wallet address + * SERVER_URL – Base URL of the x402 server (default: http://localhost:3000) + */ +import { ShadowWireClient, X402Client } from '@radr/shadowwire'; + +const WALLET_ADDRESS = process.env.WALLET_ADDRESS || 'YOUR_WALLET_ADDRESS'; +const SERVER_URL = process.env.SERVER_URL || 'http://localhost:3000'; + +const wallet = { + signMessage: async (_message: Uint8Array) => { + // Replace with real wallet adapter (e.g. Phantom, Solflare) + throw new Error('Implement wallet signing'); + }, +}; + +const client = new ShadowWireClient(); +const x402 = new X402Client({ + client, + wallet, + senderWallet: WALLET_ADDRESS, + defaultToken: 'USDC', + defaultTransferType: 'external', + requestTimeoutMs: 20_000, +}); + +/** + * Automatic flow — X402Client detects 402, pays, and retries in one call. + */ +async function payForData() { + const result = await x402.request(`${SERVER_URL}/api/data`); + + if (result.success) { + console.log('Data:', result.data); + if (result.payment) { + console.log('Paid via:', result.payment.transfer.tx_signature); + console.log('Amount hidden:', result.payment.transfer.amount_hidden); + } + } else { + console.error('Request failed:', result.error); + } +} + +/** + * Manual flow — probe, parse requirements, pay, then retry with the header. + */ +async function manualPayFlow() { + // 1. Probe the endpoint + const probe = await fetch(`${SERVER_URL}/api/data`); + + if (probe.status !== 402) { + console.log('No payment required, status:', probe.status); + return; + } + + // 2. Parse payment requirements + const body = await probe.json(); + const requirements = X402Client.parseRequirements(body); + + if (!requirements) { + console.error('Could not parse 402 response'); + return; + } + + console.log('Payment options:', requirements.accepts.length); + const requirement = requirements.accepts[0]; + console.log(`Scheme: ${requirement.scheme}, Amount: ${requirement.amount}, Asset: ${requirement.asset}`); + + // 3. Execute payment + const payment = await x402.pay(requirement); + if (!payment.success || !payment.paymentHeader) { + console.error('Payment failed:', payment.error); + return; + } + + console.log('Payment tx:', payment.transfer?.tx_signature); + + // 4. Retry with X-Payment header + const response = await fetch(`${SERVER_URL}/api/data`, { + headers: { 'X-Payment': payment.paymentHeader }, + }); + + if (response.ok) { + console.log('Unlocked:', await response.json()); + } else { + console.error('Retry failed:', response.status); + } +} + +payForData().catch(console.error); +// manualPayFlow().catch(console.error); diff --git a/examples/x402-payment.ts b/examples/x402-payment.ts deleted file mode 100644 index 5086f7b..0000000 --- a/examples/x402-payment.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { - ShadowWireClient, - X402Client, - x402Paywall, - createDiscoveryDocument, -} from '@radr/shadowwire'; - -async function clientExample() { - const client = new ShadowWireClient(); - - const wallet = { - signMessage: async (_message: Uint8Array) => { - throw new Error('Implement wallet signing'); - }, - }; - - const x402 = new X402Client({ - client, - wallet, - senderWallet: 'YOUR_WALLET_ADDRESS', - defaultToken: 'USDC', - defaultTransferType: 'external', - requestTimeoutMs: 20_000, - }); - - const res1 = await x402.request('https://api.example.com/data'); - if (res1.success) { - console.log('Data:', res1.data); - if (res1.payment) { - console.log('Paid:', res1.payment.transfer.tx_signature); - console.log('Amount hidden:', res1.payment.transfer.amount_hidden); - } - } - - const probe = await fetch('https://api.example.com/premium'); - if (probe.status === 402) { - const body = await probe.json(); - const requirements = X402Client.parseRequirements(body); - if (requirements) { - const req = requirements.accepts[0]; - const payment = await x402.pay(req); - if (payment.success && payment.paymentHeader) { - await fetch('https://api.example.com/premium', { headers: { 'X-Payment': payment.paymentHeader } }); - } - } - } -} - -async function serverExample() { - const express = require('express'); - const app = express(); - - app.get( - '/api/data', - x402Paywall({ - payTo: 'YOUR_MERCHANT_WALLET', - amount: 0.01, - asset: 'USDC', - description: 'Premium data endpoint', - facilitatorUrl: 'https://x402.kamiyo.ai', - apiKey: 'YOUR_API_KEY', - onPayment: (info: { payer: string; amount: number; signature: string; resource: string }) => { - console.log(`Payment from ${info.payer}: ${info.signature}`); - }, - }), - (req: any, res: any) => { - res.json({ data: 'premium content', payment: req.x402 }); - } - ); - - app.get('/.well-known/x402', (_req: any, res: any) => { - res.json( - createDiscoveryDocument('My API', 'YOUR_MERCHANT_WALLET', [ - { path: '/api/data', method: 'GET', price: 0.01, description: 'Premium data' }, - { path: '/api/signals', method: 'GET', price: 0.05, description: 'Trading signals' }, - ]) - ); - }); - - app.listen(3000); -} - -clientExample().catch(console.error); diff --git a/examples/x402-server.ts b/examples/x402-server.ts new file mode 100644 index 0000000..fcb3d42 --- /dev/null +++ b/examples/x402-server.ts @@ -0,0 +1,58 @@ +/** + * x402 Server Example + * + * Express server with a paywall-protected route, a free route, + * and a .well-known/x402 discovery endpoint. + * + * Environment variables: + * MERCHANT_WALLET – Solana wallet that receives payments + * FACILITATOR_URL – x402 facilitator endpoint (default: https://x402.kamiyo.ai) + * API_KEY – API key for the facilitator + * PORT – Server port (default: 3000) + */ +import { x402Paywall, createDiscoveryDocument } from '@radr/shadowwire'; + +const express = require('express'); +const app = express(); + +const MERCHANT_WALLET = process.env.MERCHANT_WALLET || 'YOUR_MERCHANT_WALLET'; +const FACILITATOR_URL = process.env.FACILITATOR_URL || 'https://x402.kamiyo.ai'; +const API_KEY = process.env.API_KEY || ''; +const PORT = Number(process.env.PORT) || 3000; + +// Free route — no payment needed +app.get('/api/status', (_req: any, res: any) => { + res.json({ status: 'ok', timestamp: Date.now() }); +}); + +// Paywall-protected route +app.get( + '/api/data', + x402Paywall({ + payTo: MERCHANT_WALLET, + amount: 0.01, + asset: 'USDC', + description: 'Premium data endpoint', + facilitatorUrl: FACILITATOR_URL, + apiKey: API_KEY, + onPayment: (info) => { + console.log(`Payment received from ${info.payer}: ${info.signature} (${info.amount} USDC)`); + }, + }), + (req: any, res: any) => { + res.json({ data: 'premium content', payment: req.x402 }); + } +); + +// Discovery endpoint +app.get('/.well-known/x402', (_req: any, res: any) => { + res.json( + createDiscoveryDocument('My API', MERCHANT_WALLET, [ + { path: '/api/data', method: 'GET', price: 0.01, description: 'Premium data' }, + ], { facilitatorUrl: FACILITATOR_URL }) + ); +}); + +app.listen(PORT, () => { + console.log(`x402 server listening on http://localhost:${PORT}`); +}); diff --git a/package-lock.json b/package-lock.json index 69dc70b..1765e24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@radr/shadowwire", - "version": "1.1.1", + "version": "1.1.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@radr/shadowwire", - "version": "1.1.1", + "version": "1.1.15", "license": "MIT", "dependencies": { "@solana/web3.js": "^1.95.3", @@ -16,7 +16,8 @@ "@types/bs58": "^4.0.1", "@types/node": "^22.5.5", "ts-node": "^10.9.2", - "typescript": "^5.6.2" + "typescript": "^5.6.2", + "vitest": "^3.0.0" } }, "node_modules/@babel/runtime": { @@ -41,6 +42,448 @@ "node": ">=12" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -96,6 +539,356 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@solana/buffer-layout": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz", @@ -238,6 +1031,17 @@ "base-x": "^3.0.6" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -247,6 +1051,20 @@ "@types/node": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.19.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", @@ -271,6 +1089,121 @@ "@types/node": "*" } }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -316,6 +1249,16 @@ "dev": true, "license": "MIT" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/base-x": { "version": "3.0.11", "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", @@ -424,6 +1367,33 @@ "node": ">=6.14.2" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -436,6 +1406,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/commander": { "version": "14.0.2", "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", @@ -452,6 +1432,34 @@ "dev": true, "license": "MIT" }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/delay": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", @@ -474,6 +1482,13 @@ "node": ">=0.3.1" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es6-promise": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", @@ -489,12 +1504,74 @@ "es6-promise": "^4.0.3" } }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "license": "MIT" }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/eyes": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", @@ -509,6 +1586,39 @@ "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", "license": "MIT" }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", @@ -585,12 +1695,36 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "license": "ISC" }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -604,6 +1738,25 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -636,6 +1789,117 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, "node_modules/rpc-websockets": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.3.1.tgz", @@ -709,6 +1973,37 @@ ], "license": "MIT" }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/stream-chain": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", @@ -724,6 +2019,19 @@ "stream-chain": "^2.2.5" } }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/superstruct": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz", @@ -738,6 +2046,67 @@ "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -843,6 +2212,177 @@ "dev": true, "license": "MIT" }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -859,6 +2399,23 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ws": { "version": "7.5.10", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", diff --git a/package.json b/package.json index 230ed52..1af00fb 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,8 @@ "license": "MIT", "scripts": { "build": "tsc && node -e \"require('fs').cpSync('wasm', 'dist/wasm', {recursive:true})\"", + "test": "vitest run", + "test:watch": "vitest", "prepublish": "npm run build", "prepare": "npm run build" }, @@ -37,6 +39,7 @@ "@types/node": "^22.5.5", "@types/bs58": "^4.0.1", "ts-node": "^10.9.2", - "typescript": "^5.6.2" + "typescript": "^5.6.2", + "vitest": "^3.0.0" } } diff --git a/src/errors.ts b/src/errors.ts index 3eebb8e..4559697 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -78,3 +78,27 @@ export class ProofGenerationError extends ShadowWireError { } } +export class X402InvalidSchemeError extends ShadowWireError { + constructor(scheme: string) { + super(`Unsupported x402 payment scheme: ${scheme}`); + this.name = 'X402InvalidSchemeError'; + Object.setPrototypeOf(this, X402InvalidSchemeError.prototype); + } +} + +export class X402HeaderTooLargeError extends ShadowWireError { + constructor(size?: number) { + super(size ? `Payment header exceeds size limit (${size} bytes)` : 'Payment header exceeds size limit'); + this.name = 'X402HeaderTooLargeError'; + Object.setPrototypeOf(this, X402HeaderTooLargeError.prototype); + } +} + +export class X402FacilitatorError extends ShadowWireError { + constructor(message: string = 'Facilitator request failed') { + super(message); + this.name = 'X402FacilitatorError'; + Object.setPrototypeOf(this, X402FacilitatorError.prototype); + } +} + diff --git a/src/index.ts b/src/index.ts index 110678a..84e056c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,9 @@ export { NetworkError, WASMNotSupportedError, ProofGenerationError, + X402InvalidSchemeError, + X402HeaderTooLargeError, + X402FacilitatorError, } from './errors'; export { diff --git a/src/x402.test.ts b/src/x402.test.ts new file mode 100644 index 0000000..7f549bb --- /dev/null +++ b/src/x402.test.ts @@ -0,0 +1,477 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { + X402Client, + x402Paywall, + createPaymentRequired, + verifyPayment, + settlePayment, + createDiscoveryDocument, + X402PaymentProof, + X402PaymentRequirement, + X402MiddlewareConfig, +} from './x402'; + +function mockResponse(status: number, body: unknown, ok?: boolean): Response { + return { + status, + statusText: status === 200 ? 'OK' : 'Error', + ok: ok ?? (status >= 200 && status < 300), + json: () => Promise.resolve(body), + headers: new Headers(), + } as unknown as Response; +} + +function validProof(): X402PaymentProof { + return { + x402Version: 2, + scheme: 'shadowwire', + network: 'solana:mainnet', + payload: { + signature: 'abc123sig', + amountHidden: true, + resource: '/api/data', + payTo: 'merchant_wallet', + sender: 'sender_wallet', + }, + }; +} + +function baseRequirement(overrides?: Partial): X402PaymentRequirement { + return { + scheme: 'shadowwire', + network: 'solana:mainnet', + amount: '10000', + asset: 'USDC', + payTo: 'merchant_wallet', + resource: '/api/data', + ...overrides, + }; +} + +function middlewareConfig(overrides?: Partial): X402MiddlewareConfig { + return { + payTo: 'merchant_wallet', + amount: 0.01, + asset: 'USDC', + description: 'Test endpoint', + facilitatorUrl: 'https://facilitator.test', + apiKey: 'test-key', + ...overrides, + }; +} + +function mockReq(headers: Record = {}, path = '/api/data') { + return { headers, path, url: path }; +} + +function mockRes() { + const res: any = { + _status: 0, + _body: null, + _headers: {} as Record, + status(code: number) { res._status = code; return res; }, + json(body: unknown) { res._body = body; return res; }, + setHeader(k: string, v: string) { res._headers[k] = v; }, + }; + return res; +} + +describe('encodePaymentHeader / decodePaymentHeader', () => { + it('round-trips a valid proof', () => { + const proof = validProof(); + const encoded = X402Client.encodePaymentHeader(proof); + const decoded = X402Client.decodePaymentHeader(encoded); + expect(decoded).toEqual(proof); + }); + + it('returns null for malformed base64', () => { + expect(X402Client.decodePaymentHeader('not!valid!base64!!!')).toBeNull(); + }); + + it('returns null for valid base64 but invalid JSON', () => { + const encoded = Buffer.from('not json', 'utf-8').toString('base64'); + expect(X402Client.decodePaymentHeader(encoded)).toBeNull(); + }); + + it('returns null when required fields are missing', () => { + const partial = { x402Version: 2, network: 'solana:mainnet' }; + const encoded = Buffer.from(JSON.stringify(partial), 'utf-8').toString('base64'); + expect(X402Client.decodePaymentHeader(encoded)).toBeNull(); + }); + + it('returns null for wrong x402Version', () => { + const wrongVersion = { ...validProof(), x402Version: 1 }; + const encoded = Buffer.from(JSON.stringify(wrongVersion), 'utf-8').toString('base64'); + expect(X402Client.decodePaymentHeader(encoded)).toBeNull(); + }); + + it('returns null for oversized header', () => { + const huge = Buffer.from('x'.repeat(20_000), 'utf-8').toString('base64'); + expect(X402Client.decodePaymentHeader(huge)).toBeNull(); + }); +}); + +describe('X402Client.request()', () => { + let client: X402Client; + let mockTransfer: ReturnType; + + beforeEach(() => { + mockTransfer = vi.fn(); + const shadowClient = { transfer: mockTransfer, getBalance: vi.fn(), calculateFee: vi.fn() } as any; + client = new X402Client({ + client: shadowClient, + wallet: { signMessage: vi.fn() } as any, + senderWallet: 'sender_wallet', + }); + }); + + it('passes through non-402 successful responses', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse(200, { ok: true }))); + const result = await client.request('https://api.test/data'); + expect(result.success).toBe(true); + expect(result.data).toEqual({ ok: true }); + expect(result.statusCode).toBe(200); + vi.unstubAllGlobals(); + }); + + it('returns error for non-402, non-ok responses', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse(500, null, false))); + const result = await client.request('https://api.test/data'); + expect(result.success).toBe(false); + expect(result.statusCode).toBe(500); + vi.unstubAllGlobals(); + }); + + it('handles 402 → pay → retry flow', async () => { + const x402Body = { + x402Version: 2, + accepts: [baseRequirement()], + }; + + mockTransfer.mockResolvedValue({ + success: true, + tx_signature: 'sig123', + amount_hidden: true, + }); + + const fetchMock = vi.fn() + .mockResolvedValueOnce(mockResponse(402, x402Body, false)) + .mockResolvedValueOnce(mockResponse(200, { premium: true })); + + vi.stubGlobal('fetch', fetchMock); + const result = await client.request('https://api.test/data'); + expect(result.success).toBe(true); + expect(result.data).toEqual({ premium: true }); + expect(result.payment?.transfer.tx_signature).toBe('sig123'); + expect(fetchMock).toHaveBeenCalledTimes(2); + vi.unstubAllGlobals(); + }); + + it('returns error when no compatible scheme is found', async () => { + const x402Body = { + x402Version: 2, + accepts: [baseRequirement({ scheme: 'stripe' })], + }; + + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse(402, x402Body, false))); + const result = await client.request('https://api.test/data'); + expect(result.success).toBe(false); + expect(result.error).toContain('No compatible'); + vi.unstubAllGlobals(); + }); + + it('returns error when transfer fails', async () => { + const x402Body = { + x402Version: 2, + accepts: [baseRequirement()], + }; + + mockTransfer.mockResolvedValue({ success: false }); + + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse(402, x402Body, false))); + const result = await client.request('https://api.test/data'); + expect(result.success).toBe(false); + expect(result.error).toContain('transfer failed'); + vi.unstubAllGlobals(); + }); + + it('returns error for 402 with empty accepts array', async () => { + const x402Body = { x402Version: 2, accepts: [] }; + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse(402, x402Body, false))); + const result = await client.request('https://api.test/data'); + expect(result.success).toBe(false); + expect(result.error).toContain('No accepted payment'); + vi.unstubAllGlobals(); + }); +}); + +describe('X402Client.pay()', () => { + let client: X402Client; + let mockTransfer: ReturnType; + + beforeEach(() => { + mockTransfer = vi.fn(); + const shadowClient = { transfer: mockTransfer, getBalance: vi.fn(), calculateFee: vi.fn() } as any; + client = new X402Client({ + client: shadowClient, + wallet: { signMessage: vi.fn() } as any, + senderWallet: 'sender_wallet', + }); + }); + + it('rejects unsupported payment schemes', async () => { + const result = await client.pay(baseRequirement({ scheme: 'stripe' })); + expect(result.success).toBe(false); + expect(result.error).toContain('Unsupported x402 payment scheme'); + expect(mockTransfer).not.toHaveBeenCalled(); + }); + + it('rejects invalid amount', async () => { + const result = await client.pay(baseRequirement({ amount: '0' })); + expect(result.success).toBe(false); + expect(result.error).toContain('Invalid payment amount'); + expect(mockTransfer).not.toHaveBeenCalled(); + }); + + it('catches transfer exceptions', async () => { + mockTransfer.mockRejectedValue(new Error('rpc down')); + const result = await client.pay(baseRequirement({ amount: '1000000' })); + expect(result.success).toBe(false); + expect(result.error).toContain('rpc down'); + }); +}); + +describe('X402Client static helpers', () => { + it('parseRequirements returns null for non-objects', () => { + expect(X402Client.parseRequirements(null)).toBeNull(); + expect(X402Client.parseRequirements('string')).toBeNull(); + expect(X402Client.parseRequirements(42)).toBeNull(); + }); + + it('parseRequirements returns null when accepts is not an array', () => { + expect(X402Client.parseRequirements({ accepts: 'not-array' })).toBeNull(); + expect(X402Client.parseRequirements({ x402Version: 2 })).toBeNull(); + }); + + it('is402 returns true only for 402 with valid body', () => { + const validBody = { x402Version: 2, accepts: [baseRequirement()] }; + expect(X402Client.is402(402, validBody)).toBe(true); + expect(X402Client.is402(200, validBody)).toBe(false); + expect(X402Client.is402(402, {})).toBe(false); + }); +}); + +describe('x402Paywall middleware', () => { + it('returns 402 when no payment header is present', async () => { + const mw = x402Paywall(middlewareConfig()); + const req = mockReq(); + const res = mockRes(); + const next = vi.fn(); + + await mw(req, res, next); + expect(res._status).toBe(402); + expect(res._body.accepts).toBeDefined(); + expect(res._body.accepts[0].scheme).toBe('shadowwire'); + expect(next).not.toHaveBeenCalled(); + }); + + it('returns 400 for oversized payment header', async () => { + const mw = x402Paywall(middlewareConfig()); + const hugeHeader = 'x'.repeat(20_000); + const req = mockReq({ 'x-payment': hugeHeader }); + const res = mockRes(); + const next = vi.fn(); + + await mw(req, res, next); + expect(res._status).toBe(400); + expect(res._body.error).toContain('exceeds size limit'); + expect(next).not.toHaveBeenCalled(); + }); + + it('returns 402 for resource mismatch in proof', async () => { + const mismatchProof = validProof(); + mismatchProof.payload.resource = '/api/other'; + const header = X402Client.encodePaymentHeader(mismatchProof); + const mw = x402Paywall(middlewareConfig()); + const req = mockReq({ 'x-payment': header }, '/api/data'); + const res = mockRes(); + const next = vi.fn(); + + await mw(req, res, next); + expect(res._status).toBe(402); + expect(res._body.verifyError).toContain('resource mismatch'); + expect(next).not.toHaveBeenCalled(); + }); + + it('returns 402 for invalid/unsupported scheme in proof', async () => { + const badProof = { ...validProof(), scheme: 'stripe' }; + const header = X402Client.encodePaymentHeader(badProof as X402PaymentProof); + const mw = x402Paywall(middlewareConfig()); + const req = mockReq({ 'x-payment': header }); + const res = mockRes(); + const next = vi.fn(); + + await mw(req, res, next); + expect(res._status).toBe(402); + expect(next).not.toHaveBeenCalled(); + }); + + it('calls next() on valid payment verification and settlement', async () => { + const header = X402Client.encodePaymentHeader(validProof()); + const mw = x402Paywall(middlewareConfig()); + const req = mockReq({ 'x-payment': header }); + const res = mockRes(); + const next = vi.fn(); + + vi.stubGlobal('fetch', vi.fn() + .mockResolvedValueOnce(mockResponse(200, { valid: true, payer: 'sender_wallet', amount: '10000', resource: '/api/data' })) + .mockResolvedValueOnce(mockResponse(200, { success: true, txHash: 'tx123', amount: 0.01, fee: 0.001, net: 0.009, network: 'solana:mainnet' }))); + + await mw(req, res, next); + expect(next).toHaveBeenCalled(); + expect(req.x402).toBeDefined(); + expect(req.x402.txHash).toBe('tx123'); + vi.unstubAllGlobals(); + }); + + it('calls next() even if onPayment callback throws', async () => { + const header = X402Client.encodePaymentHeader(validProof()); + const mw = x402Paywall(middlewareConfig({ + onPayment: () => { throw new Error('callback boom'); }, + })); + const req = mockReq({ 'x-payment': header }); + const res = mockRes(); + const next = vi.fn(); + + vi.stubGlobal('fetch', vi.fn() + .mockResolvedValueOnce(mockResponse(200, { valid: true, payer: 'sender_wallet', amount: '10000', resource: '/api/data' })) + .mockResolvedValueOnce(mockResponse(200, { success: true, txHash: 'tx123', amount: 0.01, fee: 0.001, net: 0.009, network: 'solana:mainnet' }))); + + await mw(req, res, next); + expect(next).toHaveBeenCalled(); + vi.unstubAllGlobals(); + }); +}); + +describe('createPaymentRequired', () => { + it('returns correct shape with default values', () => { + const body = createPaymentRequired('/api/data', middlewareConfig()); + expect(body.x402Version).toBe(2); + expect(body.accepts).toHaveLength(1); + expect(body.accepts[0].scheme).toBe('shadowwire'); + expect(body.accepts[0].asset).toBe('USDC'); + expect(body.accepts[0].payTo).toBe('merchant_wallet'); + expect(body.facilitator).toBe('https://facilitator.test'); + }); + + it('includes additional schemes when provided', () => { + const extra: X402PaymentRequirement = baseRequirement({ scheme: 'lightning', network: 'bitcoin' }); + const body = createPaymentRequired('/api/data', middlewareConfig({ additionalSchemes: [extra] })); + expect(body.accepts).toHaveLength(2); + expect(body.accepts[1].scheme).toBe('lightning'); + }); +}); + +describe('verifyPayment', () => { + it('returns valid result on facilitator success', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue( + mockResponse(200, { valid: true, payer: 'sender', amount: '10000', resource: '/api/data' }) + )); + + const result = await verifyPayment('header', baseRequirement(), 'https://facilitator.test', 'key'); + expect(result.valid).toBe(true); + expect(result.payer).toBe('sender'); + vi.unstubAllGlobals(); + }); + + it('returns invalid on facilitator rejection', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue( + mockResponse(403, { error: 'Invalid signature' }, false) + )); + + const result = await verifyPayment('header', baseRequirement(), 'https://facilitator.test'); + expect(result.valid).toBe(false); + expect(result.error).toContain('Invalid signature'); + vi.unstubAllGlobals(); + }); + + it('returns invalid on network failure', async () => { + vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('ECONNREFUSED'))); + + const result = await verifyPayment('header', baseRequirement(), 'https://facilitator.test'); + expect(result.valid).toBe(false); + expect(result.error).toContain('ECONNREFUSED'); + vi.unstubAllGlobals(); + }); + + it('short-circuits on oversized header', async () => { + const fetchMock = vi.fn(); + vi.stubGlobal('fetch', fetchMock); + + const result = await verifyPayment('x'.repeat(20_000), baseRequirement(), 'https://facilitator.test'); + expect(result.valid).toBe(false); + expect(result.error).toContain('exceeds size limit'); + expect(fetchMock).not.toHaveBeenCalled(); + vi.unstubAllGlobals(); + }); +}); + +describe('settlePayment', () => { + it('returns success on facilitator settlement', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue( + mockResponse(200, { success: true, txHash: 'tx456', amount: 0.01, fee: 0.001, net: 0.009, network: 'solana:mainnet' }) + )); + + const result = await settlePayment('header', baseRequirement(), 'https://facilitator.test', 'key'); + expect(result.success).toBe(true); + expect(result.txHash).toBe('tx456'); + vi.unstubAllGlobals(); + }); + + it('returns failure on facilitator rejection', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue( + mockResponse(400, { error: 'Already settled' }, false) + )); + + const result = await settlePayment('header', baseRequirement(), 'https://facilitator.test'); + expect(result.success).toBe(false); + expect(result.error).toContain('Already settled'); + vi.unstubAllGlobals(); + }); + + it('returns failure on network error', async () => { + vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('timeout'))); + + const result = await settlePayment('header', baseRequirement(), 'https://facilitator.test'); + expect(result.success).toBe(false); + expect(result.error).toContain('timeout'); + vi.unstubAllGlobals(); + }); +}); + +describe('createDiscoveryDocument', () => { + it('returns correct shape with defaults', () => { + const doc = createDiscoveryDocument('My API', 'merchant_wallet', [ + { path: '/api/data', method: 'GET', price: 0.01, description: 'Premium data' }, + ]); + + expect(doc.version).toBe('2.0'); + expect(doc.name).toBe('My API'); + expect(doc.payTo).toBe('merchant_wallet'); + expect(doc.schemes).toEqual(['shadowwire']); + expect(doc.networks).toEqual(['solana:mainnet']); + expect((doc.resources as any[])).toHaveLength(1); + expect((doc.resources as any[])[0].asset).toBe('USDC'); + expect((doc.capabilities as any).privatePayments).toBe(true); + }); + + it('includes facilitatorUrl from options', () => { + const doc = createDiscoveryDocument('API', 'wallet', [], { + facilitatorUrl: 'https://facilitator.test', + description: 'Test API', + }); + + expect(doc.facilitator).toBe('https://facilitator.test'); + expect(doc.description).toBe('Test API'); + }); +}); diff --git a/src/x402.ts b/src/x402.ts index 7b2d40e..6c157c8 100644 --- a/src/x402.ts +++ b/src/x402.ts @@ -1,11 +1,12 @@ import { ShadowWireClient } from './client'; import { TokenSymbol, WalletAdapter, TransferResponse, PoolBalance } from './types'; -import { NetworkError } from './errors'; +import { NetworkError, X402InvalidSchemeError, X402HeaderTooLargeError, X402FacilitatorError } from './errors'; const DEFAULT_TIMEOUT_MS = 15_000; const MAX_PAYMENT_HEADER_BYTES = 16_384; const PAYMENT_HEADER_NAME = 'X-Payment'; +/** Describes a single payment option a server will accept for a 402-protected resource. */ export interface X402PaymentRequirement { scheme: string; network: string; @@ -49,6 +50,7 @@ export interface X402RequestResult { statusCode: number; } +/** Configuration for creating an X402Client instance. */ export interface X402ClientConfig { client: ShadowWireClient; wallet: WalletAdapter; @@ -70,6 +72,7 @@ export interface X402VerifyResult { error?: string; } +/** Configuration for the Express x402Paywall middleware. */ export interface X402MiddlewareConfig { payTo: string; amount: number; @@ -97,12 +100,17 @@ export interface X402PaymentProof { function toBase64(input: string): string { if (typeof Buffer !== 'undefined') return Buffer.from(input, 'utf-8').toString('base64'); - return btoa(input); + const bytes = new TextEncoder().encode(input); + let bin = ''; + bytes.forEach((b) => (bin += String.fromCharCode(b))); + return btoa(bin); } function fromBase64(encoded: string): string { if (typeof Buffer !== 'undefined') return Buffer.from(encoded, 'base64').toString('utf-8'); - return atob(encoded); + const bin = atob(encoded); + const bytes = Uint8Array.from(bin, (c) => c.charCodeAt(0)); + return new TextDecoder().decode(bytes); } function byteLength(str: string): number { @@ -118,6 +126,7 @@ function sleep(ms: number) { return new Promise((r) => setTimeout(r, ms)); } +/** HTTP client that automatically handles x402 payment flows via ShadowWire transfers. */ export class X402Client { private client: ShadowWireClient; private wallet: WalletAdapter; @@ -128,6 +137,7 @@ export class X402Client { private headers: Record; private timeoutMs: number; + /** Creates a new X402 client bound to a ShadowWire instance and wallet. */ constructor(config: X402ClientConfig) { this.client = config.client; this.wallet = config.wallet; @@ -139,6 +149,7 @@ export class X402Client { this.timeoutMs = config.requestTimeoutMs ?? DEFAULT_TIMEOUT_MS; } + /** Fetches a URL, automatically paying the 402 requirement if one is returned. */ async request(url: string, options?: RequestInit): Promise> { const mergedHeaders: Record = { ...this.headers, @@ -196,8 +207,9 @@ export class X402Client { return { success: false, error: 'Unexpected error', statusCode: 500 }; } + /** Executes a ShadowWire transfer to fulfil a single payment requirement. */ async pay(requirement: X402PaymentRequirement): Promise { - if (!isShadowwire(requirement.scheme)) return { success: false, error: `Unsupported scheme: ${requirement.scheme}` }; + if (!isShadowwire(requirement.scheme)) return { success: false, error: new X402InvalidSchemeError(requirement.scheme).message }; const amount = this.parseAmount(requirement.amount, requirement.asset); if (amount <= 0) return { success: false, error: 'Invalid payment amount' }; @@ -242,6 +254,7 @@ export class X402Client { return this.client.calculateFee(amount, token || this.defaultToken); } + /** Parses a 402 response body into structured payment requirements, or null if invalid. */ static parseRequirements(body: unknown): X402Response | null { if (!body || typeof body !== 'object') return null; const obj = body as Record; @@ -249,18 +262,22 @@ export class X402Client { return obj as unknown as X402Response; } + /** Returns true if the status is 402 and the body contains valid payment requirements. */ static is402(status: number, body: unknown): boolean { return status === 402 && X402Client.parseRequirements(body) !== null; } + /** Encodes a payment proof into a base64 string suitable for the X-Payment header. */ static encodePaymentHeader(proof: X402PaymentProof): string { return toBase64(JSON.stringify(proof)); } + /** Decodes a base64 X-Payment header into a payment proof, or null if malformed. */ static decodePaymentHeader(header: string): X402PaymentProof | null { try { if (byteLength(header) > MAX_PAYMENT_HEADER_BYTES) return null; const decoded = JSON.parse(fromBase64(header)); + if (decoded.x402Version !== 2) return null; if (!decoded.scheme || !decoded.payload?.signature) return null; return decoded as X402PaymentProof; } catch { @@ -327,6 +344,7 @@ async function timedFetch(url: string, init: RequestInit, timeoutMs: number = DE } } +/** Builds a 402 response body for the given resource and middleware config. */ export function createPaymentRequired(resource: string, config: X402MiddlewareConfig): X402Response { const amount = Math.floor(config.amount * 1_000_000).toString(); @@ -355,19 +373,21 @@ export function createPaymentRequired(resource: string, config: X402MiddlewareCo }; } +/** Sends a payment proof to the facilitator for verification. */ export async function verifyPayment( paymentHeader: string, requirement: X402PaymentRequirement, facilitatorUrl: string, apiKey?: string ): Promise { - if (byteLength(paymentHeader) > MAX_PAYMENT_HEADER_BYTES) return { valid: false, error: 'Payment header exceeds size limit' }; + if (byteLength(paymentHeader) > MAX_PAYMENT_HEADER_BYTES) return { valid: false, error: new X402HeaderTooLargeError(byteLength(paymentHeader)).message }; try { const headers: Record = { 'Content-Type': 'application/json' }; if (apiKey) headers['X-API-Key'] = apiKey; - const res = await timedFetch(`${facilitatorUrl}/verify`, { + const verifyUrl = new URL('/verify', facilitatorUrl).toString(); + const res = await timedFetch(verifyUrl, { method: 'POST', headers, body: JSON.stringify({ @@ -379,7 +399,7 @@ export async function verifyPayment( if (!res.ok) { const errBody = await safeParseBody<{ error?: string }>(res); - return { valid: false, error: errBody?.error || `Facilitator returned ${res.status}` }; + return { valid: false, error: new X402FacilitatorError(errBody?.error || `Facilitator returned ${res.status}`).message }; } const data = (await safeParseBody(res)) || {}; @@ -394,10 +414,11 @@ export async function verifyPayment( }; } catch (err) { const message = err instanceof Error ? err.message : 'Unknown error'; - return { valid: false, error: `Facilitator error: ${message}` }; + return { valid: false, error: new X402FacilitatorError(message).message }; } } +/** Settles a verified payment via the facilitator, finalising the transfer. */ export async function settlePayment( paymentHeader: string, requirement: X402PaymentRequirement, @@ -410,7 +431,8 @@ export async function settlePayment( const amount = parseInt(requirement.amount, 10) / 1_000_000; - const res = await timedFetch(`${facilitatorUrl}/settle`, { + const settleUrl = new URL('/settle', facilitatorUrl).toString(); + const res = await timedFetch(settleUrl, { method: 'POST', headers, body: JSON.stringify({ paymentHeader, merchantWallet: requirement.payTo, amount, asset: requirement.asset || 'USDC' }), @@ -418,7 +440,7 @@ export async function settlePayment( if (!res.ok) { const errBody = await safeParseBody<{ error?: string }>(res); - return { success: false, error: errBody?.error || `Settlement returned ${res.status}` }; + return { success: false, error: new X402FacilitatorError(errBody?.error || `Settlement returned ${res.status}`).message }; } const data = (await safeParseBody(res)) || {}; @@ -433,10 +455,11 @@ export async function settlePayment( }; } catch (err) { const message = err instanceof Error ? err.message : 'Unknown error'; - return { success: false, error: `Settlement error: ${message}` }; + return { success: false, error: new X402FacilitatorError(message).message }; } } +/** Express middleware that gates a route behind an x402 payment wall. */ export function x402Paywall(config: X402MiddlewareConfig) { return async (req: any, res: any, next: any) => { const resource = req.path || req.url || '/'; @@ -452,7 +475,7 @@ export function x402Paywall(config: X402MiddlewareConfig) { } if (byteLength(paymentHeader) > MAX_PAYMENT_HEADER_BYTES) { - return res.status(400).json({ error: 'Payment header too large' }); + return res.status(400).json({ error: new X402HeaderTooLargeError(byteLength(paymentHeader)).message }); } const proof = X402Client.decodePaymentHeader(paymentHeader); @@ -518,7 +541,11 @@ export function x402Paywall(config: X402MiddlewareConfig) { }; if (config.onPayment && verifyResult.payer && settleResult.txHash) { - config.onPayment({ payer: verifyResult.payer, amount: config.amount, signature: settleResult.txHash, resource }); + try { + config.onPayment({ payer: verifyResult.payer, amount: config.amount, signature: settleResult.txHash, resource }); + } catch { + // Don't block the request — callback errors are non-fatal + } } next(); @@ -533,6 +560,7 @@ export interface X402DiscoveryResource { description?: string; } +/** Generates a `.well-known/x402` discovery document listing payable resources. */ export function createDiscoveryDocument( name: string, payTo: string, diff --git a/tsconfig.json b/tsconfig.json index 800c65c..c934861 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,6 @@ "sourceMap": true }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "examples"] + "exclude": ["node_modules", "dist", "examples", "src/**/*.test.ts"] } From 05e6b11a679edf92f0b05e47d5eecbf7c8b48280 Mon Sep 17 00:00:00 2001 From: KAMIYO Date: Fri, 6 Feb 2026 15:59:23 +0100 Subject: [PATCH 7/7] Default facilitatorUrl to x402.kamiyo.ai Make facilitatorUrl optional in X402MiddlewareConfig with a built-in default so SDK consumers don't need to specify it explicitly. --- src/x402.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/x402.ts b/src/x402.ts index 6c157c8..5446115 100644 --- a/src/x402.ts +++ b/src/x402.ts @@ -5,6 +5,7 @@ import { NetworkError, X402InvalidSchemeError, X402HeaderTooLargeError, X402Faci const DEFAULT_TIMEOUT_MS = 15_000; const MAX_PAYMENT_HEADER_BYTES = 16_384; const PAYMENT_HEADER_NAME = 'X-Payment'; +const DEFAULT_FACILITATOR_URL = 'https://x402.kamiyo.ai'; /** Describes a single payment option a server will accept for a 402-protected resource. */ export interface X402PaymentRequirement { @@ -79,7 +80,7 @@ export interface X402MiddlewareConfig { asset?: TokenSymbol; description?: string; maxTimeoutSeconds?: number; - facilitatorUrl: string; + facilitatorUrl?: string; apiKey: string; additionalSchemes?: X402PaymentRequirement[]; onPayment?: (info: { payer: string; amount: number; signature: string; resource: string }) => void; @@ -368,7 +369,7 @@ export function createPaymentRequired(resource: string, config: X402MiddlewareCo x402Version: 2, accepts, error: 'Payment Required', - facilitator: config.facilitatorUrl, + facilitator: config.facilitatorUrl || DEFAULT_FACILITATOR_URL, resource: { url: resource, description: config.description, mimeType: 'application/json' }, }; } @@ -509,7 +510,9 @@ export function x402Paywall(config: X402MiddlewareConfig) { return res.status(402).json(body); } - const verifyResult = await verifyPayment(paymentHeader, requirement, config.facilitatorUrl, config.apiKey); + const facilitator = config.facilitatorUrl || DEFAULT_FACILITATOR_URL; + + const verifyResult = await verifyPayment(paymentHeader, requirement, facilitator, config.apiKey); if (!verifyResult.valid) { const body = createPaymentRequired(resource, config); @@ -520,7 +523,7 @@ export function x402Paywall(config: X402MiddlewareConfig) { return res.status(402).json(body); } - const settleResult = await settlePayment(paymentHeader, requirement, config.facilitatorUrl, config.apiKey); + const settleResult = await settlePayment(paymentHeader, requirement, facilitator, config.apiKey); if (!settleResult.success) { const body = createPaymentRequired(resource, config); (body as any).settleError = settleResult.error; @@ -574,7 +577,7 @@ export function createDiscoveryDocument( payTo, schemes: ['shadowwire'], networks: ['solana:mainnet'], - facilitator: options?.facilitatorUrl, + facilitator: options?.facilitatorUrl || DEFAULT_FACILITATOR_URL, resources: resources.map((r) => ({ path: r.path, method: r.method,