@mixrpay/merchant-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,328 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/middleware/fastify.ts
21
+ var fastify_exports = {};
22
+ __export(fastify_exports, {
23
+ x402: () => x402,
24
+ x402Plugin: () => x402Plugin
25
+ });
26
+ module.exports = __toCommonJS(fastify_exports);
27
+
28
+ // src/verify.ts
29
+ var import_viem = require("viem");
30
+
31
+ // src/utils.ts
32
+ var USDC_CONTRACTS = {
33
+ 8453: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
34
+ // Base Mainnet
35
+ 84532: "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
36
+ // Base Sepolia
37
+ };
38
+ var DEFAULT_FACILITATOR = "https://x402.org/facilitator";
39
+ function getUSDCDomain(chainId) {
40
+ const verifyingContract = USDC_CONTRACTS[chainId];
41
+ if (!verifyingContract) {
42
+ throw new Error(`Unsupported chain ID: ${chainId}. Supported: ${Object.keys(USDC_CONTRACTS).join(", ")}`);
43
+ }
44
+ return {
45
+ name: "USD Coin",
46
+ version: "2",
47
+ chainId,
48
+ verifyingContract
49
+ };
50
+ }
51
+ var TRANSFER_WITH_AUTHORIZATION_TYPES = {
52
+ TransferWithAuthorization: [
53
+ { name: "from", type: "address" },
54
+ { name: "to", type: "address" },
55
+ { name: "value", type: "uint256" },
56
+ { name: "validAfter", type: "uint256" },
57
+ { name: "validBefore", type: "uint256" },
58
+ { name: "nonce", type: "bytes32" }
59
+ ]
60
+ };
61
+ function usdToMinor(usd) {
62
+ return BigInt(Math.round(usd * 1e6));
63
+ }
64
+ function minorToUsd(minor) {
65
+ return Number(minor) / 1e6;
66
+ }
67
+ function generateNonce() {
68
+ const bytes = new Uint8Array(32);
69
+ crypto.getRandomValues(bytes);
70
+ return `0x${Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
71
+ }
72
+ function base64Decode(str) {
73
+ if (typeof Buffer !== "undefined") {
74
+ return Buffer.from(str, "base64").toString("utf-8");
75
+ }
76
+ return atob(str);
77
+ }
78
+
79
+ // src/verify.ts
80
+ async function verifyX402Payment(paymentHeader, options) {
81
+ try {
82
+ let decoded;
83
+ try {
84
+ const jsonStr = base64Decode(paymentHeader);
85
+ decoded = JSON.parse(jsonStr);
86
+ } catch (e) {
87
+ return { valid: false, error: "Invalid payment header encoding" };
88
+ }
89
+ if (!decoded.payload?.authorization || !decoded.payload?.signature) {
90
+ return { valid: false, error: "Missing authorization or signature in payment" };
91
+ }
92
+ const { authorization, signature } = decoded.payload;
93
+ const chainId = options.chainId || 8453;
94
+ const message = {
95
+ from: authorization.from,
96
+ to: authorization.to,
97
+ value: BigInt(authorization.value),
98
+ validAfter: BigInt(authorization.validAfter),
99
+ validBefore: BigInt(authorization.validBefore),
100
+ nonce: authorization.nonce
101
+ };
102
+ const domain = getUSDCDomain(chainId);
103
+ let signerAddress;
104
+ try {
105
+ signerAddress = await (0, import_viem.recoverTypedDataAddress)({
106
+ domain,
107
+ types: TRANSFER_WITH_AUTHORIZATION_TYPES,
108
+ primaryType: "TransferWithAuthorization",
109
+ message,
110
+ signature
111
+ });
112
+ } catch (e) {
113
+ return { valid: false, error: "Failed to recover signer from signature" };
114
+ }
115
+ if (signerAddress.toLowerCase() !== authorization.from.toLowerCase()) {
116
+ return {
117
+ valid: false,
118
+ error: `Signature mismatch: expected ${authorization.from}, got ${signerAddress}`
119
+ };
120
+ }
121
+ const paymentAmount = BigInt(authorization.value);
122
+ if (paymentAmount < options.expectedAmount) {
123
+ return {
124
+ valid: false,
125
+ error: `Insufficient payment: expected ${options.expectedAmount}, got ${paymentAmount}`
126
+ };
127
+ }
128
+ if (authorization.to.toLowerCase() !== options.expectedRecipient.toLowerCase()) {
129
+ return {
130
+ valid: false,
131
+ error: `Wrong recipient: expected ${options.expectedRecipient}, got ${authorization.to}`
132
+ };
133
+ }
134
+ const now = Math.floor(Date.now() / 1e3);
135
+ if (Number(authorization.validBefore) < now) {
136
+ return { valid: false, error: "Payment authorization has expired" };
137
+ }
138
+ if (Number(authorization.validAfter) > now) {
139
+ return { valid: false, error: "Payment authorization is not yet valid" };
140
+ }
141
+ let txHash;
142
+ let settledAt;
143
+ if (!options.skipSettlement) {
144
+ const facilitatorUrl = options.facilitator || DEFAULT_FACILITATOR;
145
+ try {
146
+ const settlementResponse = await fetch(`${facilitatorUrl}/settle`, {
147
+ method: "POST",
148
+ headers: { "Content-Type": "application/json" },
149
+ body: JSON.stringify({
150
+ authorization,
151
+ signature,
152
+ chainId
153
+ })
154
+ });
155
+ if (!settlementResponse.ok) {
156
+ const errorBody = await settlementResponse.text();
157
+ let errorMessage = "Settlement failed";
158
+ try {
159
+ const errorJson = JSON.parse(errorBody);
160
+ errorMessage = errorJson.message || errorJson.error || errorMessage;
161
+ } catch {
162
+ errorMessage = errorBody || errorMessage;
163
+ }
164
+ return { valid: false, error: `Settlement failed: ${errorMessage}` };
165
+ }
166
+ const settlement = await settlementResponse.json();
167
+ txHash = settlement.txHash || settlement.tx_hash;
168
+ settledAt = /* @__PURE__ */ new Date();
169
+ } catch (e) {
170
+ return {
171
+ valid: false,
172
+ error: `Settlement request failed: ${e.message}`
173
+ };
174
+ }
175
+ }
176
+ return {
177
+ valid: true,
178
+ payer: authorization.from,
179
+ amount: minorToUsd(paymentAmount),
180
+ amountMinor: paymentAmount,
181
+ txHash,
182
+ settledAt: settledAt || /* @__PURE__ */ new Date(),
183
+ nonce: authorization.nonce
184
+ };
185
+ } catch (error) {
186
+ return {
187
+ valid: false,
188
+ error: `Verification error: ${error.message}`
189
+ };
190
+ }
191
+ }
192
+
193
+ // src/receipt-fetcher.ts
194
+ var DEFAULT_MIXRPAY_API_URL = process.env.MIXRPAY_BASE_URL || "http://localhost:3000";
195
+ async function fetchPaymentReceipt(params) {
196
+ const apiUrl = params.apiUrl || DEFAULT_MIXRPAY_API_URL;
197
+ const endpoint = `${apiUrl}/api/v1/receipts`;
198
+ try {
199
+ const response = await fetch(endpoint, {
200
+ method: "POST",
201
+ headers: {
202
+ "Content-Type": "application/json"
203
+ },
204
+ body: JSON.stringify({
205
+ tx_hash: params.txHash,
206
+ payer: params.payer,
207
+ recipient: params.recipient,
208
+ amount: params.amount.toString(),
209
+ chain_id: params.chainId
210
+ })
211
+ });
212
+ if (!response.ok) {
213
+ console.warn(`[x402] Receipt fetch failed: ${response.status} ${response.statusText}`);
214
+ return null;
215
+ }
216
+ const data = await response.json();
217
+ return data.receipt || null;
218
+ } catch (error) {
219
+ console.warn("[x402] Receipt fetch error:", error.message);
220
+ return null;
221
+ }
222
+ }
223
+
224
+ // src/middleware/fastify.ts
225
+ var x402Plugin = (fastify, options, done) => {
226
+ fastify.decorateRequest("x402Payment", null);
227
+ fastify.decorate("x402Defaults", {
228
+ recipient: options.recipient || process.env.MIXRPAY_MERCHANT_ADDRESS,
229
+ chainId: options.chainId || 8453,
230
+ facilitator: options.facilitator || DEFAULT_FACILITATOR
231
+ });
232
+ done();
233
+ };
234
+ function x402(options) {
235
+ return async (request, reply) => {
236
+ const context = {
237
+ path: request.url,
238
+ method: request.method,
239
+ headers: request.headers,
240
+ body: request.body
241
+ };
242
+ if (options.skip) {
243
+ const shouldSkip = await options.skip(context);
244
+ if (shouldSkip) {
245
+ return;
246
+ }
247
+ }
248
+ const defaults = request.server.x402Defaults || {};
249
+ const recipient = options.recipient || defaults.recipient || process.env.MIXRPAY_MERCHANT_ADDRESS;
250
+ if (!recipient) {
251
+ request.log.error("[x402] No recipient address configured");
252
+ return reply.status(500).send({
253
+ error: "Payment configuration error",
254
+ message: "Merchant wallet address not configured"
255
+ });
256
+ }
257
+ const price = options.getPrice ? await options.getPrice(context) : options.price;
258
+ const priceMinor = usdToMinor(price);
259
+ const chainId = options.chainId || defaults.chainId || 8453;
260
+ const facilitator = options.facilitator || defaults.facilitator || DEFAULT_FACILITATOR;
261
+ const paymentHeader = request.headers["x-payment"];
262
+ if (!paymentHeader) {
263
+ const nonce = generateNonce();
264
+ const expiresAt = Math.floor(Date.now() / 1e3) + 300;
265
+ const paymentRequired = {
266
+ recipient,
267
+ amount: priceMinor.toString(),
268
+ currency: "USDC",
269
+ chainId,
270
+ facilitator,
271
+ nonce,
272
+ expiresAt,
273
+ description: options.description
274
+ };
275
+ reply.header("X-Payment-Required", JSON.stringify(paymentRequired));
276
+ reply.header("WWW-Authenticate", `X-402 ${Buffer.from(JSON.stringify(paymentRequired)).toString("base64")}`);
277
+ return reply.status(402).send({
278
+ error: "Payment required",
279
+ payment: paymentRequired
280
+ });
281
+ }
282
+ const result = await verifyX402Payment(paymentHeader, {
283
+ expectedAmount: priceMinor,
284
+ expectedRecipient: recipient,
285
+ chainId,
286
+ facilitator,
287
+ skipSettlement: options.testMode
288
+ });
289
+ if (!result.valid) {
290
+ return reply.status(402).send({
291
+ error: "Invalid payment",
292
+ reason: result.error
293
+ });
294
+ }
295
+ request.x402Payment = result;
296
+ if (result.txHash) {
297
+ reply.header("X-Payment-TxHash", result.txHash);
298
+ }
299
+ const receiptMode = options.receiptMode || "webhook";
300
+ if ((receiptMode === "jwt" || receiptMode === "both") && result.txHash && result.payer) {
301
+ try {
302
+ const receipt = await fetchPaymentReceipt({
303
+ txHash: result.txHash,
304
+ payer: result.payer,
305
+ recipient,
306
+ amount: priceMinor,
307
+ chainId,
308
+ apiUrl: options.mixrpayApiUrl
309
+ });
310
+ if (receipt) {
311
+ result.receipt = receipt;
312
+ reply.header("X-Payment-Receipt", receipt);
313
+ }
314
+ } catch (receiptError) {
315
+ request.log.warn({ err: receiptError }, "[x402] Failed to fetch JWT receipt");
316
+ }
317
+ }
318
+ if (options.onPayment) {
319
+ await options.onPayment(result);
320
+ }
321
+ };
322
+ }
323
+ // Annotate the CommonJS export names for ESM import in node:
324
+ 0 && (module.exports = {
325
+ x402,
326
+ x402Plugin
327
+ });
328
+ //# sourceMappingURL=fastify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/middleware/fastify.ts","../../src/verify.ts","../../src/utils.ts","../../src/receipt-fetcher.ts"],"sourcesContent":["/**\n * MixrPay Merchant SDK - Fastify Plugin\n * \n * Add x402 payment requirement to Fastify routes.\n * \n * @example\n * ```typescript\n * import Fastify from 'fastify';\n * import { x402Plugin, x402 } from '@mixrpay/merchant-sdk/fastify';\n * \n * const app = Fastify();\n * \n * // Register the plugin\n * app.register(x402Plugin, { \n * recipient: '0x...', // or use MIXRPAY_MERCHANT_ADDRESS env var\n * });\n * \n * // Use on routes\n * app.post('/api/query', { preHandler: x402({ price: 0.05 }) }, async (req, reply) => {\n * return { result: 'success', payer: req.x402Payment?.payer };\n * });\n * \n * // With JWT receipts\n * app.post('/api/premium', { preHandler: x402({ price: 0.10, receiptMode: 'jwt' }) }, handler);\n * ```\n */\n\nimport type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginCallback } from 'fastify';\nimport { verifyX402Payment } from '../verify';\nimport { usdToMinor, generateNonce, DEFAULT_FACILITATOR } from '../utils';\nimport { fetchPaymentReceipt } from '../receipt-fetcher';\nimport type { X402Options, X402PaymentResult, X402PaymentRequired, PriceContext } from '../types';\n\n// Extend Fastify types\ndeclare module 'fastify' {\n interface FastifyRequest {\n x402Payment?: X402PaymentResult;\n }\n}\n\nexport interface X402PluginOptions {\n /** Default recipient address (can be overridden per route) */\n recipient?: string;\n /** Default chain ID */\n chainId?: number;\n /** Default facilitator URL */\n facilitator?: string;\n}\n\n/**\n * Fastify plugin that adds x402 payment support.\n */\nexport const x402Plugin: FastifyPluginCallback<X402PluginOptions> = (\n fastify: FastifyInstance,\n options: X402PluginOptions,\n done: (err?: Error) => void\n) => {\n // Decorate request with x402Payment property\n fastify.decorateRequest('x402Payment', null);\n\n // Store default options\n fastify.decorate('x402Defaults', {\n recipient: options.recipient || process.env.MIXRPAY_MERCHANT_ADDRESS,\n chainId: options.chainId || 8453,\n facilitator: options.facilitator || DEFAULT_FACILITATOR,\n });\n\n done();\n};\n\n/**\n * Create a preHandler hook that requires x402 payment.\n * \n * @param options - x402 configuration options\n * @returns Fastify preHandler function\n */\nexport function x402(options: X402Options) {\n return async (request: FastifyRequest, reply: FastifyReply) => {\n // Build context for callbacks\n const context: PriceContext = {\n path: request.url,\n method: request.method,\n headers: request.headers as Record<string, string | string[] | undefined>,\n body: request.body,\n };\n\n // Check if we should skip payment\n if (options.skip) {\n const shouldSkip = await options.skip(context);\n if (shouldSkip) {\n return;\n }\n }\n\n // Get defaults from plugin or options\n const defaults = (request.server as unknown as { x402Defaults?: X402PluginOptions }).x402Defaults || {};\n const recipient = options.recipient || defaults.recipient || process.env.MIXRPAY_MERCHANT_ADDRESS;\n \n if (!recipient) {\n request.log.error('[x402] No recipient address configured');\n return reply.status(500).send({\n error: 'Payment configuration error',\n message: 'Merchant wallet address not configured',\n });\n }\n\n // Determine price\n const price = options.getPrice \n ? await options.getPrice(context) \n : options.price;\n \n const priceMinor = usdToMinor(price);\n const chainId = options.chainId || defaults.chainId || 8453;\n const facilitator = options.facilitator || defaults.facilitator || DEFAULT_FACILITATOR;\n\n // Check for X-PAYMENT header\n const paymentHeader = request.headers['x-payment'] as string | undefined;\n\n if (!paymentHeader) {\n // Return 402 with payment requirements\n const nonce = generateNonce();\n const expiresAt = Math.floor(Date.now() / 1000) + 300;\n\n const paymentRequired: X402PaymentRequired = {\n recipient,\n amount: priceMinor.toString(),\n currency: 'USDC',\n chainId,\n facilitator,\n nonce,\n expiresAt,\n description: options.description,\n };\n\n reply.header('X-Payment-Required', JSON.stringify(paymentRequired));\n reply.header('WWW-Authenticate', `X-402 ${Buffer.from(JSON.stringify(paymentRequired)).toString('base64')}`);\n\n return reply.status(402).send({\n error: 'Payment required',\n payment: paymentRequired,\n });\n }\n\n // Verify the payment\n const result = await verifyX402Payment(paymentHeader, {\n expectedAmount: priceMinor,\n expectedRecipient: recipient,\n chainId,\n facilitator,\n skipSettlement: options.testMode,\n });\n\n if (!result.valid) {\n return reply.status(402).send({\n error: 'Invalid payment',\n reason: result.error,\n });\n }\n\n // Payment verified! Attach to request\n request.x402Payment = result;\n\n // Set response header with tx hash\n if (result.txHash) {\n reply.header('X-Payment-TxHash', result.txHash);\n }\n\n // Handle receipt mode\n const receiptMode = options.receiptMode || 'webhook';\n \n if ((receiptMode === 'jwt' || receiptMode === 'both') && result.txHash && result.payer) {\n try {\n const receipt = await fetchPaymentReceipt({\n txHash: result.txHash,\n payer: result.payer,\n recipient,\n amount: priceMinor,\n chainId,\n apiUrl: options.mixrpayApiUrl,\n });\n \n if (receipt) {\n result.receipt = receipt;\n reply.header('X-Payment-Receipt', receipt);\n }\n } catch (receiptError) {\n request.log.warn({ err: receiptError }, '[x402] Failed to fetch JWT receipt');\n // Continue without receipt - payment was still successful\n }\n }\n\n // Call onPayment callback\n if (options.onPayment) {\n await options.onPayment(result);\n }\n\n // Continue to handler\n };\n}\n\nexport type { X402Options, X402PaymentResult };\n\n","/**\n * MixrPay Merchant SDK - Payment Verification\n */\n\nimport { recoverTypedDataAddress } from 'viem';\nimport type { \n X402PaymentResult, \n X402PaymentPayload, \n VerifyOptions,\n TransferWithAuthorizationMessage,\n} from './types';\nimport { \n getUSDCDomain, \n TRANSFER_WITH_AUTHORIZATION_TYPES, \n base64Decode,\n minorToUsd,\n DEFAULT_FACILITATOR,\n} from './utils';\n\n// =============================================================================\n// Payment Verification\n// =============================================================================\n\n/**\n * Verify an X-PAYMENT header and optionally settle the payment.\n * \n * @param paymentHeader - The X-PAYMENT header value (base64 encoded JSON)\n * @param options - Verification options\n * @returns Payment verification result\n * \n * @example\n * ```typescript\n * const result = await verifyX402Payment(req.headers['x-payment'], {\n * expectedAmount: 50000n, // 0.05 USDC\n * expectedRecipient: '0x...',\n * });\n * \n * if (result.valid) {\n * console.log(`Payment from ${result.payer}: $${result.amount}`);\n * }\n * ```\n */\nexport async function verifyX402Payment(\n paymentHeader: string,\n options: VerifyOptions\n): Promise<X402PaymentResult> {\n try {\n // 1. Decode the X-PAYMENT header\n let decoded: X402PaymentPayload;\n try {\n const jsonStr = base64Decode(paymentHeader);\n decoded = JSON.parse(jsonStr);\n } catch (e) {\n return { valid: false, error: 'Invalid payment header encoding' };\n }\n\n // 2. Validate payload structure\n if (!decoded.payload?.authorization || !decoded.payload?.signature) {\n return { valid: false, error: 'Missing authorization or signature in payment' };\n }\n\n const { authorization, signature } = decoded.payload;\n const chainId = options.chainId || 8453;\n\n // 3. Build EIP-712 message for verification\n const message: TransferWithAuthorizationMessage = {\n from: authorization.from as `0x${string}`,\n to: authorization.to as `0x${string}`,\n value: BigInt(authorization.value),\n validAfter: BigInt(authorization.validAfter),\n validBefore: BigInt(authorization.validBefore),\n nonce: authorization.nonce as `0x${string}`,\n };\n\n // 4. Recover signer address\n const domain = getUSDCDomain(chainId);\n let signerAddress: string;\n \n try {\n signerAddress = await recoverTypedDataAddress({\n domain,\n types: TRANSFER_WITH_AUTHORIZATION_TYPES,\n primaryType: 'TransferWithAuthorization',\n message,\n signature: signature as `0x${string}`,\n });\n } catch (e) {\n return { valid: false, error: 'Failed to recover signer from signature' };\n }\n\n // 5. Verify signer matches the 'from' address\n if (signerAddress.toLowerCase() !== authorization.from.toLowerCase()) {\n return { \n valid: false, \n error: `Signature mismatch: expected ${authorization.from}, got ${signerAddress}` \n };\n }\n\n // 6. Verify payment amount\n const paymentAmount = BigInt(authorization.value);\n if (paymentAmount < options.expectedAmount) {\n return { \n valid: false, \n error: `Insufficient payment: expected ${options.expectedAmount}, got ${paymentAmount}` \n };\n }\n\n // 7. Verify recipient\n if (authorization.to.toLowerCase() !== options.expectedRecipient.toLowerCase()) {\n return { \n valid: false, \n error: `Wrong recipient: expected ${options.expectedRecipient}, got ${authorization.to}` \n };\n }\n\n // 8. Check expiration\n const now = Math.floor(Date.now() / 1000);\n if (Number(authorization.validBefore) < now) {\n return { valid: false, error: 'Payment authorization has expired' };\n }\n\n // 9. Check validAfter\n if (Number(authorization.validAfter) > now) {\n return { valid: false, error: 'Payment authorization is not yet valid' };\n }\n\n // 10. Submit to facilitator for settlement (unless skipped)\n let txHash: string | undefined;\n let settledAt: Date | undefined;\n\n if (!options.skipSettlement) {\n const facilitatorUrl = options.facilitator || DEFAULT_FACILITATOR;\n \n try {\n const settlementResponse = await fetch(`${facilitatorUrl}/settle`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ \n authorization, \n signature,\n chainId,\n }),\n });\n\n if (!settlementResponse.ok) {\n const errorBody = await settlementResponse.text();\n let errorMessage = 'Settlement failed';\n try {\n const errorJson = JSON.parse(errorBody);\n errorMessage = errorJson.message || errorJson.error || errorMessage;\n } catch {\n errorMessage = errorBody || errorMessage;\n }\n return { valid: false, error: `Settlement failed: ${errorMessage}` };\n }\n\n const settlement = await settlementResponse.json() as { txHash?: string; tx_hash?: string };\n txHash = settlement.txHash || settlement.tx_hash;\n settledAt = new Date();\n } catch (e) {\n return { \n valid: false, \n error: `Settlement request failed: ${(e as Error).message}` \n };\n }\n }\n\n // Success!\n return {\n valid: true,\n payer: authorization.from,\n amount: minorToUsd(paymentAmount),\n amountMinor: paymentAmount,\n txHash,\n settledAt: settledAt || new Date(),\n nonce: authorization.nonce,\n };\n\n } catch (error) {\n return { \n valid: false, \n error: `Verification error: ${(error as Error).message}` \n };\n }\n}\n\n/**\n * Parse and validate an X-PAYMENT header without settlement.\n * Useful for checking if a payment is structurally valid before processing.\n */\nexport async function parseX402Payment(\n paymentHeader: string,\n chainId: number = 8453\n): Promise<{\n valid: boolean;\n error?: string;\n payer?: string;\n recipient?: string;\n amount?: number;\n amountMinor?: bigint;\n expiresAt?: Date;\n}> {\n try {\n const jsonStr = base64Decode(paymentHeader);\n const decoded: X402PaymentPayload = JSON.parse(jsonStr);\n\n if (!decoded.payload?.authorization) {\n return { valid: false, error: 'Missing authorization in payment' };\n }\n\n const { authorization, signature } = decoded.payload;\n\n // Verify signature\n const domain = getUSDCDomain(chainId);\n const message: TransferWithAuthorizationMessage = {\n from: authorization.from as `0x${string}`,\n to: authorization.to as `0x${string}`,\n value: BigInt(authorization.value),\n validAfter: BigInt(authorization.validAfter),\n validBefore: BigInt(authorization.validBefore),\n nonce: authorization.nonce as `0x${string}`,\n };\n\n const signerAddress = await recoverTypedDataAddress({\n domain,\n types: TRANSFER_WITH_AUTHORIZATION_TYPES,\n primaryType: 'TransferWithAuthorization',\n message,\n signature: signature as `0x${string}`,\n });\n\n if (signerAddress.toLowerCase() !== authorization.from.toLowerCase()) {\n return { valid: false, error: 'Signature mismatch' };\n }\n\n const amountMinor = BigInt(authorization.value);\n \n return {\n valid: true,\n payer: authorization.from,\n recipient: authorization.to,\n amount: minorToUsd(amountMinor),\n amountMinor,\n expiresAt: new Date(Number(authorization.validBefore) * 1000),\n };\n } catch (error) {\n return { valid: false, error: `Parse error: ${(error as Error).message}` };\n }\n}\n\n","/**\n * MixrPay Merchant SDK - Utilities\n */\n\nimport type { EIP712Domain } from './types';\n\n// =============================================================================\n// USDC Constants by Chain\n// =============================================================================\n\nexport const USDC_CONTRACTS: Record<number, `0x${string}`> = {\n 8453: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // Base Mainnet\n 84532: '0x036CbD53842c5426634e7929541eC2318f3dCF7e', // Base Sepolia\n};\n\nexport const DEFAULT_FACILITATOR = 'https://x402.org/facilitator';\n\n// =============================================================================\n// EIP-712 Domain\n// =============================================================================\n\nexport function getUSDCDomain(chainId: number): EIP712Domain {\n const verifyingContract = USDC_CONTRACTS[chainId];\n if (!verifyingContract) {\n throw new Error(`Unsupported chain ID: ${chainId}. Supported: ${Object.keys(USDC_CONTRACTS).join(', ')}`);\n }\n\n return {\n name: 'USD Coin',\n version: '2',\n chainId,\n verifyingContract,\n };\n}\n\n// EIP-712 types for TransferWithAuthorization\nexport const TRANSFER_WITH_AUTHORIZATION_TYPES = {\n TransferWithAuthorization: [\n { name: 'from', type: 'address' },\n { name: 'to', type: 'address' },\n { name: 'value', type: 'uint256' },\n { name: 'validAfter', type: 'uint256' },\n { name: 'validBefore', type: 'uint256' },\n { name: 'nonce', type: 'bytes32' },\n ],\n} as const;\n\n// =============================================================================\n// Utility Functions\n// =============================================================================\n\n/**\n * Convert USD dollars to USDC minor units (6 decimals)\n */\nexport function usdToMinor(usd: number): bigint {\n return BigInt(Math.round(usd * 1_000_000));\n}\n\n/**\n * Convert USDC minor units to USD dollars\n */\nexport function minorToUsd(minor: bigint): number {\n return Number(minor) / 1_000_000;\n}\n\n/**\n * Generate a random nonce for x402 payments\n */\nexport function generateNonce(): `0x${string}` {\n const bytes = new Uint8Array(32);\n crypto.getRandomValues(bytes);\n return `0x${Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('')}` as `0x${string}`;\n}\n\n/**\n * Check if an address is valid\n */\nexport function isValidAddress(address: string): address is `0x${string}` {\n return /^0x[a-fA-F0-9]{40}$/.test(address);\n}\n\n/**\n * Normalize an address to lowercase checksum format\n */\nexport function normalizeAddress(address: string): `0x${string}` {\n if (!isValidAddress(address)) {\n throw new Error(`Invalid address: ${address}`);\n }\n return address.toLowerCase() as `0x${string}`;\n}\n\n/**\n * Safe base64 decode that works in both Node.js and browsers\n */\nexport function base64Decode(str: string): string {\n if (typeof Buffer !== 'undefined') {\n return Buffer.from(str, 'base64').toString('utf-8');\n }\n return atob(str);\n}\n\n/**\n * Safe base64 encode that works in both Node.js and browsers\n */\nexport function base64Encode(str: string): string {\n if (typeof Buffer !== 'undefined') {\n return Buffer.from(str, 'utf-8').toString('base64');\n }\n return btoa(str);\n}\n\n","/**\n * MixrPay Merchant SDK - Receipt Fetcher\n * \n * Internal utility to fetch JWT receipts from MixrPay API after settlement.\n * Used by x402 middleware to get receipts for the X-Payment-Receipt header.\n */\n\n// =============================================================================\n// Constants\n// =============================================================================\n\nconst DEFAULT_MIXRPAY_API_URL = process.env.MIXRPAY_BASE_URL || 'http://localhost:3000';\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface FetchReceiptParams {\n /** Settlement transaction hash */\n txHash: string;\n /** Payer address */\n payer: string;\n /** Recipient address */\n recipient: string;\n /** Amount in USDC minor units */\n amount: bigint;\n /** Chain ID */\n chainId: number;\n /** Custom MixrPay API URL (optional) */\n apiUrl?: string;\n}\n\ninterface ReceiptResponse {\n receipt: string;\n expiresAt: string;\n}\n\n// =============================================================================\n// Receipt Fetching\n// =============================================================================\n\n/**\n * Fetch a JWT payment receipt from MixrPay API.\n * \n * This is called by the x402 middleware after successful payment verification\n * when receiptMode is 'jwt' or 'both'.\n * \n * @param params - Receipt request parameters\n * @returns JWT receipt string, or null if not available\n */\nexport async function fetchPaymentReceipt(params: FetchReceiptParams): Promise<string | null> {\n const apiUrl = params.apiUrl || DEFAULT_MIXRPAY_API_URL;\n const endpoint = `${apiUrl}/api/v1/receipts`;\n\n try {\n const response = await fetch(endpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n tx_hash: params.txHash,\n payer: params.payer,\n recipient: params.recipient,\n amount: params.amount.toString(),\n chain_id: params.chainId,\n }),\n });\n\n if (!response.ok) {\n // Log but don't throw - receipt is optional\n console.warn(`[x402] Receipt fetch failed: ${response.status} ${response.statusText}`);\n return null;\n }\n\n const data = await response.json() as ReceiptResponse;\n return data.receipt || null;\n } catch (error) {\n // Log but don't throw - payment was successful, receipt is optional\n console.warn('[x402] Receipt fetch error:', (error as Error).message);\n return null;\n }\n}\n\n/**\n * Generate a local receipt for testing purposes.\n * \n * WARNING: This generates an unsigned receipt that will NOT verify.\n * Use only for local development and testing.\n * \n * @param params - Receipt parameters\n * @returns Mock JWT-like string (not cryptographically signed)\n */\nexport function generateMockReceipt(params: FetchReceiptParams): string {\n const header = {\n alg: 'none',\n typ: 'JWT',\n };\n\n const payload = {\n paymentId: `mock_${Date.now()}`,\n amount: params.amount.toString(),\n amountUsd: Number(params.amount) / 1_000_000,\n payer: params.payer,\n recipient: params.recipient,\n chainId: params.chainId,\n txHash: params.txHash,\n settledAt: new Date().toISOString(),\n iat: Math.floor(Date.now() / 1000),\n exp: Math.floor(Date.now() / 1000) + 3600,\n _mock: true,\n };\n\n const base64Header = Buffer.from(JSON.stringify(header)).toString('base64url');\n const base64Payload = Buffer.from(JSON.stringify(payload)).toString('base64url');\n\n // Mock signature (not cryptographically valid)\n return `${base64Header}.${base64Payload}.mock_signature_for_testing_only`;\n}\n\n"],"mappings":"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","names":[]}
@@ -0,0 +1,300 @@
1
+ // src/verify.ts
2
+ import { recoverTypedDataAddress } from "viem";
3
+
4
+ // src/utils.ts
5
+ var USDC_CONTRACTS = {
6
+ 8453: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
7
+ // Base Mainnet
8
+ 84532: "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
9
+ // Base Sepolia
10
+ };
11
+ var DEFAULT_FACILITATOR = "https://x402.org/facilitator";
12
+ function getUSDCDomain(chainId) {
13
+ const verifyingContract = USDC_CONTRACTS[chainId];
14
+ if (!verifyingContract) {
15
+ throw new Error(`Unsupported chain ID: ${chainId}. Supported: ${Object.keys(USDC_CONTRACTS).join(", ")}`);
16
+ }
17
+ return {
18
+ name: "USD Coin",
19
+ version: "2",
20
+ chainId,
21
+ verifyingContract
22
+ };
23
+ }
24
+ var TRANSFER_WITH_AUTHORIZATION_TYPES = {
25
+ TransferWithAuthorization: [
26
+ { name: "from", type: "address" },
27
+ { name: "to", type: "address" },
28
+ { name: "value", type: "uint256" },
29
+ { name: "validAfter", type: "uint256" },
30
+ { name: "validBefore", type: "uint256" },
31
+ { name: "nonce", type: "bytes32" }
32
+ ]
33
+ };
34
+ function usdToMinor(usd) {
35
+ return BigInt(Math.round(usd * 1e6));
36
+ }
37
+ function minorToUsd(minor) {
38
+ return Number(minor) / 1e6;
39
+ }
40
+ function generateNonce() {
41
+ const bytes = new Uint8Array(32);
42
+ crypto.getRandomValues(bytes);
43
+ return `0x${Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
44
+ }
45
+ function base64Decode(str) {
46
+ if (typeof Buffer !== "undefined") {
47
+ return Buffer.from(str, "base64").toString("utf-8");
48
+ }
49
+ return atob(str);
50
+ }
51
+
52
+ // src/verify.ts
53
+ async function verifyX402Payment(paymentHeader, options) {
54
+ try {
55
+ let decoded;
56
+ try {
57
+ const jsonStr = base64Decode(paymentHeader);
58
+ decoded = JSON.parse(jsonStr);
59
+ } catch (e) {
60
+ return { valid: false, error: "Invalid payment header encoding" };
61
+ }
62
+ if (!decoded.payload?.authorization || !decoded.payload?.signature) {
63
+ return { valid: false, error: "Missing authorization or signature in payment" };
64
+ }
65
+ const { authorization, signature } = decoded.payload;
66
+ const chainId = options.chainId || 8453;
67
+ const message = {
68
+ from: authorization.from,
69
+ to: authorization.to,
70
+ value: BigInt(authorization.value),
71
+ validAfter: BigInt(authorization.validAfter),
72
+ validBefore: BigInt(authorization.validBefore),
73
+ nonce: authorization.nonce
74
+ };
75
+ const domain = getUSDCDomain(chainId);
76
+ let signerAddress;
77
+ try {
78
+ signerAddress = await recoverTypedDataAddress({
79
+ domain,
80
+ types: TRANSFER_WITH_AUTHORIZATION_TYPES,
81
+ primaryType: "TransferWithAuthorization",
82
+ message,
83
+ signature
84
+ });
85
+ } catch (e) {
86
+ return { valid: false, error: "Failed to recover signer from signature" };
87
+ }
88
+ if (signerAddress.toLowerCase() !== authorization.from.toLowerCase()) {
89
+ return {
90
+ valid: false,
91
+ error: `Signature mismatch: expected ${authorization.from}, got ${signerAddress}`
92
+ };
93
+ }
94
+ const paymentAmount = BigInt(authorization.value);
95
+ if (paymentAmount < options.expectedAmount) {
96
+ return {
97
+ valid: false,
98
+ error: `Insufficient payment: expected ${options.expectedAmount}, got ${paymentAmount}`
99
+ };
100
+ }
101
+ if (authorization.to.toLowerCase() !== options.expectedRecipient.toLowerCase()) {
102
+ return {
103
+ valid: false,
104
+ error: `Wrong recipient: expected ${options.expectedRecipient}, got ${authorization.to}`
105
+ };
106
+ }
107
+ const now = Math.floor(Date.now() / 1e3);
108
+ if (Number(authorization.validBefore) < now) {
109
+ return { valid: false, error: "Payment authorization has expired" };
110
+ }
111
+ if (Number(authorization.validAfter) > now) {
112
+ return { valid: false, error: "Payment authorization is not yet valid" };
113
+ }
114
+ let txHash;
115
+ let settledAt;
116
+ if (!options.skipSettlement) {
117
+ const facilitatorUrl = options.facilitator || DEFAULT_FACILITATOR;
118
+ try {
119
+ const settlementResponse = await fetch(`${facilitatorUrl}/settle`, {
120
+ method: "POST",
121
+ headers: { "Content-Type": "application/json" },
122
+ body: JSON.stringify({
123
+ authorization,
124
+ signature,
125
+ chainId
126
+ })
127
+ });
128
+ if (!settlementResponse.ok) {
129
+ const errorBody = await settlementResponse.text();
130
+ let errorMessage = "Settlement failed";
131
+ try {
132
+ const errorJson = JSON.parse(errorBody);
133
+ errorMessage = errorJson.message || errorJson.error || errorMessage;
134
+ } catch {
135
+ errorMessage = errorBody || errorMessage;
136
+ }
137
+ return { valid: false, error: `Settlement failed: ${errorMessage}` };
138
+ }
139
+ const settlement = await settlementResponse.json();
140
+ txHash = settlement.txHash || settlement.tx_hash;
141
+ settledAt = /* @__PURE__ */ new Date();
142
+ } catch (e) {
143
+ return {
144
+ valid: false,
145
+ error: `Settlement request failed: ${e.message}`
146
+ };
147
+ }
148
+ }
149
+ return {
150
+ valid: true,
151
+ payer: authorization.from,
152
+ amount: minorToUsd(paymentAmount),
153
+ amountMinor: paymentAmount,
154
+ txHash,
155
+ settledAt: settledAt || /* @__PURE__ */ new Date(),
156
+ nonce: authorization.nonce
157
+ };
158
+ } catch (error) {
159
+ return {
160
+ valid: false,
161
+ error: `Verification error: ${error.message}`
162
+ };
163
+ }
164
+ }
165
+
166
+ // src/receipt-fetcher.ts
167
+ var DEFAULT_MIXRPAY_API_URL = process.env.MIXRPAY_BASE_URL || "http://localhost:3000";
168
+ async function fetchPaymentReceipt(params) {
169
+ const apiUrl = params.apiUrl || DEFAULT_MIXRPAY_API_URL;
170
+ const endpoint = `${apiUrl}/api/v1/receipts`;
171
+ try {
172
+ const response = await fetch(endpoint, {
173
+ method: "POST",
174
+ headers: {
175
+ "Content-Type": "application/json"
176
+ },
177
+ body: JSON.stringify({
178
+ tx_hash: params.txHash,
179
+ payer: params.payer,
180
+ recipient: params.recipient,
181
+ amount: params.amount.toString(),
182
+ chain_id: params.chainId
183
+ })
184
+ });
185
+ if (!response.ok) {
186
+ console.warn(`[x402] Receipt fetch failed: ${response.status} ${response.statusText}`);
187
+ return null;
188
+ }
189
+ const data = await response.json();
190
+ return data.receipt || null;
191
+ } catch (error) {
192
+ console.warn("[x402] Receipt fetch error:", error.message);
193
+ return null;
194
+ }
195
+ }
196
+
197
+ // src/middleware/fastify.ts
198
+ var x402Plugin = (fastify, options, done) => {
199
+ fastify.decorateRequest("x402Payment", null);
200
+ fastify.decorate("x402Defaults", {
201
+ recipient: options.recipient || process.env.MIXRPAY_MERCHANT_ADDRESS,
202
+ chainId: options.chainId || 8453,
203
+ facilitator: options.facilitator || DEFAULT_FACILITATOR
204
+ });
205
+ done();
206
+ };
207
+ function x402(options) {
208
+ return async (request, reply) => {
209
+ const context = {
210
+ path: request.url,
211
+ method: request.method,
212
+ headers: request.headers,
213
+ body: request.body
214
+ };
215
+ if (options.skip) {
216
+ const shouldSkip = await options.skip(context);
217
+ if (shouldSkip) {
218
+ return;
219
+ }
220
+ }
221
+ const defaults = request.server.x402Defaults || {};
222
+ const recipient = options.recipient || defaults.recipient || process.env.MIXRPAY_MERCHANT_ADDRESS;
223
+ if (!recipient) {
224
+ request.log.error("[x402] No recipient address configured");
225
+ return reply.status(500).send({
226
+ error: "Payment configuration error",
227
+ message: "Merchant wallet address not configured"
228
+ });
229
+ }
230
+ const price = options.getPrice ? await options.getPrice(context) : options.price;
231
+ const priceMinor = usdToMinor(price);
232
+ const chainId = options.chainId || defaults.chainId || 8453;
233
+ const facilitator = options.facilitator || defaults.facilitator || DEFAULT_FACILITATOR;
234
+ const paymentHeader = request.headers["x-payment"];
235
+ if (!paymentHeader) {
236
+ const nonce = generateNonce();
237
+ const expiresAt = Math.floor(Date.now() / 1e3) + 300;
238
+ const paymentRequired = {
239
+ recipient,
240
+ amount: priceMinor.toString(),
241
+ currency: "USDC",
242
+ chainId,
243
+ facilitator,
244
+ nonce,
245
+ expiresAt,
246
+ description: options.description
247
+ };
248
+ reply.header("X-Payment-Required", JSON.stringify(paymentRequired));
249
+ reply.header("WWW-Authenticate", `X-402 ${Buffer.from(JSON.stringify(paymentRequired)).toString("base64")}`);
250
+ return reply.status(402).send({
251
+ error: "Payment required",
252
+ payment: paymentRequired
253
+ });
254
+ }
255
+ const result = await verifyX402Payment(paymentHeader, {
256
+ expectedAmount: priceMinor,
257
+ expectedRecipient: recipient,
258
+ chainId,
259
+ facilitator,
260
+ skipSettlement: options.testMode
261
+ });
262
+ if (!result.valid) {
263
+ return reply.status(402).send({
264
+ error: "Invalid payment",
265
+ reason: result.error
266
+ });
267
+ }
268
+ request.x402Payment = result;
269
+ if (result.txHash) {
270
+ reply.header("X-Payment-TxHash", result.txHash);
271
+ }
272
+ const receiptMode = options.receiptMode || "webhook";
273
+ if ((receiptMode === "jwt" || receiptMode === "both") && result.txHash && result.payer) {
274
+ try {
275
+ const receipt = await fetchPaymentReceipt({
276
+ txHash: result.txHash,
277
+ payer: result.payer,
278
+ recipient,
279
+ amount: priceMinor,
280
+ chainId,
281
+ apiUrl: options.mixrpayApiUrl
282
+ });
283
+ if (receipt) {
284
+ result.receipt = receipt;
285
+ reply.header("X-Payment-Receipt", receipt);
286
+ }
287
+ } catch (receiptError) {
288
+ request.log.warn({ err: receiptError }, "[x402] Failed to fetch JWT receipt");
289
+ }
290
+ }
291
+ if (options.onPayment) {
292
+ await options.onPayment(result);
293
+ }
294
+ };
295
+ }
296
+ export {
297
+ x402,
298
+ x402Plugin
299
+ };
300
+ //# sourceMappingURL=fastify.mjs.map