@happyvertical/payments 0.80.0 → 0.80.1
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.
- package/dist/adapters/base-usdc.js +720 -1101
- package/dist/adapters/base-usdc.js.map +1 -1
- package/dist/adapters/btc.js +465 -790
- package/dist/adapters/btc.js.map +1 -1
- package/dist/adapters/stripe.js +556 -959
- package/dist/adapters/stripe.js.map +1 -1
- package/dist/chunks/errors-feZrwnTy.js +39 -0
- package/dist/chunks/errors-feZrwnTy.js.map +1 -0
- package/dist/chunks/shared-DVl6UzD9.js +260 -0
- package/dist/chunks/shared-DVl6UzD9.js.map +1 -0
- package/dist/index.js +33 -47
- package/dist/index.js.map +1 -1
- package/dist/testing/conformance.js +93 -109
- package/dist/testing/conformance.js.map +1 -1
- package/package.json +5 -5
- package/dist/chunks/errors-BgFC46qQ.js +0 -45
- package/dist/chunks/errors-BgFC46qQ.js.map +0 -1
- package/dist/chunks/shared-DGHSqDQT.js +0 -392
- package/dist/chunks/shared-DGHSqDQT.js.map +0 -1
|
@@ -1,1182 +1,801 @@
|
|
|
1
|
+
import { a as PaymentVerificationError, r as PaymentProviderError, t as PaymentConfigurationError } from "../chunks/errors-feZrwnTy.js";
|
|
2
|
+
import { _ as normalizeUrlString, b as rememberPaymentOption, d as normalizeFutureDate, f as normalizeMaxStoredPaymentOptions, h as normalizePositiveMinorUnitAmount, i as decimalToMinorUnitAmount, l as normalizeCurrency, m as normalizeNonEmptyString, n as currencyMinorUnitDecimals, o as getFetch, p as normalizeMinorUnitAmount, r as decimalToAtomicUnits, s as minorUnitsToDecimal, u as normalizeDate, v as pollPaymentStatus, y as readJsonResponse } from "../chunks/shared-DVl6UzD9.js";
|
|
1
3
|
import { createHash } from "node:crypto";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
}
|
|
463
|
-
this.seenX402TransactionIds.add(transactionId);
|
|
464
|
-
let keepReplayMarker = false;
|
|
465
|
-
try {
|
|
466
|
-
const receipt = await this.rpc(
|
|
467
|
-
"eth_getTransactionReceipt",
|
|
468
|
-
[transactionId]
|
|
469
|
-
);
|
|
470
|
-
const raw = { ...extraRaw, payload, receipt };
|
|
471
|
-
if (!receipt) {
|
|
472
|
-
return {
|
|
473
|
-
valid: false,
|
|
474
|
-
backendId: this.capabilities.id,
|
|
475
|
-
quoteId,
|
|
476
|
-
payTo: input.payTo,
|
|
477
|
-
transactionId,
|
|
478
|
-
reason: "x402 transaction receipt is not available yet.",
|
|
479
|
-
raw
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
if (receipt.status !== "0x1") {
|
|
483
|
-
return {
|
|
484
|
-
valid: false,
|
|
485
|
-
backendId: this.capabilities.id,
|
|
486
|
-
quoteId,
|
|
487
|
-
payTo: input.payTo,
|
|
488
|
-
transactionId,
|
|
489
|
-
reason: "x402 transaction failed onchain.",
|
|
490
|
-
raw
|
|
491
|
-
};
|
|
492
|
-
}
|
|
493
|
-
const blockNumber = parseHexQuantity(receipt.blockNumber ?? "0x0");
|
|
494
|
-
const currentBlock = blockNumber > 0n ? await this.getCurrentBlockNumber() : 0n;
|
|
495
|
-
const confirmations = calculateConfirmations(currentBlock, blockNumber);
|
|
496
|
-
if (confirmations < this.confirmations) {
|
|
497
|
-
return {
|
|
498
|
-
valid: false,
|
|
499
|
-
backendId: this.capabilities.id,
|
|
500
|
-
quoteId,
|
|
501
|
-
payTo: input.payTo,
|
|
502
|
-
transactionId,
|
|
503
|
-
reason: `x402 transaction has ${confirmations} confirmations; ${this.confirmations} required.`,
|
|
504
|
-
raw
|
|
505
|
-
};
|
|
506
|
-
}
|
|
507
|
-
const receiptLogs = Array.isArray(receipt.logs) ? receipt.logs : [];
|
|
508
|
-
const matchingLog = pickBestTransferLog(
|
|
509
|
-
receiptLogs.filter(
|
|
510
|
-
(log) => isRecord(log) && safeNormalizeAddress(log.address) === this.usdcContractAddress
|
|
511
|
-
),
|
|
512
|
-
expectedAmountAtomic,
|
|
513
|
-
input.payTo,
|
|
514
|
-
"exact"
|
|
515
|
-
);
|
|
516
|
-
if (!matchingLog) {
|
|
517
|
-
return {
|
|
518
|
-
valid: false,
|
|
519
|
-
backendId: this.capabilities.id,
|
|
520
|
-
quoteId,
|
|
521
|
-
payTo: input.payTo,
|
|
522
|
-
transactionId,
|
|
523
|
-
reason: "No exact USDC transfer matching the x402 proof.",
|
|
524
|
-
raw
|
|
525
|
-
};
|
|
526
|
-
}
|
|
527
|
-
keepReplayMarker = true;
|
|
528
|
-
trimSeenX402TransactionIds(this.seenX402TransactionIds);
|
|
529
|
-
return {
|
|
530
|
-
valid: true,
|
|
531
|
-
backendId: this.capabilities.id,
|
|
532
|
-
quoteId,
|
|
533
|
-
payTo: normalizeAddress(input.payTo),
|
|
534
|
-
transactionId,
|
|
535
|
-
payer: parseTransferLog(matchingLog)?.from,
|
|
536
|
-
amount: decimalToMinorUnitAmount(
|
|
537
|
-
parseHexQuantity(matchingLog.data),
|
|
538
|
-
USDC_DECIMALS,
|
|
539
|
-
"Base USDC x402 amount"
|
|
540
|
-
),
|
|
541
|
-
currency: "USDC",
|
|
542
|
-
network: input.network ?? BASE_MAINNET_CAIP2,
|
|
543
|
-
raw
|
|
544
|
-
};
|
|
545
|
-
} finally {
|
|
546
|
-
if (!keepReplayMarker) {
|
|
547
|
-
this.seenX402TransactionIds.delete(transactionId);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
async verifyWithFacilitator(input, payload) {
|
|
552
|
-
const quoteId = normalizeNonEmptyString(input.quoteId, "Base USDC quoteId");
|
|
553
|
-
if (!this.x402FacilitatorUrl) {
|
|
554
|
-
throw new PaymentVerificationError("x402 facilitator URL is missing.");
|
|
555
|
-
}
|
|
556
|
-
const maxAmountRequired = amountToUsdcAtomicUnits(
|
|
557
|
-
normalizePositiveMinorUnitAmount(input.amount, "Base USDC x402 amount"),
|
|
558
|
-
normalizeCurrency(input.currency, "Base USDC x402"),
|
|
559
|
-
"Base USDC x402 amount"
|
|
560
|
-
);
|
|
561
|
-
const response = await this.fetch(`${this.x402FacilitatorUrl}/verify`, {
|
|
562
|
-
method: "POST",
|
|
563
|
-
headers: { "Content-Type": "application/json" },
|
|
564
|
-
body: JSON.stringify({
|
|
565
|
-
paymentPayload: payload,
|
|
566
|
-
paymentRequirements: {
|
|
567
|
-
scheme: input.scheme ?? "exact",
|
|
568
|
-
network: input.network ?? BASE_MAINNET_CAIP2,
|
|
569
|
-
asset: this.usdcContractAddress,
|
|
570
|
-
payTo: normalizeAddress(input.payTo),
|
|
571
|
-
maxAmountRequired,
|
|
572
|
-
resource: input.resource,
|
|
573
|
-
method: input.method
|
|
574
|
-
}
|
|
575
|
-
})
|
|
576
|
-
});
|
|
577
|
-
const json = await readJsonResponse(
|
|
578
|
-
response,
|
|
579
|
-
"x402 facilitator verify"
|
|
580
|
-
);
|
|
581
|
-
const valid = readBoolean(json, "valid") ?? readBoolean(json, "isValid");
|
|
582
|
-
const transactionId = findNestedString(json, [
|
|
583
|
-
"transactionHash",
|
|
584
|
-
"txHash"
|
|
585
|
-
])?.trim();
|
|
586
|
-
if (valid !== true) {
|
|
587
|
-
return {
|
|
588
|
-
valid: false,
|
|
589
|
-
backendId: this.capabilities.id,
|
|
590
|
-
quoteId,
|
|
591
|
-
payTo: normalizeAddress(input.payTo),
|
|
592
|
-
transactionId,
|
|
593
|
-
network: input.network ?? BASE_MAINNET_CAIP2,
|
|
594
|
-
reason: String(json.reason ?? "facilitator rejected"),
|
|
595
|
-
raw: json
|
|
596
|
-
};
|
|
597
|
-
}
|
|
598
|
-
if (!transactionId) {
|
|
599
|
-
return {
|
|
600
|
-
valid: false,
|
|
601
|
-
backendId: this.capabilities.id,
|
|
602
|
-
quoteId,
|
|
603
|
-
payTo: normalizeAddress(input.payTo),
|
|
604
|
-
network: input.network ?? BASE_MAINNET_CAIP2,
|
|
605
|
-
reason: "x402 facilitator did not return a transaction hash.",
|
|
606
|
-
raw: json
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
if (!isEvmTransactionHash(transactionId)) {
|
|
610
|
-
return {
|
|
611
|
-
valid: false,
|
|
612
|
-
backendId: this.capabilities.id,
|
|
613
|
-
quoteId,
|
|
614
|
-
payTo: normalizeAddress(input.payTo),
|
|
615
|
-
transactionId,
|
|
616
|
-
network: input.network ?? BASE_MAINNET_CAIP2,
|
|
617
|
-
reason: "x402 facilitator transaction hash was invalid.",
|
|
618
|
-
raw: json
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
const requirementError = validateX402PayloadRequirements(payload, input);
|
|
622
|
-
if (requirementError) {
|
|
623
|
-
return {
|
|
624
|
-
valid: false,
|
|
625
|
-
backendId: this.capabilities.id,
|
|
626
|
-
quoteId,
|
|
627
|
-
payTo: normalizeAddress(input.payTo),
|
|
628
|
-
transactionId,
|
|
629
|
-
network: input.network ?? BASE_MAINNET_CAIP2,
|
|
630
|
-
reason: requirementError,
|
|
631
|
-
raw: json
|
|
632
|
-
};
|
|
633
|
-
}
|
|
634
|
-
return this.verifyX402Receipt(
|
|
635
|
-
input,
|
|
636
|
-
payload,
|
|
637
|
-
quoteId,
|
|
638
|
-
transactionId,
|
|
639
|
-
maxAmountRequired,
|
|
640
|
-
{ facilitator: json }
|
|
641
|
-
);
|
|
642
|
-
}
|
|
643
|
-
}
|
|
4
|
+
//#region src/adapters/base-usdc.ts
|
|
5
|
+
var BASE_USDC_BACKEND_ID = "base-usdc";
|
|
6
|
+
var BASE_MAINNET_CHAIN_ID = 8453;
|
|
7
|
+
var BASE_MAINNET_CAIP2 = "eip155:8453";
|
|
8
|
+
var BASE_USDC_CONTRACT = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
9
|
+
var USDC_DECIMALS = 6;
|
|
10
|
+
var TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
11
|
+
var DEFAULT_LOG_LOOKBACK_BLOCKS = 10000n;
|
|
12
|
+
var DEFAULT_MAX_SEEN_X402_TRANSACTION_IDS = 5e4;
|
|
13
|
+
var BaseUsdcAdapter = class {
|
|
14
|
+
options;
|
|
15
|
+
capabilities;
|
|
16
|
+
fetch;
|
|
17
|
+
rpcUrl;
|
|
18
|
+
usdcContractAddress;
|
|
19
|
+
confirmations;
|
|
20
|
+
configuredFromBlock;
|
|
21
|
+
masterXpub;
|
|
22
|
+
derivationPathPrefix;
|
|
23
|
+
x402FacilitatorUrl;
|
|
24
|
+
maxStoredPaymentOptions;
|
|
25
|
+
allowCumulativeTransferMatching;
|
|
26
|
+
optionsByQuote = /* @__PURE__ */ new Map();
|
|
27
|
+
seenX402TransactionIds = /* @__PURE__ */ new Set();
|
|
28
|
+
rpcId = 0;
|
|
29
|
+
constructor(options) {
|
|
30
|
+
this.options = options;
|
|
31
|
+
if (typeof options.rpcUrl !== "string" || !options.rpcUrl.trim()) throw new PaymentConfigurationError("BaseUsdcAdapter requires an RPC URL.");
|
|
32
|
+
this.masterXpub = options.masterXpub === void 0 ? void 0 : normalizeNonEmptyString(options.masterXpub, "Base USDC masterXpub");
|
|
33
|
+
this.derivationPathPrefix = normalizeDerivationPathPrefix(options.derivationPathPrefix ?? "m/0");
|
|
34
|
+
if (!this.masterXpub && !options.addressDeriver) throw new PaymentConfigurationError("BaseUsdcAdapter requires masterXpub or addressDeriver.");
|
|
35
|
+
this.fetch = getFetch(options.fetch);
|
|
36
|
+
this.maxStoredPaymentOptions = normalizeMaxStoredPaymentOptions(options.maxStoredPaymentOptions, "BaseUsdcAdapter maxStoredPaymentOptions");
|
|
37
|
+
this.allowCumulativeTransferMatching = options.allowCumulativeTransferMatching === true;
|
|
38
|
+
this.rpcUrl = normalizeUrlString(options.rpcUrl, "BaseUsdcAdapter rpcUrl");
|
|
39
|
+
this.usdcContractAddress = normalizeAddress(options.usdcContractAddress ?? "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913");
|
|
40
|
+
this.confirmations = normalizeConfirmationCount(options.confirmations ?? 1, "BaseUsdcAdapter confirmations");
|
|
41
|
+
this.configuredFromBlock = options.fromBlock === void 0 ? void 0 : normalizeFromBlock(options.fromBlock);
|
|
42
|
+
this.x402FacilitatorUrl = options.x402FacilitatorUrl === void 0 ? void 0 : normalizeUrlString(options.x402FacilitatorUrl, "BaseUsdcAdapter x402FacilitatorUrl").replace(/\/$/, "");
|
|
43
|
+
this.capabilities = {
|
|
44
|
+
id: BASE_USDC_BACKEND_ID,
|
|
45
|
+
displayName: "Base USDC",
|
|
46
|
+
settlementCurrency: "USDC",
|
|
47
|
+
supportedSettlementCurrencies: ["USDC"],
|
|
48
|
+
chain: "base",
|
|
49
|
+
settlementShape: "address",
|
|
50
|
+
x402Capable: true,
|
|
51
|
+
confirmationLatency: {
|
|
52
|
+
expectedSeconds: 2,
|
|
53
|
+
minConfirmations: this.confirmations,
|
|
54
|
+
description: "Base L2 USDC transfer confirmation"
|
|
55
|
+
},
|
|
56
|
+
supportsRefunds: true,
|
|
57
|
+
supportsPayouts: true,
|
|
58
|
+
supportsWebhooks: false,
|
|
59
|
+
metadata: {
|
|
60
|
+
chainId: BASE_MAINNET_CHAIN_ID,
|
|
61
|
+
caip2Network: BASE_MAINNET_CAIP2,
|
|
62
|
+
usdcContractAddress: this.usdcContractAddress
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async createPaymentOption(input) {
|
|
67
|
+
const quoteId = normalizeNonEmptyString(input.quoteId, "Base USDC quoteId");
|
|
68
|
+
const expiresAt = normalizeFutureDate(input.expiresAt, "Base USDC expiry");
|
|
69
|
+
const currency = normalizeCurrency(input.currency, "Base USDC");
|
|
70
|
+
const amount = normalizePositiveMinorUnitAmount(input.amount, "Base USDC amount");
|
|
71
|
+
const expectedAmountAtomic = amountToUsdcAtomicUnits(amount, currency, "Base USDC amount");
|
|
72
|
+
const settlementAmount = decimalToMinorUnitAmount(BigInt(expectedAmountAtomic), 6, "Base USDC settlement");
|
|
73
|
+
const configuredIndex = this.options.addressIndexForQuote?.(quoteId);
|
|
74
|
+
const indexes = configuredIndex === void 0 ? quoteIdToDerivationIndexes(quoteId) : [normalizeDerivationIndex(configuredIndex)];
|
|
75
|
+
const index = indexes[0] ?? 0;
|
|
76
|
+
const path = `${this.derivationPathPrefix}/${indexes.join("/")}`;
|
|
77
|
+
const payTo = normalizeAddress(await this.deriveAddress({
|
|
78
|
+
quoteId,
|
|
79
|
+
index,
|
|
80
|
+
indexes,
|
|
81
|
+
path
|
|
82
|
+
}));
|
|
83
|
+
const searchStartBlock = await this.getCurrentBlockNumber();
|
|
84
|
+
const option = {
|
|
85
|
+
backendId: this.capabilities.id,
|
|
86
|
+
quoteId,
|
|
87
|
+
payTo,
|
|
88
|
+
settlementShape: "address",
|
|
89
|
+
settlementCurrency: "USDC",
|
|
90
|
+
settlementAmount,
|
|
91
|
+
amount,
|
|
92
|
+
currency,
|
|
93
|
+
expiresAt,
|
|
94
|
+
paymentUri: buildUsdcPaymentUri({
|
|
95
|
+
tokenAddress: this.usdcContractAddress,
|
|
96
|
+
payTo,
|
|
97
|
+
amountAtomic: expectedAmountAtomic
|
|
98
|
+
}),
|
|
99
|
+
metadata: {
|
|
100
|
+
derivationIndex: index,
|
|
101
|
+
derivationIndexes: indexes,
|
|
102
|
+
derivationPath: path,
|
|
103
|
+
caip2Network: BASE_MAINNET_CAIP2,
|
|
104
|
+
tokenAddress: this.usdcContractAddress,
|
|
105
|
+
searchStartBlock: toHexQuantity(searchStartBlock)
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
rememberPaymentOption(this.optionsByQuote, quoteId, {
|
|
109
|
+
...option,
|
|
110
|
+
expectedAmountAtomic,
|
|
111
|
+
searchStartBlock
|
|
112
|
+
}, this.maxStoredPaymentOptions);
|
|
113
|
+
return option;
|
|
114
|
+
}
|
|
115
|
+
watchPayment(input) {
|
|
116
|
+
return pollPaymentStatus({
|
|
117
|
+
...input,
|
|
118
|
+
pollIntervalMs: input.pollIntervalMs ?? this.options.pollIntervalMs ?? 2e3
|
|
119
|
+
}, () => this.getStatus(input.quoteId, input.payTo, input.statusContext));
|
|
120
|
+
}
|
|
121
|
+
async getStatus(quoteId, payTo, context = {}) {
|
|
122
|
+
const normalizedQuoteId = normalizeNonEmptyString(quoteId, "Base USDC quoteId");
|
|
123
|
+
const normalizedPayTo = normalizeAddress(payTo);
|
|
124
|
+
const option = this.optionsByQuote.get(normalizedQuoteId);
|
|
125
|
+
const expectedAmountAtomic = resolveExpectedUsdcAmount(option, context);
|
|
126
|
+
const contextSearchStartBlock = context.searchStartBlock === void 0 ? void 0 : parseHexQuantity(normalizeFromBlock(context.searchStartBlock));
|
|
127
|
+
const searchStartBlock = option?.searchStartBlock ?? contextSearchStartBlock;
|
|
128
|
+
if (!option && expectedAmountAtomic !== void 0 && searchStartBlock === void 0 && this.configuredFromBlock === void 0) throw new PaymentConfigurationError("Base USDC getStatus requires statusContext.searchStartBlock when checking stateless payment status without a configured fromBlock.");
|
|
129
|
+
const logs = await this.getTransferLogs(normalizedPayTo, searchStartBlock);
|
|
130
|
+
const matchMinBlock = searchStartBlock ?? configuredFromBlockMinBlock(this.configuredFromBlock);
|
|
131
|
+
const matchingTransfer = expectedAmountAtomic === void 0 ? void 0 : this.allowCumulativeTransferMatching ? pickCumulativeTransferMatch(logs, expectedAmountAtomic, void 0, matchMinBlock) : pickSingleTransferMatch(logs, expectedAmountAtomic, void 0, matchMinBlock);
|
|
132
|
+
if (!matchingTransfer) {
|
|
133
|
+
const expiresAt = option?.expiresAt ?? (context.expiresAt === void 0 ? void 0 : normalizeDate(context.expiresAt));
|
|
134
|
+
return {
|
|
135
|
+
backendId: this.capabilities.id,
|
|
136
|
+
quoteId: normalizedQuoteId,
|
|
137
|
+
payTo: normalizedPayTo,
|
|
138
|
+
status: expiresAt && expiresAt < /* @__PURE__ */ new Date() ? "expired" : "pending",
|
|
139
|
+
settlementCurrency: "USDC",
|
|
140
|
+
settlementAmount: option?.settlementAmount ?? (context.settlementAmount === void 0 ? void 0 : normalizeMinorUnitAmount(context.settlementAmount, "Base USDC settlement status")),
|
|
141
|
+
amount: option?.amount ?? (context.amount === void 0 ? void 0 : normalizeMinorUnitAmount(context.amount, "Base USDC status")),
|
|
142
|
+
currency: option?.currency ?? (context.currency === void 0 ? void 0 : normalizeCurrency(context.currency, "Base USDC status")),
|
|
143
|
+
requiredConfirmations: this.confirmations,
|
|
144
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const confirmations = calculateConfirmations(await this.getCurrentBlockNumber(), readLogBlockNumber(matchingTransfer.latestLog));
|
|
148
|
+
const requiredConfirmations = this.confirmations;
|
|
149
|
+
const receivedAmountAtomic = decimalToMinorUnitAmount(matchingTransfer.amountAtomic, 6, "Base USDC received");
|
|
150
|
+
return {
|
|
151
|
+
backendId: this.capabilities.id,
|
|
152
|
+
quoteId: normalizedQuoteId,
|
|
153
|
+
payTo: normalizedPayTo,
|
|
154
|
+
status: confirmations >= requiredConfirmations ? "confirmed" : "processing",
|
|
155
|
+
settlementCurrency: "USDC",
|
|
156
|
+
settlementAmount: option?.settlementAmount ?? (context.settlementAmount === void 0 ? void 0 : normalizeMinorUnitAmount(context.settlementAmount, "Base USDC settlement status")),
|
|
157
|
+
receivedAmount: receivedAmountAtomic,
|
|
158
|
+
amount: option?.amount ?? (context.amount === void 0 ? void 0 : normalizeMinorUnitAmount(context.amount, "Base USDC status")),
|
|
159
|
+
currency: option?.currency ?? (context.currency === void 0 ? void 0 : normalizeCurrency(context.currency, "Base USDC status")),
|
|
160
|
+
requiredConfirmations,
|
|
161
|
+
confirmations,
|
|
162
|
+
transactionId: matchingTransfer.latestLog.transactionHash,
|
|
163
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
164
|
+
raw: matchingTransfer.latestLog
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
async verifyX402Proof(input) {
|
|
168
|
+
const quoteId = normalizeNonEmptyString(input.quoteId, "Base USDC quoteId");
|
|
169
|
+
let payload;
|
|
170
|
+
try {
|
|
171
|
+
payload = decodePaymentHeader(input.paymentHeader);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
return {
|
|
174
|
+
valid: false,
|
|
175
|
+
backendId: this.capabilities.id,
|
|
176
|
+
quoteId,
|
|
177
|
+
payTo: input.payTo,
|
|
178
|
+
reason: "x402 payment header could not be decoded.",
|
|
179
|
+
raw: {
|
|
180
|
+
paymentHeader: input.paymentHeader,
|
|
181
|
+
error: error instanceof Error ? error.message : String(error)
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const transactionId = readX402PayloadString(payload, [
|
|
186
|
+
"transactionHash",
|
|
187
|
+
"txHash",
|
|
188
|
+
"tx",
|
|
189
|
+
"hash"
|
|
190
|
+
])?.trim();
|
|
191
|
+
if (this.x402FacilitatorUrl) return this.verifyWithFacilitator(input, payload);
|
|
192
|
+
if (!transactionId) return {
|
|
193
|
+
valid: false,
|
|
194
|
+
backendId: this.capabilities.id,
|
|
195
|
+
quoteId,
|
|
196
|
+
payTo: input.payTo,
|
|
197
|
+
reason: "x402 proof did not include a transaction hash and no facilitator URL is configured.",
|
|
198
|
+
raw: payload
|
|
199
|
+
};
|
|
200
|
+
if (!isEvmTransactionHash(transactionId)) return {
|
|
201
|
+
valid: false,
|
|
202
|
+
backendId: this.capabilities.id,
|
|
203
|
+
quoteId,
|
|
204
|
+
payTo: input.payTo,
|
|
205
|
+
transactionId,
|
|
206
|
+
reason: "x402 proof transaction hash was invalid.",
|
|
207
|
+
raw: payload
|
|
208
|
+
};
|
|
209
|
+
const requirementError = validateX402PayloadRequirements(payload, input);
|
|
210
|
+
if (requirementError) return {
|
|
211
|
+
valid: false,
|
|
212
|
+
backendId: this.capabilities.id,
|
|
213
|
+
quoteId,
|
|
214
|
+
payTo: input.payTo,
|
|
215
|
+
transactionId,
|
|
216
|
+
reason: requirementError,
|
|
217
|
+
raw: payload
|
|
218
|
+
};
|
|
219
|
+
const expectedAmountAtomic = amountToUsdcAtomicUnits(normalizePositiveMinorUnitAmount(input.amount, "Base USDC x402 amount"), normalizeCurrency(input.currency, "Base USDC x402"), "Base USDC x402 amount");
|
|
220
|
+
return this.verifyX402Receipt(input, payload, quoteId, transactionId, expectedAmountAtomic);
|
|
221
|
+
}
|
|
222
|
+
async sendPayout(input) {
|
|
223
|
+
if (input.currency !== void 0) {
|
|
224
|
+
if (normalizeNonEmptyString(input.currency, "Base USDC payout currency").toUpperCase() !== "USDC") throw new PaymentConfigurationError(`BaseUsdcAdapter can only send USDC payouts, received ${input.currency}.`);
|
|
225
|
+
}
|
|
226
|
+
if (!this.options.sendTransaction) throw new PaymentConfigurationError("BaseUsdcAdapter sendPayout requires a sendTransaction callback.");
|
|
227
|
+
const amount = normalizePositiveMinorUnitAmount(input.amount, "Base USDC payout amount");
|
|
228
|
+
const amountAtomic = String(amount);
|
|
229
|
+
const destination = normalizeAddress(input.destination);
|
|
230
|
+
const quoteId = input.quoteId === void 0 ? void 0 : normalizeNonEmptyString(input.quoteId, "Base USDC payout quoteId");
|
|
231
|
+
const idempotencyKey = input.idempotencyKey === void 0 ? void 0 : normalizeNonEmptyString(input.idempotencyKey, "Base USDC payout idempotencyKey");
|
|
232
|
+
const result = await this.options.sendTransaction({
|
|
233
|
+
destination,
|
|
234
|
+
amount,
|
|
235
|
+
amountAtomic,
|
|
236
|
+
currency: "USDC",
|
|
237
|
+
tokenAddress: this.usdcContractAddress,
|
|
238
|
+
memo: input.memo,
|
|
239
|
+
quoteId,
|
|
240
|
+
idempotencyKey,
|
|
241
|
+
metadata: input.metadata
|
|
242
|
+
});
|
|
243
|
+
return {
|
|
244
|
+
backendId: this.capabilities.id,
|
|
245
|
+
status: "submitted",
|
|
246
|
+
transactionId: result.transactionId,
|
|
247
|
+
destination,
|
|
248
|
+
amount,
|
|
249
|
+
currency: "USDC",
|
|
250
|
+
raw: result.raw
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
async refundPayment(input) {
|
|
254
|
+
if (input.currency !== void 0) {
|
|
255
|
+
if (normalizeNonEmptyString(input.currency, "Base USDC refund currency").toUpperCase() !== "USDC") throw new PaymentConfigurationError(`BaseUsdcAdapter refunds require USDC currency when specified, received ${input.currency}.`);
|
|
256
|
+
}
|
|
257
|
+
if (!input.destination) throw new PaymentConfigurationError("Base USDC refunds require a destination payer address.");
|
|
258
|
+
if (input.amount === void 0) throw new PaymentConfigurationError("Base USDC refunds require an amount.");
|
|
259
|
+
const payout = await this.sendPayout({
|
|
260
|
+
destination: input.destination,
|
|
261
|
+
amount: input.amount,
|
|
262
|
+
currency: "USDC",
|
|
263
|
+
idempotencyKey: input.idempotencyKey,
|
|
264
|
+
memo: input.reason,
|
|
265
|
+
metadata: input.metadata
|
|
266
|
+
});
|
|
267
|
+
return {
|
|
268
|
+
backendId: this.capabilities.id,
|
|
269
|
+
status: "submitted",
|
|
270
|
+
transactionId: payout.transactionId,
|
|
271
|
+
amount: payout.amount,
|
|
272
|
+
currency: payout.currency,
|
|
273
|
+
raw: payout.raw
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
async deriveAddress(input) {
|
|
277
|
+
if (this.options.addressDeriver) return this.options.addressDeriver(input);
|
|
278
|
+
if (!this.masterXpub) throw new PaymentConfigurationError("BaseUsdcAdapter requires masterXpub or addressDeriver.");
|
|
279
|
+
return deriveBaseUsdcAddress({
|
|
280
|
+
masterXpub: this.masterXpub,
|
|
281
|
+
path: input.path
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
async getTransferLogs(payTo, requiredFromBlock) {
|
|
285
|
+
const fromBlock = this.configuredFromBlock ?? await this.getDefaultFromBlock(requiredFromBlock);
|
|
286
|
+
const logs = await this.rpc("eth_getLogs", [{
|
|
287
|
+
address: this.usdcContractAddress,
|
|
288
|
+
fromBlock,
|
|
289
|
+
toBlock: "latest",
|
|
290
|
+
topics: [
|
|
291
|
+
TRANSFER_TOPIC,
|
|
292
|
+
null,
|
|
293
|
+
addressToTopic(payTo)
|
|
294
|
+
]
|
|
295
|
+
}]);
|
|
296
|
+
return Array.isArray(logs) ? logs : [];
|
|
297
|
+
}
|
|
298
|
+
async getDefaultFromBlock(requiredFromBlock) {
|
|
299
|
+
const currentBlock = await this.getCurrentBlockNumber();
|
|
300
|
+
const lookbackStartBlock = currentBlock > DEFAULT_LOG_LOOKBACK_BLOCKS ? currentBlock - DEFAULT_LOG_LOOKBACK_BLOCKS : 0n;
|
|
301
|
+
return toHexQuantity(requiredFromBlock ?? lookbackStartBlock);
|
|
302
|
+
}
|
|
303
|
+
async getCurrentBlockNumber() {
|
|
304
|
+
return parseHexQuantity(await this.rpc("eth_blockNumber", []));
|
|
305
|
+
}
|
|
306
|
+
async rpc(method, params) {
|
|
307
|
+
const json = await readJsonResponse(await this.fetch(this.rpcUrl, {
|
|
308
|
+
method: "POST",
|
|
309
|
+
headers: { "Content-Type": "application/json" },
|
|
310
|
+
body: JSON.stringify({
|
|
311
|
+
jsonrpc: "2.0",
|
|
312
|
+
id: ++this.rpcId,
|
|
313
|
+
method,
|
|
314
|
+
params
|
|
315
|
+
})
|
|
316
|
+
}), `Base RPC ${method}`);
|
|
317
|
+
if (json.error) throw new PaymentProviderError(`Base RPC ${method}: ${json.error.message}`, { retryable: json.error.code === -32005 });
|
|
318
|
+
return json.result;
|
|
319
|
+
}
|
|
320
|
+
async verifyX402Receipt(input, payload, quoteId, transactionId, expectedAmountAtomic, extraRaw) {
|
|
321
|
+
if (this.seenX402TransactionIds.has(transactionId)) return {
|
|
322
|
+
valid: false,
|
|
323
|
+
backendId: this.capabilities.id,
|
|
324
|
+
quoteId,
|
|
325
|
+
payTo: input.payTo,
|
|
326
|
+
transactionId,
|
|
327
|
+
reason: "x402 transaction has already been verified.",
|
|
328
|
+
raw: {
|
|
329
|
+
...extraRaw,
|
|
330
|
+
payload
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
this.seenX402TransactionIds.add(transactionId);
|
|
334
|
+
let keepReplayMarker = false;
|
|
335
|
+
try {
|
|
336
|
+
const receipt = await this.rpc("eth_getTransactionReceipt", [transactionId]);
|
|
337
|
+
const raw = {
|
|
338
|
+
...extraRaw,
|
|
339
|
+
payload,
|
|
340
|
+
receipt
|
|
341
|
+
};
|
|
342
|
+
if (!receipt) return {
|
|
343
|
+
valid: false,
|
|
344
|
+
backendId: this.capabilities.id,
|
|
345
|
+
quoteId,
|
|
346
|
+
payTo: input.payTo,
|
|
347
|
+
transactionId,
|
|
348
|
+
reason: "x402 transaction receipt is not available yet.",
|
|
349
|
+
raw
|
|
350
|
+
};
|
|
351
|
+
if (receipt.status !== "0x1") return {
|
|
352
|
+
valid: false,
|
|
353
|
+
backendId: this.capabilities.id,
|
|
354
|
+
quoteId,
|
|
355
|
+
payTo: input.payTo,
|
|
356
|
+
transactionId,
|
|
357
|
+
reason: "x402 transaction failed onchain.",
|
|
358
|
+
raw
|
|
359
|
+
};
|
|
360
|
+
const blockNumber = parseHexQuantity(receipt.blockNumber ?? "0x0");
|
|
361
|
+
const confirmations = calculateConfirmations(blockNumber > 0n ? await this.getCurrentBlockNumber() : 0n, blockNumber);
|
|
362
|
+
if (confirmations < this.confirmations) return {
|
|
363
|
+
valid: false,
|
|
364
|
+
backendId: this.capabilities.id,
|
|
365
|
+
quoteId,
|
|
366
|
+
payTo: input.payTo,
|
|
367
|
+
transactionId,
|
|
368
|
+
reason: `x402 transaction has ${confirmations} confirmations; ${this.confirmations} required.`,
|
|
369
|
+
raw
|
|
370
|
+
};
|
|
371
|
+
const matchingLog = pickBestTransferLog((Array.isArray(receipt.logs) ? receipt.logs : []).filter((log) => isRecord(log) && safeNormalizeAddress(log.address) === this.usdcContractAddress), expectedAmountAtomic, input.payTo, "exact");
|
|
372
|
+
if (!matchingLog) return {
|
|
373
|
+
valid: false,
|
|
374
|
+
backendId: this.capabilities.id,
|
|
375
|
+
quoteId,
|
|
376
|
+
payTo: input.payTo,
|
|
377
|
+
transactionId,
|
|
378
|
+
reason: "No exact USDC transfer matching the x402 proof.",
|
|
379
|
+
raw
|
|
380
|
+
};
|
|
381
|
+
keepReplayMarker = true;
|
|
382
|
+
trimSeenX402TransactionIds(this.seenX402TransactionIds);
|
|
383
|
+
return {
|
|
384
|
+
valid: true,
|
|
385
|
+
backendId: this.capabilities.id,
|
|
386
|
+
quoteId,
|
|
387
|
+
payTo: normalizeAddress(input.payTo),
|
|
388
|
+
transactionId,
|
|
389
|
+
payer: parseTransferLog(matchingLog)?.from,
|
|
390
|
+
amount: decimalToMinorUnitAmount(parseHexQuantity(matchingLog.data), 6, "Base USDC x402 amount"),
|
|
391
|
+
currency: "USDC",
|
|
392
|
+
network: input.network ?? "eip155:8453",
|
|
393
|
+
raw
|
|
394
|
+
};
|
|
395
|
+
} finally {
|
|
396
|
+
if (!keepReplayMarker) this.seenX402TransactionIds.delete(transactionId);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
async verifyWithFacilitator(input, payload) {
|
|
400
|
+
const quoteId = normalizeNonEmptyString(input.quoteId, "Base USDC quoteId");
|
|
401
|
+
if (!this.x402FacilitatorUrl) throw new PaymentVerificationError("x402 facilitator URL is missing.");
|
|
402
|
+
const maxAmountRequired = amountToUsdcAtomicUnits(normalizePositiveMinorUnitAmount(input.amount, "Base USDC x402 amount"), normalizeCurrency(input.currency, "Base USDC x402"), "Base USDC x402 amount");
|
|
403
|
+
const json = await readJsonResponse(await this.fetch(`${this.x402FacilitatorUrl}/verify`, {
|
|
404
|
+
method: "POST",
|
|
405
|
+
headers: { "Content-Type": "application/json" },
|
|
406
|
+
body: JSON.stringify({
|
|
407
|
+
paymentPayload: payload,
|
|
408
|
+
paymentRequirements: {
|
|
409
|
+
scheme: input.scheme ?? "exact",
|
|
410
|
+
network: input.network ?? "eip155:8453",
|
|
411
|
+
asset: this.usdcContractAddress,
|
|
412
|
+
payTo: normalizeAddress(input.payTo),
|
|
413
|
+
maxAmountRequired,
|
|
414
|
+
resource: input.resource,
|
|
415
|
+
method: input.method
|
|
416
|
+
}
|
|
417
|
+
})
|
|
418
|
+
}), "x402 facilitator verify");
|
|
419
|
+
const valid = readBoolean(json, "valid") ?? readBoolean(json, "isValid");
|
|
420
|
+
const transactionId = findNestedString(json, ["transactionHash", "txHash"])?.trim();
|
|
421
|
+
if (valid !== true) return {
|
|
422
|
+
valid: false,
|
|
423
|
+
backendId: this.capabilities.id,
|
|
424
|
+
quoteId,
|
|
425
|
+
payTo: normalizeAddress(input.payTo),
|
|
426
|
+
transactionId,
|
|
427
|
+
network: input.network ?? "eip155:8453",
|
|
428
|
+
reason: String(json.reason ?? "facilitator rejected"),
|
|
429
|
+
raw: json
|
|
430
|
+
};
|
|
431
|
+
if (!transactionId) return {
|
|
432
|
+
valid: false,
|
|
433
|
+
backendId: this.capabilities.id,
|
|
434
|
+
quoteId,
|
|
435
|
+
payTo: normalizeAddress(input.payTo),
|
|
436
|
+
network: input.network ?? "eip155:8453",
|
|
437
|
+
reason: "x402 facilitator did not return a transaction hash.",
|
|
438
|
+
raw: json
|
|
439
|
+
};
|
|
440
|
+
if (!isEvmTransactionHash(transactionId)) return {
|
|
441
|
+
valid: false,
|
|
442
|
+
backendId: this.capabilities.id,
|
|
443
|
+
quoteId,
|
|
444
|
+
payTo: normalizeAddress(input.payTo),
|
|
445
|
+
transactionId,
|
|
446
|
+
network: input.network ?? "eip155:8453",
|
|
447
|
+
reason: "x402 facilitator transaction hash was invalid.",
|
|
448
|
+
raw: json
|
|
449
|
+
};
|
|
450
|
+
const requirementError = validateX402PayloadRequirements(payload, input);
|
|
451
|
+
if (requirementError) return {
|
|
452
|
+
valid: false,
|
|
453
|
+
backendId: this.capabilities.id,
|
|
454
|
+
quoteId,
|
|
455
|
+
payTo: normalizeAddress(input.payTo),
|
|
456
|
+
transactionId,
|
|
457
|
+
network: input.network ?? "eip155:8453",
|
|
458
|
+
reason: requirementError,
|
|
459
|
+
raw: json
|
|
460
|
+
};
|
|
461
|
+
return this.verifyX402Receipt(input, payload, quoteId, transactionId, maxAmountRequired, { facilitator: json });
|
|
462
|
+
}
|
|
463
|
+
};
|
|
644
464
|
async function deriveBaseUsdcAddress(input) {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
const point = secp256k1.Point.fromBytes(child.publicKey);
|
|
662
|
-
const { x, y } = point.toAffine();
|
|
663
|
-
const publicKey = concatBytes(bigintToFixedBytes(x), bigintToFixedBytes(y));
|
|
664
|
-
const hash = keccak_256(publicKey);
|
|
665
|
-
return `0x${bytesToHex(hash.slice(-20))}`;
|
|
666
|
-
} catch (error) {
|
|
667
|
-
if (error instanceof PaymentConfigurationError) {
|
|
668
|
-
throw error;
|
|
669
|
-
}
|
|
670
|
-
throw new PaymentConfigurationError(
|
|
671
|
-
"BIP32 xpub address derivation requires @scure/bip32, @noble/curves, and @noble/hashes.",
|
|
672
|
-
{ cause: error }
|
|
673
|
-
);
|
|
674
|
-
}
|
|
465
|
+
const masterXpub = normalizeNonEmptyString(input.masterXpub, "Base USDC masterXpub");
|
|
466
|
+
const path = normalizeNonEmptyString(input.path, "Base USDC derivation path");
|
|
467
|
+
try {
|
|
468
|
+
const [{ HDKey }, { secp256k1 }, { keccak_256 }] = await Promise.all([
|
|
469
|
+
import("@scure/bip32"),
|
|
470
|
+
import("@noble/curves/secp256k1.js"),
|
|
471
|
+
import("@noble/hashes/sha3.js")
|
|
472
|
+
]);
|
|
473
|
+
const child = HDKey.fromExtendedKey(masterXpub).derive(path);
|
|
474
|
+
if (!child.publicKey) throw new PaymentConfigurationError(`No public key derived for ${path}.`);
|
|
475
|
+
const { x, y } = secp256k1.Point.fromBytes(child.publicKey).toAffine();
|
|
476
|
+
return `0x${bytesToHex(keccak_256(concatBytes(bigintToFixedBytes(x), bigintToFixedBytes(y))).slice(-20))}`;
|
|
477
|
+
} catch (error) {
|
|
478
|
+
if (error instanceof PaymentConfigurationError) throw error;
|
|
479
|
+
throw new PaymentConfigurationError("BIP32 xpub address derivation requires @scure/bip32, @noble/curves, and @noble/hashes.", { cause: error });
|
|
480
|
+
}
|
|
675
481
|
}
|
|
676
482
|
function quoteIdToDerivationIndex(quoteId) {
|
|
677
|
-
|
|
483
|
+
return quoteIdToDerivationIndexes(quoteId)[0] ?? 0;
|
|
678
484
|
}
|
|
679
485
|
function quoteIdToDerivationIndexes(quoteId) {
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
486
|
+
const normalizedQuoteId = normalizeNonEmptyString(quoteId, "Base USDC quoteId");
|
|
487
|
+
const hash = createHash("sha256").update(normalizedQuoteId).digest();
|
|
488
|
+
return [
|
|
489
|
+
0,
|
|
490
|
+
4,
|
|
491
|
+
8,
|
|
492
|
+
12
|
|
493
|
+
].map((offset) => hash.readUInt32BE(offset) & 2147483647);
|
|
686
494
|
}
|
|
687
495
|
function normalizeDerivationIndex(index) {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
`BaseUsdcAdapter addressIndexForQuote must return a non-hardened BIP32 child index, received ${String(index)}.`
|
|
691
|
-
);
|
|
692
|
-
}
|
|
693
|
-
return index;
|
|
496
|
+
if (!Number.isInteger(index) || index < 0 || index > 2147483647) throw new PaymentConfigurationError(`BaseUsdcAdapter addressIndexForQuote must return a non-hardened BIP32 child index, received ${String(index)}.`);
|
|
497
|
+
return index;
|
|
694
498
|
}
|
|
695
499
|
function normalizeDerivationPathPrefix(value) {
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
}
|
|
701
|
-
const normalized = value.trim().replace(/\/+$/, "");
|
|
702
|
-
if (!normalized) {
|
|
703
|
-
throw new PaymentConfigurationError(
|
|
704
|
-
"BaseUsdcAdapter derivationPathPrefix must not be empty when configured."
|
|
705
|
-
);
|
|
706
|
-
}
|
|
707
|
-
return normalized;
|
|
500
|
+
if (typeof value !== "string") throw new PaymentConfigurationError("BaseUsdcAdapter derivationPathPrefix must be a string.");
|
|
501
|
+
const normalized = value.trim().replace(/\/+$/, "");
|
|
502
|
+
if (!normalized) throw new PaymentConfigurationError("BaseUsdcAdapter derivationPathPrefix must not be empty when configured.");
|
|
503
|
+
return normalized;
|
|
708
504
|
}
|
|
709
505
|
function normalizeConfirmationCount(value, label) {
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
`${label} must be a non-negative integer, received ${String(value)}.`
|
|
713
|
-
);
|
|
714
|
-
}
|
|
715
|
-
return value;
|
|
506
|
+
if (!Number.isInteger(value) || value < 0) throw new PaymentConfigurationError(`${label} must be a non-negative integer, received ${String(value)}.`);
|
|
507
|
+
return value;
|
|
716
508
|
}
|
|
717
509
|
function readBoolean(value, key) {
|
|
718
|
-
|
|
719
|
-
|
|
510
|
+
const item = value[key];
|
|
511
|
+
return typeof item === "boolean" ? item : void 0;
|
|
720
512
|
}
|
|
721
513
|
function normalizeFromBlock(value) {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
throw new PaymentConfigurationError(
|
|
740
|
-
`BaseUsdcAdapter fromBlock must be a non-negative JSON-RPC block tag or quantity, received ${String(value)}.`
|
|
741
|
-
);
|
|
742
|
-
}
|
|
743
|
-
const normalized = value.trim();
|
|
744
|
-
if (["earliest", "latest", "pending"].includes(normalized)) {
|
|
745
|
-
return normalized;
|
|
746
|
-
}
|
|
747
|
-
if (/^0x[0-9a-fA-F]+$/.test(normalized)) {
|
|
748
|
-
return normalized.toLowerCase();
|
|
749
|
-
}
|
|
750
|
-
throw new PaymentConfigurationError(
|
|
751
|
-
`BaseUsdcAdapter fromBlock must be a non-negative JSON-RPC block tag or quantity, received ${String(value)}.`
|
|
752
|
-
);
|
|
514
|
+
if (typeof value === "bigint") {
|
|
515
|
+
if (value < 0n) throw new PaymentConfigurationError(`BaseUsdcAdapter fromBlock must be a non-negative JSON-RPC block tag or quantity, received ${String(value)}.`);
|
|
516
|
+
return toHexQuantity(value);
|
|
517
|
+
}
|
|
518
|
+
if (typeof value === "number") {
|
|
519
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new PaymentConfigurationError(`BaseUsdcAdapter fromBlock must be a non-negative JSON-RPC block tag or quantity, received ${String(value)}.`);
|
|
520
|
+
return toHexQuantity(BigInt(value));
|
|
521
|
+
}
|
|
522
|
+
if (typeof value !== "string") throw new PaymentConfigurationError(`BaseUsdcAdapter fromBlock must be a non-negative JSON-RPC block tag or quantity, received ${String(value)}.`);
|
|
523
|
+
const normalized = value.trim();
|
|
524
|
+
if ([
|
|
525
|
+
"earliest",
|
|
526
|
+
"latest",
|
|
527
|
+
"pending"
|
|
528
|
+
].includes(normalized)) return normalized;
|
|
529
|
+
if (/^0x[0-9a-fA-F]+$/.test(normalized)) return normalized.toLowerCase();
|
|
530
|
+
throw new PaymentConfigurationError(`BaseUsdcAdapter fromBlock must be a non-negative JSON-RPC block tag or quantity, received ${String(value)}.`);
|
|
753
531
|
}
|
|
754
532
|
function configuredFromBlockMinBlock(value) {
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
533
|
+
if (value === void 0 || [
|
|
534
|
+
"earliest",
|
|
535
|
+
"latest",
|
|
536
|
+
"pending"
|
|
537
|
+
].includes(value)) return 0n;
|
|
538
|
+
return parseHexQuantity(value);
|
|
759
539
|
}
|
|
760
540
|
function calculateConfirmations(currentBlock, blockNumber) {
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
const confirmations = currentBlock - blockNumber + 1n;
|
|
765
|
-
return confirmations > BigInt(Number.MAX_SAFE_INTEGER) ? Number.MAX_SAFE_INTEGER : Number(confirmations);
|
|
541
|
+
if (blockNumber <= 0n || currentBlock < blockNumber) return 0;
|
|
542
|
+
const confirmations = currentBlock - blockNumber + 1n;
|
|
543
|
+
return confirmations > BigInt(Number.MAX_SAFE_INTEGER) ? Number.MAX_SAFE_INTEGER : Number(confirmations);
|
|
766
544
|
}
|
|
767
545
|
function resolveExpectedUsdcAmount(option, context) {
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
return String(
|
|
773
|
-
normalizePositiveMinorUnitAmount(
|
|
774
|
-
context.settlementAmount,
|
|
775
|
-
"Base USDC expected amount"
|
|
776
|
-
)
|
|
777
|
-
);
|
|
778
|
-
}
|
|
779
|
-
if (context.amount === void 0 || context.currency === void 0) {
|
|
780
|
-
return void 0;
|
|
781
|
-
}
|
|
782
|
-
return amountToUsdcAtomicUnits(
|
|
783
|
-
normalizePositiveMinorUnitAmount(
|
|
784
|
-
context.amount,
|
|
785
|
-
"Base USDC expected amount"
|
|
786
|
-
),
|
|
787
|
-
normalizeCurrency(context.currency, "Base USDC expected amount"),
|
|
788
|
-
"Base USDC expected amount"
|
|
789
|
-
);
|
|
546
|
+
if (option) return option.expectedAmountAtomic;
|
|
547
|
+
if (context.settlementAmount !== void 0) return String(normalizePositiveMinorUnitAmount(context.settlementAmount, "Base USDC expected amount"));
|
|
548
|
+
if (context.amount === void 0 || context.currency === void 0) return;
|
|
549
|
+
return amountToUsdcAtomicUnits(normalizePositiveMinorUnitAmount(context.amount, "Base USDC expected amount"), normalizeCurrency(context.currency, "Base USDC expected amount"), "Base USDC expected amount");
|
|
790
550
|
}
|
|
791
551
|
function amountToUsdcAtomicUnits(amount, currency, context) {
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
`${context} currency must be USD or USDC for Base USDC settlement, received ${currency}.`
|
|
796
|
-
);
|
|
797
|
-
}
|
|
798
|
-
const decimalAmount = minorUnitsToDecimal(
|
|
799
|
-
amount,
|
|
800
|
-
currencyMinorUnitDecimals(normalizedCurrency)
|
|
801
|
-
);
|
|
802
|
-
return decimalToAtomicUnits(decimalAmount, USDC_DECIMALS);
|
|
552
|
+
const normalizedCurrency = normalizeCurrency(currency, context);
|
|
553
|
+
if (!["USD", "USDC"].includes(normalizedCurrency)) throw new PaymentConfigurationError(`${context} currency must be USD or USDC for Base USDC settlement, received ${currency}.`);
|
|
554
|
+
return decimalToAtomicUnits(minorUnitsToDecimal(amount, currencyMinorUnitDecimals(normalizedCurrency)), 6);
|
|
803
555
|
}
|
|
804
556
|
function pickBestTransferLog(logs, expectedAmountAtomic, payTo, comparison = "atLeast", minBlockNumber = 0n) {
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
}
|
|
814
|
-
if (payTo && transfer.to !== normalizeAddress(payTo)) {
|
|
815
|
-
return false;
|
|
816
|
-
}
|
|
817
|
-
return comparison === "exact" ? transfer.amountAtomic === expected : transfer.amountAtomic >= expected;
|
|
818
|
-
});
|
|
557
|
+
const expected = BigInt(expectedAmountAtomic);
|
|
558
|
+
return logs.find((log) => {
|
|
559
|
+
const transfer = parseTransferLog(log);
|
|
560
|
+
if (!transfer || log.removed) return false;
|
|
561
|
+
if (readLogBlockNumber(log) < minBlockNumber) return false;
|
|
562
|
+
if (payTo && transfer.to !== normalizeAddress(payTo)) return false;
|
|
563
|
+
return comparison === "exact" ? transfer.amountAtomic === expected : transfer.amountAtomic >= expected;
|
|
564
|
+
});
|
|
819
565
|
}
|
|
820
566
|
function pickSingleTransferMatch(logs, expectedAmountAtomic, payTo, minBlockNumber = 0n) {
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
);
|
|
828
|
-
const transfer = log ? parseTransferLog(log) : void 0;
|
|
829
|
-
return log && transfer ? { amountAtomic: transfer.amountAtomic, latestLog: log } : void 0;
|
|
567
|
+
const log = pickBestTransferLog(logs, expectedAmountAtomic, payTo, "atLeast", minBlockNumber);
|
|
568
|
+
const transfer = log ? parseTransferLog(log) : void 0;
|
|
569
|
+
return log && transfer ? {
|
|
570
|
+
amountAtomic: transfer.amountAtomic,
|
|
571
|
+
latestLog: log
|
|
572
|
+
} : void 0;
|
|
830
573
|
}
|
|
831
574
|
function pickCumulativeTransferMatch(logs, expectedAmountAtomic, payTo, minBlockNumber = 0n) {
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
latestLog
|
|
854
|
-
});
|
|
855
|
-
}
|
|
856
|
-
const candidates = [...byPayer.values()].filter(
|
|
857
|
-
(item) => item.amountAtomic >= expected
|
|
858
|
-
);
|
|
859
|
-
if (candidates.length === 0) {
|
|
860
|
-
return void 0;
|
|
861
|
-
}
|
|
862
|
-
return candidates.reduce(
|
|
863
|
-
(current, item) => compareTransferLogs(item.latestLog, current.latestLog) > 0 ? item : current
|
|
864
|
-
);
|
|
575
|
+
const expected = BigInt(expectedAmountAtomic);
|
|
576
|
+
const matched = logs.map((log) => ({
|
|
577
|
+
log,
|
|
578
|
+
transfer: parseTransferLog(log)
|
|
579
|
+
})).filter((item) => {
|
|
580
|
+
if (!item.transfer || item.log.removed) return false;
|
|
581
|
+
if (readLogBlockNumber(item.log) < minBlockNumber) return false;
|
|
582
|
+
return payTo ? item.transfer.to === normalizeAddress(payTo) : true;
|
|
583
|
+
});
|
|
584
|
+
const byPayer = /* @__PURE__ */ new Map();
|
|
585
|
+
for (const item of matched) {
|
|
586
|
+
const existing = byPayer.get(item.transfer.from);
|
|
587
|
+
const latestLog = existing && compareTransferLogs(existing.latestLog, item.log) >= 0 ? existing.latestLog : item.log;
|
|
588
|
+
byPayer.set(item.transfer.from, {
|
|
589
|
+
amountAtomic: (existing?.amountAtomic ?? 0n) + item.transfer.amountAtomic,
|
|
590
|
+
latestLog
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
const candidates = [...byPayer.values()].filter((item) => item.amountAtomic >= expected);
|
|
594
|
+
if (candidates.length === 0) return;
|
|
595
|
+
return candidates.reduce((current, item) => compareTransferLogs(item.latestLog, current.latestLog) > 0 ? item : current);
|
|
865
596
|
}
|
|
866
597
|
function parseTransferLog(log) {
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
};
|
|
879
|
-
} catch {
|
|
880
|
-
return void 0;
|
|
881
|
-
}
|
|
598
|
+
if (!isRecord(log) || !Array.isArray(log.topics) || typeof log.data !== "string") return;
|
|
599
|
+
if (log.topics[0]?.toLowerCase() !== TRANSFER_TOPIC || log.topics.length < 3) return;
|
|
600
|
+
try {
|
|
601
|
+
return {
|
|
602
|
+
from: topicToAddress(log.topics[1] ?? ""),
|
|
603
|
+
to: topicToAddress(log.topics[2] ?? ""),
|
|
604
|
+
amountAtomic: parseHexQuantity(log.data)
|
|
605
|
+
};
|
|
606
|
+
} catch {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
882
609
|
}
|
|
883
610
|
function readLogBlockNumber(log) {
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
611
|
+
try {
|
|
612
|
+
return parseHexQuantity(log.blockNumber ?? "0x0");
|
|
613
|
+
} catch {
|
|
614
|
+
return 0n;
|
|
615
|
+
}
|
|
889
616
|
}
|
|
890
617
|
function compareTransferLogs(left, right) {
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
if (leftBlock < rightBlock) {
|
|
897
|
-
return -1;
|
|
898
|
-
}
|
|
899
|
-
return (left.transactionHash ?? "").toLowerCase().localeCompare((right.transactionHash ?? "").toLowerCase());
|
|
618
|
+
const leftBlock = readLogBlockNumber(left);
|
|
619
|
+
const rightBlock = readLogBlockNumber(right);
|
|
620
|
+
if (leftBlock > rightBlock) return 1;
|
|
621
|
+
if (leftBlock < rightBlock) return -1;
|
|
622
|
+
return (left.transactionHash ?? "").toLowerCase().localeCompare((right.transactionHash ?? "").toLowerCase());
|
|
900
623
|
}
|
|
901
624
|
function trimSeenX402TransactionIds(seenTransactionIds) {
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
seenTransactionIds.delete(oldest);
|
|
908
|
-
}
|
|
625
|
+
while (seenTransactionIds.size > DEFAULT_MAX_SEEN_X402_TRANSACTION_IDS) {
|
|
626
|
+
const oldest = seenTransactionIds.values().next().value;
|
|
627
|
+
if (oldest === void 0) break;
|
|
628
|
+
seenTransactionIds.delete(oldest);
|
|
629
|
+
}
|
|
909
630
|
}
|
|
910
631
|
function buildUsdcPaymentUri(input) {
|
|
911
|
-
|
|
632
|
+
return `ethereum:${input.tokenAddress}@${BASE_MAINNET_CHAIN_ID}/transfer?address=${input.payTo}&uint256=${input.amountAtomic}`;
|
|
912
633
|
}
|
|
913
634
|
function isRecord(value) {
|
|
914
|
-
|
|
635
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
915
636
|
}
|
|
916
637
|
function normalizeAddress(address) {
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
}
|
|
922
|
-
const normalized = address.trim();
|
|
923
|
-
if (!/^0x[a-fA-F0-9]{40}$/.test(normalized)) {
|
|
924
|
-
throw new PaymentConfigurationError(`Invalid EVM address: ${address}`);
|
|
925
|
-
}
|
|
926
|
-
return normalized.toLowerCase();
|
|
638
|
+
if (typeof address !== "string") throw new PaymentConfigurationError(`Invalid EVM address: ${String(address)}`);
|
|
639
|
+
const normalized = address.trim();
|
|
640
|
+
if (!/^0x[a-fA-F0-9]{40}$/.test(normalized)) throw new PaymentConfigurationError(`Invalid EVM address: ${address}`);
|
|
641
|
+
return normalized.toLowerCase();
|
|
927
642
|
}
|
|
928
643
|
function isEvmTransactionHash(value) {
|
|
929
|
-
|
|
644
|
+
return /^0x[a-fA-F0-9]{64}$/.test(value);
|
|
930
645
|
}
|
|
931
646
|
function safeNormalizeAddress(address) {
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
647
|
+
try {
|
|
648
|
+
return normalizeAddress(address);
|
|
649
|
+
} catch {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
937
652
|
}
|
|
938
653
|
function addressToTopic(address) {
|
|
939
|
-
|
|
654
|
+
return `0x${normalizeAddress(address).slice(2).padStart(64, "0")}`;
|
|
940
655
|
}
|
|
941
656
|
function topicToAddress(topic) {
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
}
|
|
945
|
-
return normalizeAddress(`0x${topic.slice(-40)}`);
|
|
657
|
+
if (!/^0x[a-fA-F0-9]{64}$/.test(topic)) throw new PaymentProviderError(`Invalid EVM address topic: ${topic}`);
|
|
658
|
+
return normalizeAddress(`0x${topic.slice(-40)}`);
|
|
946
659
|
}
|
|
947
660
|
function parseHexQuantity(value) {
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
);
|
|
961
|
-
}
|
|
962
|
-
return BigInt(value);
|
|
963
|
-
}
|
|
964
|
-
if (typeof value !== "string") {
|
|
965
|
-
throw new PaymentProviderError(
|
|
966
|
-
`Invalid JSON-RPC hex quantity: ${String(value)}`
|
|
967
|
-
);
|
|
968
|
-
}
|
|
969
|
-
const normalized = value.trim();
|
|
970
|
-
if (!/^0x[0-9a-fA-F]+$/.test(normalized)) {
|
|
971
|
-
throw new PaymentProviderError(
|
|
972
|
-
`Invalid JSON-RPC hex quantity: ${String(value)}`
|
|
973
|
-
);
|
|
974
|
-
}
|
|
975
|
-
return BigInt(normalized);
|
|
661
|
+
if (typeof value === "bigint") {
|
|
662
|
+
if (value < 0n) throw new PaymentProviderError(`Invalid JSON-RPC hex quantity: ${String(value)}`);
|
|
663
|
+
return value;
|
|
664
|
+
}
|
|
665
|
+
if (typeof value === "number") {
|
|
666
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new PaymentProviderError(`Invalid JSON-RPC hex quantity: ${String(value)}`);
|
|
667
|
+
return BigInt(value);
|
|
668
|
+
}
|
|
669
|
+
if (typeof value !== "string") throw new PaymentProviderError(`Invalid JSON-RPC hex quantity: ${String(value)}`);
|
|
670
|
+
const normalized = value.trim();
|
|
671
|
+
if (!/^0x[0-9a-fA-F]+$/.test(normalized)) throw new PaymentProviderError(`Invalid JSON-RPC hex quantity: ${String(value)}`);
|
|
672
|
+
return BigInt(normalized);
|
|
976
673
|
}
|
|
977
674
|
function toHexQuantity(value) {
|
|
978
|
-
|
|
675
|
+
return `0x${value.toString(16)}`;
|
|
979
676
|
}
|
|
980
677
|
function decodePaymentHeader(value) {
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
Buffer.from(
|
|
987
|
-
trimmed.replace(/-/g, "+").replace(/_/g, "/").padEnd(trimmed.length + (4 - trimmed.length % 4) % 4, "="),
|
|
988
|
-
"base64"
|
|
989
|
-
).toString("utf8")
|
|
990
|
-
);
|
|
991
|
-
if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) {
|
|
992
|
-
throw new PaymentVerificationError(
|
|
993
|
-
"x402 payment header must be an object."
|
|
994
|
-
);
|
|
995
|
-
}
|
|
996
|
-
return decoded;
|
|
678
|
+
const trimmed = value.trim();
|
|
679
|
+
if (trimmed.length > 65536) throw new PaymentVerificationError("x402 payment header is too large.");
|
|
680
|
+
const decoded = trimmed.startsWith("{") ? JSON.parse(trimmed) : JSON.parse(Buffer.from(trimmed.replace(/-/g, "+").replace(/_/g, "/").padEnd(trimmed.length + (4 - trimmed.length % 4) % 4, "="), "base64").toString("utf8"));
|
|
681
|
+
if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) throw new PaymentVerificationError("x402 payment header must be an object.");
|
|
682
|
+
return decoded;
|
|
997
683
|
}
|
|
998
684
|
function validateX402PayloadRequirements(payload, input) {
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
}
|
|
1035
|
-
if (actualMethod.toUpperCase() !== expectedMethod.toUpperCase()) {
|
|
1036
|
-
return "x402 proof method did not match the requested method.";
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
const actualExpiry = readX402PayloadString(payload, [
|
|
1040
|
-
"expiresAt",
|
|
1041
|
-
"expires",
|
|
1042
|
-
"validBefore",
|
|
1043
|
-
"deadline"
|
|
1044
|
-
]);
|
|
1045
|
-
const actualExpiryDate = actualExpiry === void 0 ? void 0 : parseX402ExpiryDate(actualExpiry);
|
|
1046
|
-
if (actualExpiry !== void 0 && !actualExpiryDate) {
|
|
1047
|
-
return "x402 proof expiry was invalid.";
|
|
1048
|
-
}
|
|
1049
|
-
if (actualExpiryDate !== void 0 && actualExpiryDate < /* @__PURE__ */ new Date()) {
|
|
1050
|
-
return "x402 proof is expired.";
|
|
1051
|
-
}
|
|
1052
|
-
if (input.expiresAt !== void 0) {
|
|
1053
|
-
const expectedExpiry = normalizeDate(input.expiresAt);
|
|
1054
|
-
if (expectedExpiry < /* @__PURE__ */ new Date()) {
|
|
1055
|
-
return "x402 proof is expired.";
|
|
1056
|
-
}
|
|
1057
|
-
if (!actualExpiry) {
|
|
1058
|
-
return "x402 proof did not include an expiry.";
|
|
1059
|
-
}
|
|
1060
|
-
if (actualExpiryDate?.getTime() !== expectedExpiry.getTime()) {
|
|
1061
|
-
return "x402 proof expiry did not match the requested expiry.";
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
return void 0;
|
|
685
|
+
const expectedScheme = input.scheme ?? "exact";
|
|
686
|
+
const actualScheme = readX402PayloadString(payload, ["scheme"]);
|
|
687
|
+
if (!actualScheme) return "x402 proof did not include the payment scheme.";
|
|
688
|
+
if (actualScheme && actualScheme !== expectedScheme) return `x402 proof scheme ${actualScheme} did not match ${expectedScheme}.`;
|
|
689
|
+
const expectedNetwork = normalizeX402Network(input.network ?? "eip155:8453");
|
|
690
|
+
const actualNetwork = normalizeX402Network(readX402PayloadString(payload, ["network", "networkId"]));
|
|
691
|
+
if (!actualNetwork) return "x402 proof did not include the payment network.";
|
|
692
|
+
if (actualNetwork && actualNetwork !== expectedNetwork) return `x402 proof network ${actualNetwork} did not match ${expectedNetwork}.`;
|
|
693
|
+
const expectedResource = input.resource;
|
|
694
|
+
if (expectedResource !== void 0) {
|
|
695
|
+
const actualResource = readX402PayloadString(payload, ["resource"]);
|
|
696
|
+
if (!actualResource) return "x402 proof did not include the paid resource.";
|
|
697
|
+
if (actualResource !== expectedResource) return "x402 proof resource did not match the requested resource.";
|
|
698
|
+
}
|
|
699
|
+
const expectedMethod = input.method;
|
|
700
|
+
if (expectedMethod !== void 0) {
|
|
701
|
+
const actualMethod = readX402PayloadString(payload, ["method"]);
|
|
702
|
+
if (!actualMethod) return "x402 proof did not include the HTTP method.";
|
|
703
|
+
if (actualMethod.toUpperCase() !== expectedMethod.toUpperCase()) return "x402 proof method did not match the requested method.";
|
|
704
|
+
}
|
|
705
|
+
const actualExpiry = readX402PayloadString(payload, [
|
|
706
|
+
"expiresAt",
|
|
707
|
+
"expires",
|
|
708
|
+
"validBefore",
|
|
709
|
+
"deadline"
|
|
710
|
+
]);
|
|
711
|
+
const actualExpiryDate = actualExpiry === void 0 ? void 0 : parseX402ExpiryDate(actualExpiry);
|
|
712
|
+
if (actualExpiry !== void 0 && !actualExpiryDate) return "x402 proof expiry was invalid.";
|
|
713
|
+
if (actualExpiryDate !== void 0 && actualExpiryDate < /* @__PURE__ */ new Date()) return "x402 proof is expired.";
|
|
714
|
+
if (input.expiresAt !== void 0) {
|
|
715
|
+
const expectedExpiry = normalizeDate(input.expiresAt);
|
|
716
|
+
if (expectedExpiry < /* @__PURE__ */ new Date()) return "x402 proof is expired.";
|
|
717
|
+
if (!actualExpiry) return "x402 proof did not include an expiry.";
|
|
718
|
+
if (actualExpiryDate?.getTime() !== expectedExpiry.getTime()) return "x402 proof expiry did not match the requested expiry.";
|
|
719
|
+
}
|
|
1065
720
|
}
|
|
1066
721
|
function parseX402ExpiryDate(value) {
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
return normalizeDate(value);
|
|
1079
|
-
} catch {
|
|
1080
|
-
return void 0;
|
|
1081
|
-
}
|
|
722
|
+
try {
|
|
723
|
+
if (/^\d+$/.test(value)) {
|
|
724
|
+
const timestamp = Number(value);
|
|
725
|
+
if (!Number.isSafeInteger(timestamp)) return;
|
|
726
|
+
const date = new Date(timestamp < 0xe8d4a51000 ? timestamp * 1e3 : timestamp);
|
|
727
|
+
return Number.isNaN(date.getTime()) ? void 0 : date;
|
|
728
|
+
}
|
|
729
|
+
return normalizeDate(value);
|
|
730
|
+
} catch {
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
1082
733
|
}
|
|
1083
734
|
function normalizeX402Network(value) {
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
735
|
+
if (value === void 0) return;
|
|
736
|
+
const normalized = value.trim().toLowerCase();
|
|
737
|
+
if ([
|
|
738
|
+
"base",
|
|
739
|
+
"base-mainnet",
|
|
740
|
+
"eip155:8453"
|
|
741
|
+
].includes(normalized)) return BASE_MAINNET_CAIP2;
|
|
742
|
+
return value;
|
|
1092
743
|
}
|
|
1093
744
|
function readX402PayloadString(value, keys) {
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
}
|
|
1106
|
-
const direct = readLooseString(root, key);
|
|
1107
|
-
if (direct) {
|
|
1108
|
-
return direct;
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
return void 0;
|
|
745
|
+
const root = asRecord(value);
|
|
746
|
+
const nestedPayload = asRecord(root?.payload);
|
|
747
|
+
const authorization = asRecord(nestedPayload?.authorization);
|
|
748
|
+
for (const key of keys) {
|
|
749
|
+
const authorized = readLooseString(authorization, key);
|
|
750
|
+
if (authorized) return authorized;
|
|
751
|
+
const nested = readLooseString(nestedPayload, key);
|
|
752
|
+
if (nested) return nested;
|
|
753
|
+
const direct = readLooseString(root, key);
|
|
754
|
+
if (direct) return direct;
|
|
755
|
+
}
|
|
1112
756
|
}
|
|
1113
757
|
function asRecord(value) {
|
|
1114
|
-
|
|
758
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
1115
759
|
}
|
|
1116
760
|
function readLooseString(value, key) {
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
}
|
|
1121
|
-
if (typeof item === "number") {
|
|
1122
|
-
return String(item);
|
|
1123
|
-
}
|
|
1124
|
-
return void 0;
|
|
761
|
+
const item = value?.[key];
|
|
762
|
+
if (typeof item === "string") return item;
|
|
763
|
+
if (typeof item === "number") return String(item);
|
|
1125
764
|
}
|
|
1126
765
|
function findNestedString(value, keys, depth = 0) {
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
for (const child of Object.values(record)) {
|
|
1141
|
-
const match = findNestedString(child, keys, depth + 1);
|
|
1142
|
-
if (match) {
|
|
1143
|
-
return match;
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
return void 0;
|
|
766
|
+
if (!value || typeof value !== "object" || depth > 16) return;
|
|
767
|
+
const record = value;
|
|
768
|
+
for (const key of keys) {
|
|
769
|
+
const direct = record[key];
|
|
770
|
+
if (typeof direct === "string" && direct.length > 0) return direct;
|
|
771
|
+
if (typeof direct === "number" && Number.isFinite(direct)) return String(direct);
|
|
772
|
+
}
|
|
773
|
+
for (const child of Object.values(record)) {
|
|
774
|
+
const match = findNestedString(child, keys, depth + 1);
|
|
775
|
+
if (match) return match;
|
|
776
|
+
}
|
|
1147
777
|
}
|
|
1148
778
|
function bigintToFixedBytes(value) {
|
|
1149
|
-
|
|
1150
|
-
return hexToBytes(hex);
|
|
779
|
+
return hexToBytes(value.toString(16).padStart(64, "0"));
|
|
1151
780
|
}
|
|
1152
781
|
function concatBytes(...values) {
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
782
|
+
const totalLength = values.reduce((sum, value) => sum + value.length, 0);
|
|
783
|
+
const result = new Uint8Array(totalLength);
|
|
784
|
+
let offset = 0;
|
|
785
|
+
for (const value of values) {
|
|
786
|
+
result.set(value, offset);
|
|
787
|
+
offset += value.length;
|
|
788
|
+
}
|
|
789
|
+
return result;
|
|
1161
790
|
}
|
|
1162
791
|
function bytesToHex(value) {
|
|
1163
|
-
|
|
1164
|
-
""
|
|
1165
|
-
);
|
|
792
|
+
return Array.from(value, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1166
793
|
}
|
|
1167
794
|
function hexToBytes(value) {
|
|
1168
|
-
|
|
1169
|
-
|
|
795
|
+
const bytes = value.match(/.{1,2}/g) ?? [];
|
|
796
|
+
return Uint8Array.from(bytes.map((byte) => Number.parseInt(byte, 16)));
|
|
1170
797
|
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
BASE_USDC_CONTRACT,
|
|
1176
|
-
BaseUsdcAdapter,
|
|
1177
|
-
USDC_DECIMALS,
|
|
1178
|
-
deriveBaseUsdcAddress,
|
|
1179
|
-
quoteIdToDerivationIndex,
|
|
1180
|
-
quoteIdToDerivationIndexes
|
|
1181
|
-
};
|
|
1182
|
-
//# sourceMappingURL=base-usdc.js.map
|
|
798
|
+
//#endregion
|
|
799
|
+
export { BASE_MAINNET_CAIP2, BASE_MAINNET_CHAIN_ID, BASE_USDC_BACKEND_ID, BASE_USDC_CONTRACT, BaseUsdcAdapter, USDC_DECIMALS, deriveBaseUsdcAddress, quoteIdToDerivationIndex, quoteIdToDerivationIndexes };
|
|
800
|
+
|
|
801
|
+
//# sourceMappingURL=base-usdc.js.map
|