@happyvertical/payments 0.74.8
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/AGENT.md +33 -0
- package/LICENSE +7 -0
- package/README.md +97 -0
- package/dist/adapters/base-usdc.d.ts +85 -0
- package/dist/adapters/base-usdc.d.ts.map +1 -0
- package/dist/adapters/base-usdc.js +1182 -0
- package/dist/adapters/base-usdc.js.map +1 -0
- package/dist/adapters/btc.d.ts +63 -0
- package/dist/adapters/btc.d.ts.map +1 -0
- package/dist/adapters/btc.js +843 -0
- package/dist/adapters/btc.js.map +1 -0
- package/dist/adapters/stripe.d.ts +54 -0
- package/dist/adapters/stripe.d.ts.map +1 -0
- package/dist/adapters/stripe.js +696 -0
- package/dist/adapters/stripe.js.map +1 -0
- package/dist/chunks/errors-BgFC46qQ.js +45 -0
- package/dist/chunks/errors-BgFC46qQ.js.map +1 -0
- package/dist/chunks/shared-DGHSqDQT.js +392 -0
- package/dist/chunks/shared-DGHSqDQT.js.map +1 -0
- package/dist/errors.d.ts +31 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/factory.d.ts +13 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/shared.d.ts +32 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/testing/conformance.d.ts +33 -0
- package/dist/testing/conformance.d.ts.map +1 -0
- package/dist/testing/conformance.js +114 -0
- package/dist/testing/conformance.js.map +1 -0
- package/dist/types.d.ts +174 -0
- package/dist/types.d.ts.map +1 -0
- package/metadata.json +30 -0
- package/package.json +88 -0
|
@@ -0,0 +1,1182 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { P as PaymentConfigurationError, b as PaymentProviderError, d as PaymentVerificationError } from "../chunks/errors-BgFC46qQ.js";
|
|
3
|
+
import { n as normalizeNonEmptyString, g as getFetch, a as normalizeMaxStoredPaymentOptions, b as normalizeUrlString, c as normalizeFutureDate, d as normalizeCurrency, e as normalizePositiveMinorUnitAmount, f as decimalToMinorUnitAmount, r as rememberPaymentOption, p as pollPaymentStatus, h as normalizeDate, i as normalizeMinorUnitAmount, j as readJsonResponse, m as minorUnitsToDecimal, k as currencyMinorUnitDecimals, l as decimalToAtomicUnits } from "../chunks/shared-DGHSqDQT.js";
|
|
4
|
+
const BASE_USDC_BACKEND_ID = "base-usdc";
|
|
5
|
+
const BASE_MAINNET_CHAIN_ID = 8453;
|
|
6
|
+
const BASE_MAINNET_CAIP2 = "eip155:8453";
|
|
7
|
+
const BASE_USDC_CONTRACT = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
8
|
+
const USDC_DECIMALS = 6;
|
|
9
|
+
const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
10
|
+
const DEFAULT_LOG_LOOKBACK_BLOCKS = 10000n;
|
|
11
|
+
const DEFAULT_MAX_SEEN_X402_TRANSACTION_IDS = 5e4;
|
|
12
|
+
class BaseUsdcAdapter {
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this.options = options;
|
|
15
|
+
if (typeof options.rpcUrl !== "string" || !options.rpcUrl.trim()) {
|
|
16
|
+
throw new PaymentConfigurationError(
|
|
17
|
+
"BaseUsdcAdapter requires an RPC URL."
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
this.masterXpub = options.masterXpub === void 0 ? void 0 : normalizeNonEmptyString(options.masterXpub, "Base USDC masterXpub");
|
|
21
|
+
this.derivationPathPrefix = normalizeDerivationPathPrefix(
|
|
22
|
+
options.derivationPathPrefix ?? "m/0"
|
|
23
|
+
);
|
|
24
|
+
if (!this.masterXpub && !options.addressDeriver) {
|
|
25
|
+
throw new PaymentConfigurationError(
|
|
26
|
+
"BaseUsdcAdapter requires masterXpub or addressDeriver."
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
this.fetch = getFetch(options.fetch);
|
|
30
|
+
this.maxStoredPaymentOptions = normalizeMaxStoredPaymentOptions(
|
|
31
|
+
options.maxStoredPaymentOptions,
|
|
32
|
+
"BaseUsdcAdapter maxStoredPaymentOptions"
|
|
33
|
+
);
|
|
34
|
+
this.allowCumulativeTransferMatching = options.allowCumulativeTransferMatching === true;
|
|
35
|
+
this.rpcUrl = normalizeUrlString(options.rpcUrl, "BaseUsdcAdapter rpcUrl");
|
|
36
|
+
this.usdcContractAddress = normalizeAddress(
|
|
37
|
+
options.usdcContractAddress ?? BASE_USDC_CONTRACT
|
|
38
|
+
);
|
|
39
|
+
this.confirmations = normalizeConfirmationCount(
|
|
40
|
+
options.confirmations ?? 1,
|
|
41
|
+
"BaseUsdcAdapter confirmations"
|
|
42
|
+
);
|
|
43
|
+
this.configuredFromBlock = options.fromBlock === void 0 ? void 0 : normalizeFromBlock(options.fromBlock);
|
|
44
|
+
this.x402FacilitatorUrl = options.x402FacilitatorUrl === void 0 ? void 0 : normalizeUrlString(
|
|
45
|
+
options.x402FacilitatorUrl,
|
|
46
|
+
"BaseUsdcAdapter x402FacilitatorUrl"
|
|
47
|
+
).replace(/\/$/, "");
|
|
48
|
+
this.capabilities = {
|
|
49
|
+
id: BASE_USDC_BACKEND_ID,
|
|
50
|
+
displayName: "Base USDC",
|
|
51
|
+
settlementCurrency: "USDC",
|
|
52
|
+
supportedSettlementCurrencies: ["USDC"],
|
|
53
|
+
chain: "base",
|
|
54
|
+
settlementShape: "address",
|
|
55
|
+
x402Capable: true,
|
|
56
|
+
confirmationLatency: {
|
|
57
|
+
expectedSeconds: 2,
|
|
58
|
+
minConfirmations: this.confirmations,
|
|
59
|
+
description: "Base L2 USDC transfer confirmation"
|
|
60
|
+
},
|
|
61
|
+
supportsRefunds: true,
|
|
62
|
+
supportsPayouts: true,
|
|
63
|
+
supportsWebhooks: false,
|
|
64
|
+
metadata: {
|
|
65
|
+
chainId: BASE_MAINNET_CHAIN_ID,
|
|
66
|
+
caip2Network: BASE_MAINNET_CAIP2,
|
|
67
|
+
usdcContractAddress: this.usdcContractAddress
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
capabilities;
|
|
72
|
+
fetch;
|
|
73
|
+
rpcUrl;
|
|
74
|
+
usdcContractAddress;
|
|
75
|
+
confirmations;
|
|
76
|
+
configuredFromBlock;
|
|
77
|
+
masterXpub;
|
|
78
|
+
derivationPathPrefix;
|
|
79
|
+
x402FacilitatorUrl;
|
|
80
|
+
maxStoredPaymentOptions;
|
|
81
|
+
allowCumulativeTransferMatching;
|
|
82
|
+
optionsByQuote = /* @__PURE__ */ new Map();
|
|
83
|
+
seenX402TransactionIds = /* @__PURE__ */ new Set();
|
|
84
|
+
rpcId = 0;
|
|
85
|
+
async createPaymentOption(input) {
|
|
86
|
+
const quoteId = normalizeNonEmptyString(input.quoteId, "Base USDC quoteId");
|
|
87
|
+
const expiresAt = normalizeFutureDate(input.expiresAt, "Base USDC expiry");
|
|
88
|
+
const currency = normalizeCurrency(input.currency, "Base USDC");
|
|
89
|
+
const amount = normalizePositiveMinorUnitAmount(
|
|
90
|
+
input.amount,
|
|
91
|
+
"Base USDC amount"
|
|
92
|
+
);
|
|
93
|
+
const expectedAmountAtomic = amountToUsdcAtomicUnits(
|
|
94
|
+
amount,
|
|
95
|
+
currency,
|
|
96
|
+
"Base USDC amount"
|
|
97
|
+
);
|
|
98
|
+
const settlementAmount = decimalToMinorUnitAmount(
|
|
99
|
+
BigInt(expectedAmountAtomic),
|
|
100
|
+
USDC_DECIMALS,
|
|
101
|
+
"Base USDC settlement"
|
|
102
|
+
);
|
|
103
|
+
const configuredIndex = this.options.addressIndexForQuote?.(quoteId);
|
|
104
|
+
const indexes = configuredIndex === void 0 ? quoteIdToDerivationIndexes(quoteId) : [normalizeDerivationIndex(configuredIndex)];
|
|
105
|
+
const index = indexes[0] ?? 0;
|
|
106
|
+
const path = `${this.derivationPathPrefix}/${indexes.join("/")}`;
|
|
107
|
+
const payTo = normalizeAddress(
|
|
108
|
+
await this.deriveAddress({
|
|
109
|
+
quoteId,
|
|
110
|
+
index,
|
|
111
|
+
indexes,
|
|
112
|
+
path
|
|
113
|
+
})
|
|
114
|
+
);
|
|
115
|
+
const searchStartBlock = await this.getCurrentBlockNumber();
|
|
116
|
+
const option = {
|
|
117
|
+
backendId: this.capabilities.id,
|
|
118
|
+
quoteId,
|
|
119
|
+
payTo,
|
|
120
|
+
settlementShape: "address",
|
|
121
|
+
settlementCurrency: "USDC",
|
|
122
|
+
settlementAmount,
|
|
123
|
+
amount,
|
|
124
|
+
currency,
|
|
125
|
+
expiresAt,
|
|
126
|
+
paymentUri: buildUsdcPaymentUri({
|
|
127
|
+
tokenAddress: this.usdcContractAddress,
|
|
128
|
+
payTo,
|
|
129
|
+
amountAtomic: expectedAmountAtomic
|
|
130
|
+
}),
|
|
131
|
+
metadata: {
|
|
132
|
+
derivationIndex: index,
|
|
133
|
+
derivationIndexes: indexes,
|
|
134
|
+
derivationPath: path,
|
|
135
|
+
caip2Network: BASE_MAINNET_CAIP2,
|
|
136
|
+
tokenAddress: this.usdcContractAddress,
|
|
137
|
+
searchStartBlock: toHexQuantity(searchStartBlock)
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
rememberPaymentOption(
|
|
141
|
+
this.optionsByQuote,
|
|
142
|
+
quoteId,
|
|
143
|
+
{ ...option, expectedAmountAtomic, searchStartBlock },
|
|
144
|
+
this.maxStoredPaymentOptions
|
|
145
|
+
);
|
|
146
|
+
return option;
|
|
147
|
+
}
|
|
148
|
+
watchPayment(input) {
|
|
149
|
+
return pollPaymentStatus(
|
|
150
|
+
{
|
|
151
|
+
...input,
|
|
152
|
+
pollIntervalMs: input.pollIntervalMs ?? this.options.pollIntervalMs ?? 2e3
|
|
153
|
+
},
|
|
154
|
+
() => this.getStatus(input.quoteId, input.payTo, input.statusContext)
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
async getStatus(quoteId, payTo, context = {}) {
|
|
158
|
+
const normalizedQuoteId = normalizeNonEmptyString(
|
|
159
|
+
quoteId,
|
|
160
|
+
"Base USDC quoteId"
|
|
161
|
+
);
|
|
162
|
+
const normalizedPayTo = normalizeAddress(payTo);
|
|
163
|
+
const option = this.optionsByQuote.get(normalizedQuoteId);
|
|
164
|
+
const expectedAmountAtomic = resolveExpectedUsdcAmount(option, context);
|
|
165
|
+
const contextSearchStartBlock = context.searchStartBlock === void 0 ? void 0 : parseHexQuantity(normalizeFromBlock(context.searchStartBlock));
|
|
166
|
+
const searchStartBlock = option?.searchStartBlock ?? contextSearchStartBlock;
|
|
167
|
+
if (!option && expectedAmountAtomic !== void 0 && searchStartBlock === void 0 && this.configuredFromBlock === void 0) {
|
|
168
|
+
throw new PaymentConfigurationError(
|
|
169
|
+
"Base USDC getStatus requires statusContext.searchStartBlock when checking stateless payment status without a configured fromBlock."
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
const logs = await this.getTransferLogs(normalizedPayTo, searchStartBlock);
|
|
173
|
+
const matchMinBlock = searchStartBlock ?? configuredFromBlockMinBlock(this.configuredFromBlock);
|
|
174
|
+
const matchingTransfer = expectedAmountAtomic === void 0 ? void 0 : this.allowCumulativeTransferMatching ? pickCumulativeTransferMatch(
|
|
175
|
+
logs,
|
|
176
|
+
expectedAmountAtomic,
|
|
177
|
+
void 0,
|
|
178
|
+
matchMinBlock
|
|
179
|
+
) : pickSingleTransferMatch(
|
|
180
|
+
logs,
|
|
181
|
+
expectedAmountAtomic,
|
|
182
|
+
void 0,
|
|
183
|
+
matchMinBlock
|
|
184
|
+
);
|
|
185
|
+
if (!matchingTransfer) {
|
|
186
|
+
const expiresAt = option?.expiresAt ?? (context.expiresAt === void 0 ? void 0 : normalizeDate(context.expiresAt));
|
|
187
|
+
return {
|
|
188
|
+
backendId: this.capabilities.id,
|
|
189
|
+
quoteId: normalizedQuoteId,
|
|
190
|
+
payTo: normalizedPayTo,
|
|
191
|
+
status: expiresAt && expiresAt < /* @__PURE__ */ new Date() ? "expired" : "pending",
|
|
192
|
+
settlementCurrency: "USDC",
|
|
193
|
+
settlementAmount: option?.settlementAmount ?? (context.settlementAmount === void 0 ? void 0 : normalizeMinorUnitAmount(
|
|
194
|
+
context.settlementAmount,
|
|
195
|
+
"Base USDC settlement status"
|
|
196
|
+
)),
|
|
197
|
+
amount: option?.amount ?? (context.amount === void 0 ? void 0 : normalizeMinorUnitAmount(context.amount, "Base USDC status")),
|
|
198
|
+
currency: option?.currency ?? (context.currency === void 0 ? void 0 : normalizeCurrency(context.currency, "Base USDC status")),
|
|
199
|
+
requiredConfirmations: this.confirmations,
|
|
200
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const currentBlock = await this.getCurrentBlockNumber();
|
|
204
|
+
const blockNumber = readLogBlockNumber(matchingTransfer.latestLog);
|
|
205
|
+
const confirmations = calculateConfirmations(currentBlock, blockNumber);
|
|
206
|
+
const requiredConfirmations = this.confirmations;
|
|
207
|
+
const receivedAmountAtomic = decimalToMinorUnitAmount(
|
|
208
|
+
matchingTransfer.amountAtomic,
|
|
209
|
+
USDC_DECIMALS,
|
|
210
|
+
"Base USDC received"
|
|
211
|
+
);
|
|
212
|
+
return {
|
|
213
|
+
backendId: this.capabilities.id,
|
|
214
|
+
quoteId: normalizedQuoteId,
|
|
215
|
+
payTo: normalizedPayTo,
|
|
216
|
+
status: confirmations >= requiredConfirmations ? "confirmed" : "processing",
|
|
217
|
+
settlementCurrency: "USDC",
|
|
218
|
+
settlementAmount: option?.settlementAmount ?? (context.settlementAmount === void 0 ? void 0 : normalizeMinorUnitAmount(
|
|
219
|
+
context.settlementAmount,
|
|
220
|
+
"Base USDC settlement status"
|
|
221
|
+
)),
|
|
222
|
+
receivedAmount: receivedAmountAtomic,
|
|
223
|
+
amount: option?.amount ?? (context.amount === void 0 ? void 0 : normalizeMinorUnitAmount(context.amount, "Base USDC status")),
|
|
224
|
+
currency: option?.currency ?? (context.currency === void 0 ? void 0 : normalizeCurrency(context.currency, "Base USDC status")),
|
|
225
|
+
requiredConfirmations,
|
|
226
|
+
confirmations,
|
|
227
|
+
transactionId: matchingTransfer.latestLog.transactionHash,
|
|
228
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
229
|
+
raw: matchingTransfer.latestLog
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
async verifyX402Proof(input) {
|
|
233
|
+
const quoteId = normalizeNonEmptyString(input.quoteId, "Base USDC quoteId");
|
|
234
|
+
let payload;
|
|
235
|
+
try {
|
|
236
|
+
payload = decodePaymentHeader(input.paymentHeader);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
return {
|
|
239
|
+
valid: false,
|
|
240
|
+
backendId: this.capabilities.id,
|
|
241
|
+
quoteId,
|
|
242
|
+
payTo: input.payTo,
|
|
243
|
+
reason: "x402 payment header could not be decoded.",
|
|
244
|
+
raw: {
|
|
245
|
+
paymentHeader: input.paymentHeader,
|
|
246
|
+
error: error instanceof Error ? error.message : String(error)
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const transactionId = readX402PayloadString(payload, [
|
|
251
|
+
"transactionHash",
|
|
252
|
+
"txHash",
|
|
253
|
+
"tx",
|
|
254
|
+
"hash"
|
|
255
|
+
])?.trim();
|
|
256
|
+
if (this.x402FacilitatorUrl) {
|
|
257
|
+
return this.verifyWithFacilitator(input, payload);
|
|
258
|
+
}
|
|
259
|
+
if (!transactionId) {
|
|
260
|
+
return {
|
|
261
|
+
valid: false,
|
|
262
|
+
backendId: this.capabilities.id,
|
|
263
|
+
quoteId,
|
|
264
|
+
payTo: input.payTo,
|
|
265
|
+
reason: "x402 proof did not include a transaction hash and no facilitator URL is configured.",
|
|
266
|
+
raw: payload
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
if (!isEvmTransactionHash(transactionId)) {
|
|
270
|
+
return {
|
|
271
|
+
valid: false,
|
|
272
|
+
backendId: this.capabilities.id,
|
|
273
|
+
quoteId,
|
|
274
|
+
payTo: input.payTo,
|
|
275
|
+
transactionId,
|
|
276
|
+
reason: "x402 proof transaction hash was invalid.",
|
|
277
|
+
raw: payload
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
const requirementError = validateX402PayloadRequirements(payload, input);
|
|
281
|
+
if (requirementError) {
|
|
282
|
+
return {
|
|
283
|
+
valid: false,
|
|
284
|
+
backendId: this.capabilities.id,
|
|
285
|
+
quoteId,
|
|
286
|
+
payTo: input.payTo,
|
|
287
|
+
transactionId,
|
|
288
|
+
reason: requirementError,
|
|
289
|
+
raw: payload
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
const expectedAmountAtomic = amountToUsdcAtomicUnits(
|
|
293
|
+
normalizePositiveMinorUnitAmount(input.amount, "Base USDC x402 amount"),
|
|
294
|
+
normalizeCurrency(input.currency, "Base USDC x402"),
|
|
295
|
+
"Base USDC x402 amount"
|
|
296
|
+
);
|
|
297
|
+
return this.verifyX402Receipt(
|
|
298
|
+
input,
|
|
299
|
+
payload,
|
|
300
|
+
quoteId,
|
|
301
|
+
transactionId,
|
|
302
|
+
expectedAmountAtomic
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
async sendPayout(input) {
|
|
306
|
+
if (input.currency !== void 0) {
|
|
307
|
+
const currency = normalizeNonEmptyString(
|
|
308
|
+
input.currency,
|
|
309
|
+
"Base USDC payout currency"
|
|
310
|
+
);
|
|
311
|
+
if (currency.toUpperCase() !== "USDC") {
|
|
312
|
+
throw new PaymentConfigurationError(
|
|
313
|
+
`BaseUsdcAdapter can only send USDC payouts, received ${input.currency}.`
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (!this.options.sendTransaction) {
|
|
318
|
+
throw new PaymentConfigurationError(
|
|
319
|
+
"BaseUsdcAdapter sendPayout requires a sendTransaction callback."
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
const amount = normalizePositiveMinorUnitAmount(
|
|
323
|
+
input.amount,
|
|
324
|
+
"Base USDC payout amount"
|
|
325
|
+
);
|
|
326
|
+
const amountAtomic = String(amount);
|
|
327
|
+
const destination = normalizeAddress(input.destination);
|
|
328
|
+
const quoteId = input.quoteId === void 0 ? void 0 : normalizeNonEmptyString(input.quoteId, "Base USDC payout quoteId");
|
|
329
|
+
const idempotencyKey = input.idempotencyKey === void 0 ? void 0 : normalizeNonEmptyString(
|
|
330
|
+
input.idempotencyKey,
|
|
331
|
+
"Base USDC payout idempotencyKey"
|
|
332
|
+
);
|
|
333
|
+
const result = await this.options.sendTransaction({
|
|
334
|
+
destination,
|
|
335
|
+
amount,
|
|
336
|
+
amountAtomic,
|
|
337
|
+
currency: "USDC",
|
|
338
|
+
tokenAddress: this.usdcContractAddress,
|
|
339
|
+
memo: input.memo,
|
|
340
|
+
quoteId,
|
|
341
|
+
idempotencyKey,
|
|
342
|
+
metadata: input.metadata
|
|
343
|
+
});
|
|
344
|
+
return {
|
|
345
|
+
backendId: this.capabilities.id,
|
|
346
|
+
status: "submitted",
|
|
347
|
+
transactionId: result.transactionId,
|
|
348
|
+
destination,
|
|
349
|
+
amount,
|
|
350
|
+
currency: "USDC",
|
|
351
|
+
raw: result.raw
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
async refundPayment(input) {
|
|
355
|
+
if (input.currency !== void 0) {
|
|
356
|
+
const currency = normalizeNonEmptyString(
|
|
357
|
+
input.currency,
|
|
358
|
+
"Base USDC refund currency"
|
|
359
|
+
);
|
|
360
|
+
if (currency.toUpperCase() !== "USDC") {
|
|
361
|
+
throw new PaymentConfigurationError(
|
|
362
|
+
`BaseUsdcAdapter refunds require USDC currency when specified, received ${input.currency}.`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (!input.destination) {
|
|
367
|
+
throw new PaymentConfigurationError(
|
|
368
|
+
"Base USDC refunds require a destination payer address."
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
if (input.amount === void 0) {
|
|
372
|
+
throw new PaymentConfigurationError(
|
|
373
|
+
"Base USDC refunds require an amount."
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
const payout = await this.sendPayout({
|
|
377
|
+
destination: input.destination,
|
|
378
|
+
amount: input.amount,
|
|
379
|
+
currency: "USDC",
|
|
380
|
+
idempotencyKey: input.idempotencyKey,
|
|
381
|
+
memo: input.reason,
|
|
382
|
+
metadata: input.metadata
|
|
383
|
+
});
|
|
384
|
+
return {
|
|
385
|
+
backendId: this.capabilities.id,
|
|
386
|
+
status: "submitted",
|
|
387
|
+
transactionId: payout.transactionId,
|
|
388
|
+
amount: payout.amount,
|
|
389
|
+
currency: payout.currency,
|
|
390
|
+
raw: payout.raw
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
async deriveAddress(input) {
|
|
394
|
+
if (this.options.addressDeriver) {
|
|
395
|
+
return this.options.addressDeriver(input);
|
|
396
|
+
}
|
|
397
|
+
if (!this.masterXpub) {
|
|
398
|
+
throw new PaymentConfigurationError(
|
|
399
|
+
"BaseUsdcAdapter requires masterXpub or addressDeriver."
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
return deriveBaseUsdcAddress({
|
|
403
|
+
masterXpub: this.masterXpub,
|
|
404
|
+
path: input.path
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
async getTransferLogs(payTo, requiredFromBlock) {
|
|
408
|
+
const fromBlock = this.configuredFromBlock ?? await this.getDefaultFromBlock(requiredFromBlock);
|
|
409
|
+
const logs = await this.rpc("eth_getLogs", [
|
|
410
|
+
{
|
|
411
|
+
address: this.usdcContractAddress,
|
|
412
|
+
fromBlock,
|
|
413
|
+
toBlock: "latest",
|
|
414
|
+
topics: [TRANSFER_TOPIC, null, addressToTopic(payTo)]
|
|
415
|
+
}
|
|
416
|
+
]);
|
|
417
|
+
return Array.isArray(logs) ? logs : [];
|
|
418
|
+
}
|
|
419
|
+
async getDefaultFromBlock(requiredFromBlock) {
|
|
420
|
+
const currentBlock = await this.getCurrentBlockNumber();
|
|
421
|
+
const lookbackStartBlock = currentBlock > DEFAULT_LOG_LOOKBACK_BLOCKS ? currentBlock - DEFAULT_LOG_LOOKBACK_BLOCKS : 0n;
|
|
422
|
+
const startBlock = requiredFromBlock ?? lookbackStartBlock;
|
|
423
|
+
return toHexQuantity(startBlock);
|
|
424
|
+
}
|
|
425
|
+
async getCurrentBlockNumber() {
|
|
426
|
+
return parseHexQuantity(await this.rpc("eth_blockNumber", []));
|
|
427
|
+
}
|
|
428
|
+
async rpc(method, params) {
|
|
429
|
+
const response = await this.fetch(this.rpcUrl, {
|
|
430
|
+
method: "POST",
|
|
431
|
+
headers: { "Content-Type": "application/json" },
|
|
432
|
+
body: JSON.stringify({
|
|
433
|
+
jsonrpc: "2.0",
|
|
434
|
+
id: ++this.rpcId,
|
|
435
|
+
method,
|
|
436
|
+
params
|
|
437
|
+
})
|
|
438
|
+
});
|
|
439
|
+
const json = await readJsonResponse(
|
|
440
|
+
response,
|
|
441
|
+
`Base RPC ${method}`
|
|
442
|
+
);
|
|
443
|
+
if (json.error) {
|
|
444
|
+
throw new PaymentProviderError(
|
|
445
|
+
`Base RPC ${method}: ${json.error.message}`,
|
|
446
|
+
{ retryable: json.error.code === -32005 }
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
return json.result;
|
|
450
|
+
}
|
|
451
|
+
async verifyX402Receipt(input, payload, quoteId, transactionId, expectedAmountAtomic, extraRaw) {
|
|
452
|
+
if (this.seenX402TransactionIds.has(transactionId)) {
|
|
453
|
+
return {
|
|
454
|
+
valid: false,
|
|
455
|
+
backendId: this.capabilities.id,
|
|
456
|
+
quoteId,
|
|
457
|
+
payTo: input.payTo,
|
|
458
|
+
transactionId,
|
|
459
|
+
reason: "x402 transaction has already been verified.",
|
|
460
|
+
raw: { ...extraRaw, payload }
|
|
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
|
+
}
|
|
644
|
+
async function deriveBaseUsdcAddress(input) {
|
|
645
|
+
const masterXpub = normalizeNonEmptyString(
|
|
646
|
+
input.masterXpub,
|
|
647
|
+
"Base USDC masterXpub"
|
|
648
|
+
);
|
|
649
|
+
const path = normalizeNonEmptyString(input.path, "Base USDC derivation path");
|
|
650
|
+
try {
|
|
651
|
+
const [{ HDKey }, { secp256k1 }, { keccak_256 }] = await Promise.all([
|
|
652
|
+
import("@scure/bip32"),
|
|
653
|
+
import("@noble/curves/secp256k1.js"),
|
|
654
|
+
import("@noble/hashes/sha3.js")
|
|
655
|
+
]);
|
|
656
|
+
const root = HDKey.fromExtendedKey(masterXpub);
|
|
657
|
+
const child = root.derive(path);
|
|
658
|
+
if (!child.publicKey) {
|
|
659
|
+
throw new PaymentConfigurationError(`No public key derived for ${path}.`);
|
|
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
|
+
}
|
|
675
|
+
}
|
|
676
|
+
function quoteIdToDerivationIndex(quoteId) {
|
|
677
|
+
return quoteIdToDerivationIndexes(quoteId)[0] ?? 0;
|
|
678
|
+
}
|
|
679
|
+
function quoteIdToDerivationIndexes(quoteId) {
|
|
680
|
+
const normalizedQuoteId = normalizeNonEmptyString(
|
|
681
|
+
quoteId,
|
|
682
|
+
"Base USDC quoteId"
|
|
683
|
+
);
|
|
684
|
+
const hash = createHash("sha256").update(normalizedQuoteId).digest();
|
|
685
|
+
return [0, 4, 8, 12].map((offset) => hash.readUInt32BE(offset) & 2147483647);
|
|
686
|
+
}
|
|
687
|
+
function normalizeDerivationIndex(index) {
|
|
688
|
+
if (!Number.isInteger(index) || index < 0 || index > 2147483647) {
|
|
689
|
+
throw new PaymentConfigurationError(
|
|
690
|
+
`BaseUsdcAdapter addressIndexForQuote must return a non-hardened BIP32 child index, received ${String(index)}.`
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
return index;
|
|
694
|
+
}
|
|
695
|
+
function normalizeDerivationPathPrefix(value) {
|
|
696
|
+
if (typeof value !== "string") {
|
|
697
|
+
throw new PaymentConfigurationError(
|
|
698
|
+
"BaseUsdcAdapter derivationPathPrefix must be a string."
|
|
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;
|
|
708
|
+
}
|
|
709
|
+
function normalizeConfirmationCount(value, label) {
|
|
710
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
711
|
+
throw new PaymentConfigurationError(
|
|
712
|
+
`${label} must be a non-negative integer, received ${String(value)}.`
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
return value;
|
|
716
|
+
}
|
|
717
|
+
function readBoolean(value, key) {
|
|
718
|
+
const item = value[key];
|
|
719
|
+
return typeof item === "boolean" ? item : void 0;
|
|
720
|
+
}
|
|
721
|
+
function normalizeFromBlock(value) {
|
|
722
|
+
if (typeof value === "bigint") {
|
|
723
|
+
if (value < 0n) {
|
|
724
|
+
throw new PaymentConfigurationError(
|
|
725
|
+
`BaseUsdcAdapter fromBlock must be a non-negative JSON-RPC block tag or quantity, received ${String(value)}.`
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
return toHexQuantity(value);
|
|
729
|
+
}
|
|
730
|
+
if (typeof value === "number") {
|
|
731
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
732
|
+
throw new PaymentConfigurationError(
|
|
733
|
+
`BaseUsdcAdapter fromBlock must be a non-negative JSON-RPC block tag or quantity, received ${String(value)}.`
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
return toHexQuantity(BigInt(value));
|
|
737
|
+
}
|
|
738
|
+
if (typeof value !== "string") {
|
|
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
|
+
);
|
|
753
|
+
}
|
|
754
|
+
function configuredFromBlockMinBlock(value) {
|
|
755
|
+
if (value === void 0 || ["earliest", "latest", "pending"].includes(value)) {
|
|
756
|
+
return 0n;
|
|
757
|
+
}
|
|
758
|
+
return parseHexQuantity(value);
|
|
759
|
+
}
|
|
760
|
+
function calculateConfirmations(currentBlock, blockNumber) {
|
|
761
|
+
if (blockNumber <= 0n || currentBlock < blockNumber) {
|
|
762
|
+
return 0;
|
|
763
|
+
}
|
|
764
|
+
const confirmations = currentBlock - blockNumber + 1n;
|
|
765
|
+
return confirmations > BigInt(Number.MAX_SAFE_INTEGER) ? Number.MAX_SAFE_INTEGER : Number(confirmations);
|
|
766
|
+
}
|
|
767
|
+
function resolveExpectedUsdcAmount(option, context) {
|
|
768
|
+
if (option) {
|
|
769
|
+
return option.expectedAmountAtomic;
|
|
770
|
+
}
|
|
771
|
+
if (context.settlementAmount !== void 0) {
|
|
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
|
+
);
|
|
790
|
+
}
|
|
791
|
+
function amountToUsdcAtomicUnits(amount, currency, context) {
|
|
792
|
+
const normalizedCurrency = normalizeCurrency(currency, context);
|
|
793
|
+
if (!["USD", "USDC"].includes(normalizedCurrency)) {
|
|
794
|
+
throw new PaymentConfigurationError(
|
|
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);
|
|
803
|
+
}
|
|
804
|
+
function pickBestTransferLog(logs, expectedAmountAtomic, payTo, comparison = "atLeast", minBlockNumber = 0n) {
|
|
805
|
+
const expected = BigInt(expectedAmountAtomic);
|
|
806
|
+
return logs.find((log) => {
|
|
807
|
+
const transfer = parseTransferLog(log);
|
|
808
|
+
if (!transfer || log.removed) {
|
|
809
|
+
return false;
|
|
810
|
+
}
|
|
811
|
+
if (readLogBlockNumber(log) < minBlockNumber) {
|
|
812
|
+
return false;
|
|
813
|
+
}
|
|
814
|
+
if (payTo && transfer.to !== normalizeAddress(payTo)) {
|
|
815
|
+
return false;
|
|
816
|
+
}
|
|
817
|
+
return comparison === "exact" ? transfer.amountAtomic === expected : transfer.amountAtomic >= expected;
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
function pickSingleTransferMatch(logs, expectedAmountAtomic, payTo, minBlockNumber = 0n) {
|
|
821
|
+
const log = pickBestTransferLog(
|
|
822
|
+
logs,
|
|
823
|
+
expectedAmountAtomic,
|
|
824
|
+
payTo,
|
|
825
|
+
"atLeast",
|
|
826
|
+
minBlockNumber
|
|
827
|
+
);
|
|
828
|
+
const transfer = log ? parseTransferLog(log) : void 0;
|
|
829
|
+
return log && transfer ? { amountAtomic: transfer.amountAtomic, latestLog: log } : void 0;
|
|
830
|
+
}
|
|
831
|
+
function pickCumulativeTransferMatch(logs, expectedAmountAtomic, payTo, minBlockNumber = 0n) {
|
|
832
|
+
const expected = BigInt(expectedAmountAtomic);
|
|
833
|
+
const matched = logs.map((log) => ({
|
|
834
|
+
log,
|
|
835
|
+
transfer: parseTransferLog(log)
|
|
836
|
+
})).filter(
|
|
837
|
+
(item) => {
|
|
838
|
+
if (!item.transfer || item.log.removed) {
|
|
839
|
+
return false;
|
|
840
|
+
}
|
|
841
|
+
if (readLogBlockNumber(item.log) < minBlockNumber) {
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
return true;
|
|
845
|
+
}
|
|
846
|
+
);
|
|
847
|
+
const byPayer = /* @__PURE__ */ new Map();
|
|
848
|
+
for (const item of matched) {
|
|
849
|
+
const existing = byPayer.get(item.transfer.from);
|
|
850
|
+
const latestLog = existing && compareTransferLogs(existing.latestLog, item.log) >= 0 ? existing.latestLog : item.log;
|
|
851
|
+
byPayer.set(item.transfer.from, {
|
|
852
|
+
amountAtomic: (existing?.amountAtomic ?? 0n) + item.transfer.amountAtomic,
|
|
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
|
+
);
|
|
865
|
+
}
|
|
866
|
+
function parseTransferLog(log) {
|
|
867
|
+
if (!isRecord(log) || !Array.isArray(log.topics) || typeof log.data !== "string") {
|
|
868
|
+
return void 0;
|
|
869
|
+
}
|
|
870
|
+
if (log.topics[0]?.toLowerCase() !== TRANSFER_TOPIC || log.topics.length < 3) {
|
|
871
|
+
return void 0;
|
|
872
|
+
}
|
|
873
|
+
try {
|
|
874
|
+
return {
|
|
875
|
+
from: topicToAddress(log.topics[1] ?? ""),
|
|
876
|
+
to: topicToAddress(log.topics[2] ?? ""),
|
|
877
|
+
amountAtomic: parseHexQuantity(log.data)
|
|
878
|
+
};
|
|
879
|
+
} catch {
|
|
880
|
+
return void 0;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
function readLogBlockNumber(log) {
|
|
884
|
+
try {
|
|
885
|
+
return parseHexQuantity(log.blockNumber ?? "0x0");
|
|
886
|
+
} catch {
|
|
887
|
+
return 0n;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
function compareTransferLogs(left, right) {
|
|
891
|
+
const leftBlock = readLogBlockNumber(left);
|
|
892
|
+
const rightBlock = readLogBlockNumber(right);
|
|
893
|
+
if (leftBlock > rightBlock) {
|
|
894
|
+
return 1;
|
|
895
|
+
}
|
|
896
|
+
if (leftBlock < rightBlock) {
|
|
897
|
+
return -1;
|
|
898
|
+
}
|
|
899
|
+
return (left.transactionHash ?? "").toLowerCase().localeCompare((right.transactionHash ?? "").toLowerCase());
|
|
900
|
+
}
|
|
901
|
+
function trimSeenX402TransactionIds(seenTransactionIds) {
|
|
902
|
+
while (seenTransactionIds.size > DEFAULT_MAX_SEEN_X402_TRANSACTION_IDS) {
|
|
903
|
+
const oldest = seenTransactionIds.values().next().value;
|
|
904
|
+
if (oldest === void 0) {
|
|
905
|
+
break;
|
|
906
|
+
}
|
|
907
|
+
seenTransactionIds.delete(oldest);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
function buildUsdcPaymentUri(input) {
|
|
911
|
+
return `ethereum:${input.tokenAddress}@${BASE_MAINNET_CHAIN_ID}/transfer?address=${input.payTo}&uint256=${input.amountAtomic}`;
|
|
912
|
+
}
|
|
913
|
+
function isRecord(value) {
|
|
914
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
915
|
+
}
|
|
916
|
+
function normalizeAddress(address) {
|
|
917
|
+
if (typeof address !== "string") {
|
|
918
|
+
throw new PaymentConfigurationError(
|
|
919
|
+
`Invalid EVM address: ${String(address)}`
|
|
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();
|
|
927
|
+
}
|
|
928
|
+
function isEvmTransactionHash(value) {
|
|
929
|
+
return /^0x[a-fA-F0-9]{64}$/.test(value);
|
|
930
|
+
}
|
|
931
|
+
function safeNormalizeAddress(address) {
|
|
932
|
+
try {
|
|
933
|
+
return normalizeAddress(address);
|
|
934
|
+
} catch {
|
|
935
|
+
return void 0;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
function addressToTopic(address) {
|
|
939
|
+
return `0x${normalizeAddress(address).slice(2).padStart(64, "0")}`;
|
|
940
|
+
}
|
|
941
|
+
function topicToAddress(topic) {
|
|
942
|
+
if (!/^0x[a-fA-F0-9]{64}$/.test(topic)) {
|
|
943
|
+
throw new PaymentProviderError(`Invalid EVM address topic: ${topic}`);
|
|
944
|
+
}
|
|
945
|
+
return normalizeAddress(`0x${topic.slice(-40)}`);
|
|
946
|
+
}
|
|
947
|
+
function parseHexQuantity(value) {
|
|
948
|
+
if (typeof value === "bigint") {
|
|
949
|
+
if (value < 0n) {
|
|
950
|
+
throw new PaymentProviderError(
|
|
951
|
+
`Invalid JSON-RPC hex quantity: ${String(value)}`
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
return value;
|
|
955
|
+
}
|
|
956
|
+
if (typeof value === "number") {
|
|
957
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
958
|
+
throw new PaymentProviderError(
|
|
959
|
+
`Invalid JSON-RPC hex quantity: ${String(value)}`
|
|
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);
|
|
976
|
+
}
|
|
977
|
+
function toHexQuantity(value) {
|
|
978
|
+
return `0x${value.toString(16)}`;
|
|
979
|
+
}
|
|
980
|
+
function decodePaymentHeader(value) {
|
|
981
|
+
const trimmed = value.trim();
|
|
982
|
+
if (trimmed.length > 65536) {
|
|
983
|
+
throw new PaymentVerificationError("x402 payment header is too large.");
|
|
984
|
+
}
|
|
985
|
+
const decoded = trimmed.startsWith("{") ? JSON.parse(trimmed) : JSON.parse(
|
|
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;
|
|
997
|
+
}
|
|
998
|
+
function validateX402PayloadRequirements(payload, input) {
|
|
999
|
+
const expectedScheme = input.scheme ?? "exact";
|
|
1000
|
+
const actualScheme = readX402PayloadString(payload, ["scheme"]);
|
|
1001
|
+
if (!actualScheme) {
|
|
1002
|
+
return "x402 proof did not include the payment scheme.";
|
|
1003
|
+
}
|
|
1004
|
+
if (actualScheme && actualScheme !== expectedScheme) {
|
|
1005
|
+
return `x402 proof scheme ${actualScheme} did not match ${expectedScheme}.`;
|
|
1006
|
+
}
|
|
1007
|
+
const expectedNetwork = normalizeX402Network(
|
|
1008
|
+
input.network ?? BASE_MAINNET_CAIP2
|
|
1009
|
+
);
|
|
1010
|
+
const actualNetwork = normalizeX402Network(
|
|
1011
|
+
readX402PayloadString(payload, ["network", "networkId"])
|
|
1012
|
+
);
|
|
1013
|
+
if (!actualNetwork) {
|
|
1014
|
+
return "x402 proof did not include the payment network.";
|
|
1015
|
+
}
|
|
1016
|
+
if (actualNetwork && actualNetwork !== expectedNetwork) {
|
|
1017
|
+
return `x402 proof network ${actualNetwork} did not match ${expectedNetwork}.`;
|
|
1018
|
+
}
|
|
1019
|
+
const expectedResource = input.resource;
|
|
1020
|
+
if (expectedResource !== void 0) {
|
|
1021
|
+
const actualResource = readX402PayloadString(payload, ["resource"]);
|
|
1022
|
+
if (!actualResource) {
|
|
1023
|
+
return "x402 proof did not include the paid resource.";
|
|
1024
|
+
}
|
|
1025
|
+
if (actualResource !== expectedResource) {
|
|
1026
|
+
return "x402 proof resource did not match the requested resource.";
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
const expectedMethod = input.method;
|
|
1030
|
+
if (expectedMethod !== void 0) {
|
|
1031
|
+
const actualMethod = readX402PayloadString(payload, ["method"]);
|
|
1032
|
+
if (!actualMethod) {
|
|
1033
|
+
return "x402 proof did not include the HTTP method.";
|
|
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;
|
|
1065
|
+
}
|
|
1066
|
+
function parseX402ExpiryDate(value) {
|
|
1067
|
+
try {
|
|
1068
|
+
if (/^\d+$/.test(value)) {
|
|
1069
|
+
const timestamp = Number(value);
|
|
1070
|
+
if (!Number.isSafeInteger(timestamp)) {
|
|
1071
|
+
return void 0;
|
|
1072
|
+
}
|
|
1073
|
+
const date = new Date(
|
|
1074
|
+
timestamp < 1e12 ? timestamp * 1e3 : timestamp
|
|
1075
|
+
);
|
|
1076
|
+
return Number.isNaN(date.getTime()) ? void 0 : date;
|
|
1077
|
+
}
|
|
1078
|
+
return normalizeDate(value);
|
|
1079
|
+
} catch {
|
|
1080
|
+
return void 0;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
function normalizeX402Network(value) {
|
|
1084
|
+
if (value === void 0) {
|
|
1085
|
+
return void 0;
|
|
1086
|
+
}
|
|
1087
|
+
const normalized = value.trim().toLowerCase();
|
|
1088
|
+
if (["base", "base-mainnet", BASE_MAINNET_CAIP2].includes(normalized)) {
|
|
1089
|
+
return BASE_MAINNET_CAIP2;
|
|
1090
|
+
}
|
|
1091
|
+
return value;
|
|
1092
|
+
}
|
|
1093
|
+
function readX402PayloadString(value, keys) {
|
|
1094
|
+
const root = asRecord(value);
|
|
1095
|
+
const nestedPayload = asRecord(root?.payload);
|
|
1096
|
+
const authorization = asRecord(nestedPayload?.authorization);
|
|
1097
|
+
for (const key of keys) {
|
|
1098
|
+
const authorized = readLooseString(authorization, key);
|
|
1099
|
+
if (authorized) {
|
|
1100
|
+
return authorized;
|
|
1101
|
+
}
|
|
1102
|
+
const nested = readLooseString(nestedPayload, key);
|
|
1103
|
+
if (nested) {
|
|
1104
|
+
return nested;
|
|
1105
|
+
}
|
|
1106
|
+
const direct = readLooseString(root, key);
|
|
1107
|
+
if (direct) {
|
|
1108
|
+
return direct;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return void 0;
|
|
1112
|
+
}
|
|
1113
|
+
function asRecord(value) {
|
|
1114
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
1115
|
+
}
|
|
1116
|
+
function readLooseString(value, key) {
|
|
1117
|
+
const item = value?.[key];
|
|
1118
|
+
if (typeof item === "string") {
|
|
1119
|
+
return item;
|
|
1120
|
+
}
|
|
1121
|
+
if (typeof item === "number") {
|
|
1122
|
+
return String(item);
|
|
1123
|
+
}
|
|
1124
|
+
return void 0;
|
|
1125
|
+
}
|
|
1126
|
+
function findNestedString(value, keys, depth = 0) {
|
|
1127
|
+
if (!value || typeof value !== "object" || depth > 16) {
|
|
1128
|
+
return void 0;
|
|
1129
|
+
}
|
|
1130
|
+
const record = value;
|
|
1131
|
+
for (const key of keys) {
|
|
1132
|
+
const direct = record[key];
|
|
1133
|
+
if (typeof direct === "string" && direct.length > 0) {
|
|
1134
|
+
return direct;
|
|
1135
|
+
}
|
|
1136
|
+
if (typeof direct === "number" && Number.isFinite(direct)) {
|
|
1137
|
+
return String(direct);
|
|
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;
|
|
1147
|
+
}
|
|
1148
|
+
function bigintToFixedBytes(value) {
|
|
1149
|
+
const hex = value.toString(16).padStart(64, "0");
|
|
1150
|
+
return hexToBytes(hex);
|
|
1151
|
+
}
|
|
1152
|
+
function concatBytes(...values) {
|
|
1153
|
+
const totalLength = values.reduce((sum, value) => sum + value.length, 0);
|
|
1154
|
+
const result = new Uint8Array(totalLength);
|
|
1155
|
+
let offset = 0;
|
|
1156
|
+
for (const value of values) {
|
|
1157
|
+
result.set(value, offset);
|
|
1158
|
+
offset += value.length;
|
|
1159
|
+
}
|
|
1160
|
+
return result;
|
|
1161
|
+
}
|
|
1162
|
+
function bytesToHex(value) {
|
|
1163
|
+
return Array.from(value, (byte) => byte.toString(16).padStart(2, "0")).join(
|
|
1164
|
+
""
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
function hexToBytes(value) {
|
|
1168
|
+
const bytes = value.match(/.{1,2}/g) ?? [];
|
|
1169
|
+
return Uint8Array.from(bytes.map((byte) => Number.parseInt(byte, 16)));
|
|
1170
|
+
}
|
|
1171
|
+
export {
|
|
1172
|
+
BASE_MAINNET_CAIP2,
|
|
1173
|
+
BASE_MAINNET_CHAIN_ID,
|
|
1174
|
+
BASE_USDC_BACKEND_ID,
|
|
1175
|
+
BASE_USDC_CONTRACT,
|
|
1176
|
+
BaseUsdcAdapter,
|
|
1177
|
+
USDC_DECIMALS,
|
|
1178
|
+
deriveBaseUsdcAddress,
|
|
1179
|
+
quoteIdToDerivationIndex,
|
|
1180
|
+
quoteIdToDerivationIndexes
|
|
1181
|
+
};
|
|
1182
|
+
//# sourceMappingURL=base-usdc.js.map
|