@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
package/dist/adapters/btc.js
CHANGED
|
@@ -1,843 +1,518 @@
|
|
|
1
|
+
import { r as PaymentProviderError, t as PaymentConfigurationError } from "../chunks/errors-feZrwnTy.js";
|
|
2
|
+
import { _ as normalizeUrlString, b as rememberPaymentOption, c as normalizeAmount, d as normalizeFutureDate, f as normalizeMaxStoredPaymentOptions, g as normalizePositivePaymentAmount, h as normalizePositiveMinorUnitAmount, i as decimalToMinorUnitAmount, m as normalizeNonEmptyString, n as currencyMinorUnitDecimals, o as getFetch, p as normalizeMinorUnitAmount, s as minorUnitsToDecimal, t as applyExpiryToPendingStatus, u as normalizeDate, v as pollPaymentStatus, y as readJsonResponse } from "../chunks/shared-DVl6UzD9.js";
|
|
1
3
|
import { createHmac, timingSafeEqual } 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
|
-
async sendPayout(input) {
|
|
284
|
-
if (input.currency !== void 0) {
|
|
285
|
-
const currency = normalizeNonEmptyString(
|
|
286
|
-
input.currency,
|
|
287
|
-
"BTC payout currency"
|
|
288
|
-
);
|
|
289
|
-
if (currency.toUpperCase() !== "BTC") {
|
|
290
|
-
throw new PaymentConfigurationError(
|
|
291
|
-
`BtcAdapter can only send BTC payouts, received ${input.currency}.`
|
|
292
|
-
);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
const destination = normalizeNonEmptyString(
|
|
296
|
-
input.destination,
|
|
297
|
-
"BTC payout destination"
|
|
298
|
-
);
|
|
299
|
-
const quoteId = input.quoteId === void 0 ? void 0 : normalizeNonEmptyString(input.quoteId, "BTC payout quoteId");
|
|
300
|
-
const idempotencyKey = input.idempotencyKey === void 0 ? quoteId : normalizeNonEmptyString(
|
|
301
|
-
input.idempotencyKey,
|
|
302
|
-
"BTC payout idempotencyKey"
|
|
303
|
-
);
|
|
304
|
-
const amount = normalizePositiveMinorUnitAmount(
|
|
305
|
-
input.amount,
|
|
306
|
-
"BTC payout amount"
|
|
307
|
-
);
|
|
308
|
-
const payoutAmount = minorUnitsToDecimal(
|
|
309
|
-
amount,
|
|
310
|
-
currencyMinorUnitDecimals("BTC")
|
|
311
|
-
);
|
|
312
|
-
const result = await this.request(
|
|
313
|
-
`/api/v1/stores/${encodeURIComponent(
|
|
314
|
-
this.storeId
|
|
315
|
-
)}/payment-methods/onchain/${encodeURIComponent(
|
|
316
|
-
this.paymentMethod
|
|
317
|
-
)}/wallet/transactions`,
|
|
318
|
-
{
|
|
319
|
-
method: "POST",
|
|
320
|
-
body: JSON.stringify({
|
|
321
|
-
destinations: [
|
|
322
|
-
{
|
|
323
|
-
destination,
|
|
324
|
-
amount: payoutAmount
|
|
325
|
-
}
|
|
326
|
-
],
|
|
327
|
-
subtractFromAmount: false,
|
|
328
|
-
metadata: {
|
|
329
|
-
...input.metadata,
|
|
330
|
-
...quoteId === void 0 ? {} : { quoteId },
|
|
331
|
-
...idempotencyKey === void 0 ? {} : { idempotencyKey },
|
|
332
|
-
...input.memo === void 0 ? {} : { memo: input.memo }
|
|
333
|
-
}
|
|
334
|
-
})
|
|
335
|
-
}
|
|
336
|
-
);
|
|
337
|
-
return {
|
|
338
|
-
backendId: this.capabilities.id,
|
|
339
|
-
status: "pending_signature",
|
|
340
|
-
payoutId: normalizeOptionalProviderString(
|
|
341
|
-
readProviderString(result, "id")
|
|
342
|
-
),
|
|
343
|
-
psbt: normalizeOptionalProviderString(readProviderString(result, "psbt")) ?? normalizeOptionalProviderString(
|
|
344
|
-
readProviderString(result, "psbtBase64")
|
|
345
|
-
) ?? normalizeOptionalProviderString(
|
|
346
|
-
readProviderString(result, "unsignedPsbt")
|
|
347
|
-
),
|
|
348
|
-
transactionId: normalizeOptionalProviderString(
|
|
349
|
-
readProviderString(result, "transactionId")
|
|
350
|
-
),
|
|
351
|
-
destination,
|
|
352
|
-
amount,
|
|
353
|
-
currency: "BTC",
|
|
354
|
-
raw: result
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
async refundPayment(input) {
|
|
358
|
-
if (input.currency !== void 0 && normalizeCurrencyCode(input.currency) !== "BTC") {
|
|
359
|
-
throw new PaymentConfigurationError(
|
|
360
|
-
`BTC refunds require BTC currency when specified, received ${input.currency}.`
|
|
361
|
-
);
|
|
362
|
-
}
|
|
363
|
-
if (!input.destination) {
|
|
364
|
-
throw new PaymentConfigurationError(
|
|
365
|
-
"BTC refunds require a destination address."
|
|
366
|
-
);
|
|
367
|
-
}
|
|
368
|
-
if (input.amount === void 0) {
|
|
369
|
-
throw new PaymentConfigurationError("BTC refunds require an amount.");
|
|
370
|
-
}
|
|
371
|
-
const payout = await this.sendPayout({
|
|
372
|
-
destination: input.destination,
|
|
373
|
-
amount: input.amount,
|
|
374
|
-
currency: "BTC",
|
|
375
|
-
idempotencyKey: input.idempotencyKey,
|
|
376
|
-
memo: input.reason,
|
|
377
|
-
metadata: input.metadata
|
|
378
|
-
});
|
|
379
|
-
return {
|
|
380
|
-
backendId: this.capabilities.id,
|
|
381
|
-
status: "requires_action",
|
|
382
|
-
refundId: payout.payoutId,
|
|
383
|
-
transactionId: payout.transactionId,
|
|
384
|
-
amount: payout.amount,
|
|
385
|
-
currency: payout.currency,
|
|
386
|
-
raw: payout.raw
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
parseWebhookEvent(payload, signature) {
|
|
390
|
-
if (!this.webhookSecret) {
|
|
391
|
-
throw new PaymentConfigurationError(
|
|
392
|
-
"BtcAdapter parseWebhookEvent requires webhookSecret."
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
if (!signature) {
|
|
396
|
-
throw new PaymentProviderError("Missing BTCPay webhook signature.");
|
|
397
|
-
}
|
|
398
|
-
verifyBtcpayWebhookSignature(payload, signature, this.webhookSecret);
|
|
399
|
-
const event = parseBtcpayWebhookPayload(payload);
|
|
400
|
-
const type = normalizeOptionalWebhookString(
|
|
401
|
-
readProviderString(event, "type")
|
|
402
|
-
);
|
|
403
|
-
const deliveryId = normalizeOptionalWebhookString(
|
|
404
|
-
readProviderString(event, "deliveryId")
|
|
405
|
-
);
|
|
406
|
-
if (!deliveryId) {
|
|
407
|
-
throw new PaymentProviderError("BTCPay webhook deliveryId is required.");
|
|
408
|
-
}
|
|
409
|
-
const duplicate = this.seenWebhookDeliveryIds.has(deliveryId);
|
|
410
|
-
if (!duplicate) {
|
|
411
|
-
this.seenWebhookDeliveryIds.add(deliveryId);
|
|
412
|
-
while (this.seenWebhookDeliveryIds.size > this.maxStoredWebhookDeliveryIds) {
|
|
413
|
-
const oldest = this.seenWebhookDeliveryIds.values().next().value;
|
|
414
|
-
if (oldest === void 0) {
|
|
415
|
-
break;
|
|
416
|
-
}
|
|
417
|
-
this.seenWebhookDeliveryIds.delete(oldest);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
const invoiceId = readProviderString(event, "invoiceId") ?? findNestedProviderString(event, ["invoiceId", "id"]);
|
|
421
|
-
return {
|
|
422
|
-
deliveryId,
|
|
423
|
-
invoiceId: normalizeOptionalWebhookString(invoiceId),
|
|
424
|
-
quoteId: normalizeOptionalWebhookString(
|
|
425
|
-
findNestedProviderString(event, ["quoteId"])
|
|
426
|
-
),
|
|
427
|
-
duplicate,
|
|
428
|
-
type,
|
|
429
|
-
status: mapBtcpayStatus(
|
|
430
|
-
readString(event, "status"),
|
|
431
|
-
readString(event, "additionalStatus"),
|
|
432
|
-
type
|
|
433
|
-
),
|
|
434
|
-
raw: event
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
getRequiredConfirmations(input) {
|
|
438
|
-
return normalizeConfirmationCount(
|
|
439
|
-
(this.options.confirmationPolicy ?? defaultBtcConfirmationPolicy)(input),
|
|
440
|
-
"BTC confirmationPolicy result"
|
|
441
|
-
);
|
|
442
|
-
}
|
|
443
|
-
async getInvoice(invoiceId) {
|
|
444
|
-
return this.request(
|
|
445
|
-
`/api/v1/stores/${encodeURIComponent(
|
|
446
|
-
this.storeId
|
|
447
|
-
)}/invoices/${encodeURIComponent(invoiceId)}`
|
|
448
|
-
);
|
|
449
|
-
}
|
|
450
|
-
async findInvoice(quoteId) {
|
|
451
|
-
const invoices = await this.request(
|
|
452
|
-
`/api/v1/stores/${encodeURIComponent(
|
|
453
|
-
this.storeId
|
|
454
|
-
)}/invoices?textSearch=${encodeURIComponent(quoteId)}`
|
|
455
|
-
);
|
|
456
|
-
const list = Array.isArray(invoices) ? invoices : Array.isArray(invoices.items) ? invoices.items : [];
|
|
457
|
-
const matches = list.filter(
|
|
458
|
-
(invoice) => invoiceMatchesQuote(invoice, quoteId)
|
|
459
|
-
);
|
|
460
|
-
if (matches.length > 1) {
|
|
461
|
-
throw new PaymentProviderError(
|
|
462
|
-
`BTCPay invoice lookup for quote ${quoteId} returned multiple matches.`
|
|
463
|
-
);
|
|
464
|
-
}
|
|
465
|
-
const [match] = matches;
|
|
466
|
-
if (!match || typeof match !== "object") {
|
|
467
|
-
throw new PaymentProviderError(
|
|
468
|
-
`BTCPay invoice for quote ${quoteId} was not found.`
|
|
469
|
-
);
|
|
470
|
-
}
|
|
471
|
-
return match;
|
|
472
|
-
}
|
|
473
|
-
async getPaymentMethods(invoiceId, options = {}) {
|
|
474
|
-
const response = await this.request(
|
|
475
|
-
`/api/v1/stores/${encodeURIComponent(
|
|
476
|
-
this.storeId
|
|
477
|
-
)}/invoices/${encodeURIComponent(invoiceId)}/payment-methods`
|
|
478
|
-
);
|
|
479
|
-
return extractPaymentMethods(response, options.requireUsable) ?? [];
|
|
480
|
-
}
|
|
481
|
-
async request(path, init = {}) {
|
|
482
|
-
const headers = new Headers(init.headers);
|
|
483
|
-
headers.set("Authorization", `token ${this.apiKey}`);
|
|
484
|
-
headers.set("Content-Type", "application/json");
|
|
485
|
-
headers.set("Accept", "application/json");
|
|
486
|
-
const response = await this.fetch(`${this.baseUrl}${path}`, {
|
|
487
|
-
...init,
|
|
488
|
-
headers
|
|
489
|
-
});
|
|
490
|
-
return readJsonResponse(response, `BTCPay ${path}`);
|
|
491
|
-
}
|
|
492
|
-
}
|
|
4
|
+
//#region src/adapters/btc.ts
|
|
5
|
+
var BTC_BACKEND_ID = "btc";
|
|
6
|
+
var DEFAULT_MAX_STORED_WEBHOOK_DELIVERY_IDS = 5e4;
|
|
7
|
+
var BtcAdapter = class {
|
|
8
|
+
options;
|
|
9
|
+
capabilities;
|
|
10
|
+
fetch;
|
|
11
|
+
baseUrl;
|
|
12
|
+
apiKey;
|
|
13
|
+
storeId;
|
|
14
|
+
currency;
|
|
15
|
+
paymentMethod;
|
|
16
|
+
webhookSecret;
|
|
17
|
+
maxStoredPaymentOptions;
|
|
18
|
+
maxStoredWebhookDeliveryIds;
|
|
19
|
+
optionsByQuote = /* @__PURE__ */ new Map();
|
|
20
|
+
seenWebhookDeliveryIds = /* @__PURE__ */ new Set();
|
|
21
|
+
constructor(options) {
|
|
22
|
+
this.options = options;
|
|
23
|
+
if (typeof options.baseUrl !== "string" || !options.baseUrl.trim()) throw new PaymentConfigurationError("BtcAdapter requires baseUrl.");
|
|
24
|
+
if (typeof options.apiKey !== "string" || !options.apiKey.trim()) throw new PaymentConfigurationError("BtcAdapter requires apiKey.");
|
|
25
|
+
if (typeof options.storeId !== "string" || !options.storeId.trim()) throw new PaymentConfigurationError("BtcAdapter requires storeId.");
|
|
26
|
+
if (options.webhookSecret !== void 0) {
|
|
27
|
+
if (typeof options.webhookSecret !== "string") throw new PaymentConfigurationError("BtcAdapter webhookSecret must be a string when configured.");
|
|
28
|
+
if (!options.webhookSecret.trim()) throw new PaymentConfigurationError("BtcAdapter webhookSecret must not be empty when configured.");
|
|
29
|
+
}
|
|
30
|
+
this.fetch = getFetch(options.fetch);
|
|
31
|
+
this.maxStoredPaymentOptions = normalizeMaxStoredPaymentOptions(options.maxStoredPaymentOptions, "BtcAdapter maxStoredPaymentOptions");
|
|
32
|
+
this.maxStoredWebhookDeliveryIds = normalizeMaxStoredPaymentOptions(options.maxStoredWebhookDeliveryIds ?? DEFAULT_MAX_STORED_WEBHOOK_DELIVERY_IDS, "BtcAdapter maxStoredWebhookDeliveryIds");
|
|
33
|
+
this.baseUrl = normalizeBtcpayBaseUrl(options.baseUrl);
|
|
34
|
+
this.apiKey = options.apiKey.trim();
|
|
35
|
+
this.storeId = options.storeId.trim();
|
|
36
|
+
this.currency = normalizeCurrencyCode(options.currency ?? "USD");
|
|
37
|
+
this.paymentMethod = normalizePaymentMethod(options.paymentMethod ?? "BTC-CHAIN");
|
|
38
|
+
this.webhookSecret = options.webhookSecret?.trim();
|
|
39
|
+
const managedProviderName = normalizeOptionalProviderString(options.managedProviderName);
|
|
40
|
+
this.capabilities = {
|
|
41
|
+
id: "btc",
|
|
42
|
+
displayName: "Bitcoin via BTCPay Server",
|
|
43
|
+
settlementCurrency: "BTC",
|
|
44
|
+
supportedSettlementCurrencies: ["BTC"],
|
|
45
|
+
chain: "bitcoin",
|
|
46
|
+
settlementShape: "address",
|
|
47
|
+
x402Capable: false,
|
|
48
|
+
confirmationLatency: {
|
|
49
|
+
expectedSeconds: 600,
|
|
50
|
+
maxExpectedSeconds: 1800,
|
|
51
|
+
minConfirmations: 1,
|
|
52
|
+
description: "Tiered Bitcoin confirmation policy: 1 confirmation below $100, 2 below $1,000, 3 at $1,000 and above."
|
|
53
|
+
},
|
|
54
|
+
supportsRefunds: true,
|
|
55
|
+
supportsPayouts: true,
|
|
56
|
+
supportsWebhooks: true,
|
|
57
|
+
metadata: {
|
|
58
|
+
provider: "btcpay-server",
|
|
59
|
+
managedProviderName
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async createPaymentOption(input) {
|
|
64
|
+
const quoteId = normalizeNonEmptyString(input.quoteId, "BTC quoteId");
|
|
65
|
+
const expiresAt = normalizeFutureDate(input.expiresAt, "BTCPay invoice expiry");
|
|
66
|
+
const currency = normalizeCurrencyCode(input.currency);
|
|
67
|
+
const amount = normalizePositivePaymentAmount(input.amount, currency, "BTCPay invoice amount");
|
|
68
|
+
const invoiceAmount = minorUnitsToDecimal(amount, currencyMinorUnitDecimals(currency));
|
|
69
|
+
const invoice = await this.request(`/api/v1/stores/${encodeURIComponent(this.storeId)}/invoices`, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
body: JSON.stringify({
|
|
72
|
+
amount: invoiceAmount,
|
|
73
|
+
currency,
|
|
74
|
+
metadata: {
|
|
75
|
+
...input.metadata,
|
|
76
|
+
quoteId,
|
|
77
|
+
buyerEmail: input.buyerEmail,
|
|
78
|
+
description: input.description
|
|
79
|
+
},
|
|
80
|
+
checkout: { expirationMinutes: Math.max(1, Math.ceil((expiresAt.getTime() - Date.now()) / 6e4)) },
|
|
81
|
+
additionalSearchTerms: [quoteId]
|
|
82
|
+
})
|
|
83
|
+
});
|
|
84
|
+
const invoiceId = normalizeOptionalProviderString(readProviderString(invoice, "id"));
|
|
85
|
+
const method = selectBtcPaymentMethod(extractPaymentMethods(invoice) ?? (invoiceId ? await this.getPaymentMethods(invoiceId) : []), this.paymentMethod);
|
|
86
|
+
const settlementAmountDecimal = method?.decimalAmount === void 0 ? void 0 : normalizeAmount(method.decimalAmount);
|
|
87
|
+
const settlementAmount = settlementAmountDecimal === void 0 ? void 0 : decimalToMinorUnitAmount(settlementAmountDecimal, currencyMinorUnitDecimals("BTC"), "BTC settlement");
|
|
88
|
+
const payTo = method?.destination?.trim();
|
|
89
|
+
if (!invoiceId) throw new PaymentProviderError("BTCPay invoice response did not include an invoice id.");
|
|
90
|
+
if (!method || !payTo || settlementAmount === void 0 || settlementAmount === 0) throw new PaymentProviderError(`BTCPay invoice response did not include a ${this.paymentMethod} payment address and amount.`);
|
|
91
|
+
const requiredConfirmations = this.getRequiredConfirmations({
|
|
92
|
+
quoteId,
|
|
93
|
+
amount,
|
|
94
|
+
currency
|
|
95
|
+
});
|
|
96
|
+
const option = {
|
|
97
|
+
backendId: this.capabilities.id,
|
|
98
|
+
quoteId,
|
|
99
|
+
payTo,
|
|
100
|
+
settlementShape: "address",
|
|
101
|
+
settlementCurrency: "BTC",
|
|
102
|
+
settlementAmount,
|
|
103
|
+
amount,
|
|
104
|
+
currency,
|
|
105
|
+
expiresAt,
|
|
106
|
+
providerPaymentId: invoiceId,
|
|
107
|
+
paymentUri: normalizeOptionalProviderString(method?.paymentLink),
|
|
108
|
+
metadata: {
|
|
109
|
+
invoiceId,
|
|
110
|
+
checkoutLink: normalizeOptionalProviderString(readString(invoice, "checkoutLink")),
|
|
111
|
+
requiredConfirmations,
|
|
112
|
+
rate: method?.rate,
|
|
113
|
+
networkFee: method?.networkFee
|
|
114
|
+
},
|
|
115
|
+
requiredConfirmations
|
|
116
|
+
};
|
|
117
|
+
rememberPaymentOption(this.optionsByQuote, quoteId, option, this.maxStoredPaymentOptions);
|
|
118
|
+
return option;
|
|
119
|
+
}
|
|
120
|
+
watchPayment(input) {
|
|
121
|
+
return pollPaymentStatus({
|
|
122
|
+
...input,
|
|
123
|
+
pollIntervalMs: input.pollIntervalMs ?? this.options.pollIntervalMs ?? 3e4
|
|
124
|
+
}, () => this.getStatus(input.quoteId, input.payTo, input.statusContext));
|
|
125
|
+
}
|
|
126
|
+
async getStatus(quoteId, payTo, context = {}) {
|
|
127
|
+
const normalizedQuoteId = normalizeNonEmptyString(quoteId, "BTC quoteId");
|
|
128
|
+
const normalizedPayTo = normalizeNonEmptyString(payTo, "BTC payTo");
|
|
129
|
+
const contextProviderPaymentId = context.providerPaymentId === void 0 ? void 0 : normalizeNonEmptyString(context.providerPaymentId, "BTC providerPaymentId");
|
|
130
|
+
const option = this.optionsByQuote.get(normalizedQuoteId);
|
|
131
|
+
const contextAmount = context.amount === void 0 ? void 0 : normalizeMinorUnitAmount(context.amount, "BTC status");
|
|
132
|
+
const contextCurrency = context.currency === void 0 ? void 0 : normalizeCurrencyCode(context.currency);
|
|
133
|
+
const contextSettlementAmount = context.settlementAmount === void 0 ? void 0 : normalizeMinorUnitAmount(context.settlementAmount, "BTC settlement status");
|
|
134
|
+
const invoice = option?.providerPaymentId !== void 0 || contextProviderPaymentId !== void 0 ? await this.getInvoice(option?.providerPaymentId ?? contextProviderPaymentId ?? "") : await this.findInvoice(normalizedQuoteId);
|
|
135
|
+
const invoiceId = normalizeOptionalProviderString(readProviderString(invoice, "id")) ?? option?.providerPaymentId;
|
|
136
|
+
const statusWithExpiry = applyExpiryToPendingStatus(mapBtcpayStatus(readString(invoice, "status"), readString(invoice, "additionalStatus")), option?.expiresAt ?? (context.expiresAt === void 0 ? void 0 : normalizeDate(context.expiresAt)));
|
|
137
|
+
let confirmations = findNestedNonNegativeInteger(invoice, ["confirmations", "confirmationCount"]);
|
|
138
|
+
const contextRequiredConfirmations = context.requiredConfirmations === void 0 ? void 0 : normalizeConfirmationCount(context.requiredConfirmations, "BTC requiredConfirmations");
|
|
139
|
+
const requiredConfirmations = option?.requiredConfirmations ?? contextRequiredConfirmations ?? this.getRequiredConfirmations({
|
|
140
|
+
quoteId: normalizedQuoteId,
|
|
141
|
+
amount: option?.amount ?? contextAmount ?? decimalToMinorUnitAmount(readDecimalString(invoice, "amount") ?? "0", currencyMinorUnitDecimals(option?.currency ?? contextCurrency ?? this.currency), "BTCPay invoice"),
|
|
142
|
+
currency: option?.currency ?? contextCurrency ?? this.currency
|
|
143
|
+
});
|
|
144
|
+
if (statusWithExpiry === "confirmed" && confirmations === void 0 && invoiceId) try {
|
|
145
|
+
confirmations = maxConfirmationCount(await this.getPaymentMethods(invoiceId, { requireUsable: false }));
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (!(error instanceof PaymentProviderError)) throw error;
|
|
148
|
+
}
|
|
149
|
+
const effectiveStatus = enforceBtcConfirmations(statusWithExpiry, confirmations, requiredConfirmations);
|
|
150
|
+
const invoiceSettlementAmount = findNestedDecimalString(invoice, ["btcDue", "amountDue"]);
|
|
151
|
+
const receivedSettlementAmount = readDecimalString(invoice, "amountPaid") ?? findNestedDecimalString(invoice, ["paid"]);
|
|
152
|
+
return {
|
|
153
|
+
backendId: this.capabilities.id,
|
|
154
|
+
quoteId: normalizedQuoteId,
|
|
155
|
+
payTo: normalizedPayTo,
|
|
156
|
+
status: effectiveStatus,
|
|
157
|
+
settlementCurrency: "BTC",
|
|
158
|
+
settlementAmount: option?.settlementAmount ?? contextSettlementAmount ?? (invoiceSettlementAmount === void 0 ? void 0 : decimalToMinorUnitAmount(invoiceSettlementAmount, currencyMinorUnitDecimals("BTC"), "BTC settlement")),
|
|
159
|
+
receivedAmount: receivedSettlementAmount === void 0 ? void 0 : decimalToMinorUnitAmount(receivedSettlementAmount, currencyMinorUnitDecimals("BTC"), "BTC received"),
|
|
160
|
+
amount: option?.amount ?? contextAmount,
|
|
161
|
+
currency: option?.currency ?? contextCurrency,
|
|
162
|
+
requiredConfirmations,
|
|
163
|
+
confirmations,
|
|
164
|
+
providerPaymentId: invoiceId,
|
|
165
|
+
transactionId: normalizeOptionalProviderString(findNestedProviderString(invoice, ["transactionId", "txid"])),
|
|
166
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
167
|
+
raw: invoice
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
async sendPayout(input) {
|
|
171
|
+
if (input.currency !== void 0) {
|
|
172
|
+
if (normalizeNonEmptyString(input.currency, "BTC payout currency").toUpperCase() !== "BTC") throw new PaymentConfigurationError(`BtcAdapter can only send BTC payouts, received ${input.currency}.`);
|
|
173
|
+
}
|
|
174
|
+
const destination = normalizeNonEmptyString(input.destination, "BTC payout destination");
|
|
175
|
+
const quoteId = input.quoteId === void 0 ? void 0 : normalizeNonEmptyString(input.quoteId, "BTC payout quoteId");
|
|
176
|
+
const idempotencyKey = input.idempotencyKey === void 0 ? quoteId : normalizeNonEmptyString(input.idempotencyKey, "BTC payout idempotencyKey");
|
|
177
|
+
const amount = normalizePositiveMinorUnitAmount(input.amount, "BTC payout amount");
|
|
178
|
+
const payoutAmount = minorUnitsToDecimal(amount, currencyMinorUnitDecimals("BTC"));
|
|
179
|
+
const result = await this.request(`/api/v1/stores/${encodeURIComponent(this.storeId)}/payment-methods/onchain/${encodeURIComponent(this.paymentMethod)}/wallet/transactions`, {
|
|
180
|
+
method: "POST",
|
|
181
|
+
body: JSON.stringify({
|
|
182
|
+
destinations: [{
|
|
183
|
+
destination,
|
|
184
|
+
amount: payoutAmount
|
|
185
|
+
}],
|
|
186
|
+
subtractFromAmount: false,
|
|
187
|
+
metadata: {
|
|
188
|
+
...input.metadata,
|
|
189
|
+
...quoteId === void 0 ? {} : { quoteId },
|
|
190
|
+
...idempotencyKey === void 0 ? {} : { idempotencyKey },
|
|
191
|
+
...input.memo === void 0 ? {} : { memo: input.memo }
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
});
|
|
195
|
+
return {
|
|
196
|
+
backendId: this.capabilities.id,
|
|
197
|
+
status: "pending_signature",
|
|
198
|
+
payoutId: normalizeOptionalProviderString(readProviderString(result, "id")),
|
|
199
|
+
psbt: normalizeOptionalProviderString(readProviderString(result, "psbt")) ?? normalizeOptionalProviderString(readProviderString(result, "psbtBase64")) ?? normalizeOptionalProviderString(readProviderString(result, "unsignedPsbt")),
|
|
200
|
+
transactionId: normalizeOptionalProviderString(readProviderString(result, "transactionId")),
|
|
201
|
+
destination,
|
|
202
|
+
amount,
|
|
203
|
+
currency: "BTC",
|
|
204
|
+
raw: result
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
async refundPayment(input) {
|
|
208
|
+
if (input.currency !== void 0 && normalizeCurrencyCode(input.currency) !== "BTC") throw new PaymentConfigurationError(`BTC refunds require BTC currency when specified, received ${input.currency}.`);
|
|
209
|
+
if (!input.destination) throw new PaymentConfigurationError("BTC refunds require a destination address.");
|
|
210
|
+
if (input.amount === void 0) throw new PaymentConfigurationError("BTC refunds require an amount.");
|
|
211
|
+
const payout = await this.sendPayout({
|
|
212
|
+
destination: input.destination,
|
|
213
|
+
amount: input.amount,
|
|
214
|
+
currency: "BTC",
|
|
215
|
+
idempotencyKey: input.idempotencyKey,
|
|
216
|
+
memo: input.reason,
|
|
217
|
+
metadata: input.metadata
|
|
218
|
+
});
|
|
219
|
+
return {
|
|
220
|
+
backendId: this.capabilities.id,
|
|
221
|
+
status: "requires_action",
|
|
222
|
+
refundId: payout.payoutId,
|
|
223
|
+
transactionId: payout.transactionId,
|
|
224
|
+
amount: payout.amount,
|
|
225
|
+
currency: payout.currency,
|
|
226
|
+
raw: payout.raw
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
parseWebhookEvent(payload, signature) {
|
|
230
|
+
if (!this.webhookSecret) throw new PaymentConfigurationError("BtcAdapter parseWebhookEvent requires webhookSecret.");
|
|
231
|
+
if (!signature) throw new PaymentProviderError("Missing BTCPay webhook signature.");
|
|
232
|
+
verifyBtcpayWebhookSignature(payload, signature, this.webhookSecret);
|
|
233
|
+
const event = parseBtcpayWebhookPayload(payload);
|
|
234
|
+
const type = normalizeOptionalWebhookString(readProviderString(event, "type"));
|
|
235
|
+
const deliveryId = normalizeOptionalWebhookString(readProviderString(event, "deliveryId"));
|
|
236
|
+
if (!deliveryId) throw new PaymentProviderError("BTCPay webhook deliveryId is required.");
|
|
237
|
+
const duplicate = this.seenWebhookDeliveryIds.has(deliveryId);
|
|
238
|
+
if (!duplicate) {
|
|
239
|
+
this.seenWebhookDeliveryIds.add(deliveryId);
|
|
240
|
+
while (this.seenWebhookDeliveryIds.size > this.maxStoredWebhookDeliveryIds) {
|
|
241
|
+
const oldest = this.seenWebhookDeliveryIds.values().next().value;
|
|
242
|
+
if (oldest === void 0) break;
|
|
243
|
+
this.seenWebhookDeliveryIds.delete(oldest);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
deliveryId,
|
|
248
|
+
invoiceId: normalizeOptionalWebhookString(readProviderString(event, "invoiceId") ?? findNestedProviderString(event, ["invoiceId", "id"])),
|
|
249
|
+
quoteId: normalizeOptionalWebhookString(findNestedProviderString(event, ["quoteId"])),
|
|
250
|
+
duplicate,
|
|
251
|
+
type,
|
|
252
|
+
status: mapBtcpayStatus(readString(event, "status"), readString(event, "additionalStatus"), type),
|
|
253
|
+
raw: event
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
getRequiredConfirmations(input) {
|
|
257
|
+
return normalizeConfirmationCount((this.options.confirmationPolicy ?? defaultBtcConfirmationPolicy)(input), "BTC confirmationPolicy result");
|
|
258
|
+
}
|
|
259
|
+
async getInvoice(invoiceId) {
|
|
260
|
+
return this.request(`/api/v1/stores/${encodeURIComponent(this.storeId)}/invoices/${encodeURIComponent(invoiceId)}`);
|
|
261
|
+
}
|
|
262
|
+
async findInvoice(quoteId) {
|
|
263
|
+
const invoices = await this.request(`/api/v1/stores/${encodeURIComponent(this.storeId)}/invoices?textSearch=${encodeURIComponent(quoteId)}`);
|
|
264
|
+
const matches = (Array.isArray(invoices) ? invoices : Array.isArray(invoices.items) ? invoices.items : []).filter((invoice) => invoiceMatchesQuote(invoice, quoteId));
|
|
265
|
+
if (matches.length > 1) throw new PaymentProviderError(`BTCPay invoice lookup for quote ${quoteId} returned multiple matches.`);
|
|
266
|
+
const [match] = matches;
|
|
267
|
+
if (!match || typeof match !== "object") throw new PaymentProviderError(`BTCPay invoice for quote ${quoteId} was not found.`);
|
|
268
|
+
return match;
|
|
269
|
+
}
|
|
270
|
+
async getPaymentMethods(invoiceId, options = {}) {
|
|
271
|
+
return extractPaymentMethods(await this.request(`/api/v1/stores/${encodeURIComponent(this.storeId)}/invoices/${encodeURIComponent(invoiceId)}/payment-methods`), options.requireUsable) ?? [];
|
|
272
|
+
}
|
|
273
|
+
async request(path, init = {}) {
|
|
274
|
+
const headers = new Headers(init.headers);
|
|
275
|
+
headers.set("Authorization", `token ${this.apiKey}`);
|
|
276
|
+
headers.set("Content-Type", "application/json");
|
|
277
|
+
headers.set("Accept", "application/json");
|
|
278
|
+
return readJsonResponse(await this.fetch(`${this.baseUrl}${path}`, {
|
|
279
|
+
...init,
|
|
280
|
+
headers
|
|
281
|
+
}), `BTCPay ${path}`);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
493
284
|
function invoiceMatchesQuote(invoice, quoteId) {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
const metadata = record.metadata && typeof record.metadata === "object" ? record.metadata : void 0;
|
|
499
|
-
if (readProviderString(metadata, "quoteId")?.trim() === quoteId) {
|
|
500
|
-
return true;
|
|
501
|
-
}
|
|
502
|
-
return false;
|
|
285
|
+
if (!invoice || typeof invoice !== "object") return false;
|
|
286
|
+
const record = invoice;
|
|
287
|
+
if (readProviderString(record.metadata && typeof record.metadata === "object" ? record.metadata : void 0, "quoteId")?.trim() === quoteId) return true;
|
|
288
|
+
return false;
|
|
503
289
|
}
|
|
504
290
|
function defaultBtcConfirmationPolicy(input) {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
const mediumValueThreshold = decimalToMinorUnitAmount(
|
|
512
|
-
"100",
|
|
513
|
-
decimals,
|
|
514
|
-
"BTC confirmation threshold"
|
|
515
|
-
);
|
|
516
|
-
if (input.amount >= highValueThreshold) {
|
|
517
|
-
return 3;
|
|
518
|
-
}
|
|
519
|
-
if (input.amount >= mediumValueThreshold) {
|
|
520
|
-
return 2;
|
|
521
|
-
}
|
|
522
|
-
return 1;
|
|
291
|
+
const decimals = currencyMinorUnitDecimals(input.currency);
|
|
292
|
+
const highValueThreshold = decimalToMinorUnitAmount("1000", decimals, "BTC confirmation threshold");
|
|
293
|
+
const mediumValueThreshold = decimalToMinorUnitAmount("100", decimals, "BTC confirmation threshold");
|
|
294
|
+
if (input.amount >= highValueThreshold) return 3;
|
|
295
|
+
if (input.amount >= mediumValueThreshold) return 2;
|
|
296
|
+
return 1;
|
|
523
297
|
}
|
|
524
298
|
function verifyBtcpayWebhookSignature(payload, signature, secret) {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
secret,
|
|
534
|
-
"BTCPay webhook secret"
|
|
535
|
-
);
|
|
536
|
-
const expected = createHmac("sha256", normalizedSecret).update(payload).digest("hex");
|
|
537
|
-
const actual = normalizedSignature.replace(/^sha256=/, "");
|
|
538
|
-
if (!/^[a-f0-9]{64}$/i.test(actual)) {
|
|
539
|
-
throw new PaymentProviderError("Invalid BTCPay webhook signature.");
|
|
540
|
-
}
|
|
541
|
-
const expectedBuffer = Buffer.from(expected, "hex");
|
|
542
|
-
const actualBuffer = Buffer.from(actual, "hex");
|
|
543
|
-
if (expectedBuffer.length !== actualBuffer.length || !timingSafeEqual(expectedBuffer, actualBuffer)) {
|
|
544
|
-
throw new PaymentProviderError("Invalid BTCPay webhook signature.");
|
|
545
|
-
}
|
|
299
|
+
if (typeof payload !== "string") throw new PaymentProviderError("BTCPay webhook payload must be a string.");
|
|
300
|
+
const normalizedSignature = normalizeNonEmptyString(signature, "BTCPay webhook signature");
|
|
301
|
+
const expected = createHmac("sha256", normalizeNonEmptyString(secret, "BTCPay webhook secret")).update(payload).digest("hex");
|
|
302
|
+
const actual = normalizedSignature.replace(/^sha256=/, "");
|
|
303
|
+
if (!/^[a-f0-9]{64}$/i.test(actual)) throw new PaymentProviderError("Invalid BTCPay webhook signature.");
|
|
304
|
+
const expectedBuffer = Buffer.from(expected, "hex");
|
|
305
|
+
const actualBuffer = Buffer.from(actual, "hex");
|
|
306
|
+
if (expectedBuffer.length !== actualBuffer.length || !timingSafeEqual(expectedBuffer, actualBuffer)) throw new PaymentProviderError("Invalid BTCPay webhook signature.");
|
|
546
307
|
}
|
|
547
308
|
function parseBtcpayWebhookPayload(payload) {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
throw error;
|
|
557
|
-
}
|
|
558
|
-
throw new PaymentProviderError("Invalid BTCPay webhook JSON payload.", {
|
|
559
|
-
cause: error
|
|
560
|
-
});
|
|
561
|
-
}
|
|
309
|
+
try {
|
|
310
|
+
const event = JSON.parse(payload);
|
|
311
|
+
if (!event || typeof event !== "object" || Array.isArray(event)) throw new PaymentProviderError("Invalid BTCPay webhook JSON payload.");
|
|
312
|
+
return event;
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if (error instanceof PaymentProviderError) throw error;
|
|
315
|
+
throw new PaymentProviderError("Invalid BTCPay webhook JSON payload.", { cause: error });
|
|
316
|
+
}
|
|
562
317
|
}
|
|
563
318
|
function normalizeOptionalWebhookString(value) {
|
|
564
|
-
|
|
565
|
-
|
|
319
|
+
const normalized = value?.trim();
|
|
320
|
+
return normalized ? normalized : void 0;
|
|
566
321
|
}
|
|
567
322
|
function normalizeOptionalProviderString(value) {
|
|
568
|
-
|
|
569
|
-
|
|
323
|
+
const normalized = value?.trim();
|
|
324
|
+
return normalized ? normalized : void 0;
|
|
570
325
|
}
|
|
571
326
|
function mapBtcpayStatus(status, additionalStatus, type) {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
"invoiceprocessing",
|
|
607
|
-
"invoicereceivedpayment",
|
|
608
|
-
"invoicepaymentsettling"
|
|
609
|
-
].includes(token)
|
|
610
|
-
)) {
|
|
611
|
-
return "processing";
|
|
612
|
-
}
|
|
613
|
-
return "pending";
|
|
327
|
+
const tokens = [
|
|
328
|
+
normalizeBtcpayToken(status),
|
|
329
|
+
normalizeBtcpayToken(additionalStatus),
|
|
330
|
+
normalizeBtcpayToken(type)
|
|
331
|
+
];
|
|
332
|
+
if (tokens.some((token) => [
|
|
333
|
+
"incomplete",
|
|
334
|
+
"invoiceincomplete",
|
|
335
|
+
"new",
|
|
336
|
+
"unpaid"
|
|
337
|
+
].includes(token))) return "pending";
|
|
338
|
+
if (tokens.some((token) => [
|
|
339
|
+
"settled",
|
|
340
|
+
"complete",
|
|
341
|
+
"confirmed",
|
|
342
|
+
"invoicesettled",
|
|
343
|
+
"invoicepaymentsettled"
|
|
344
|
+
].includes(token))) return "confirmed";
|
|
345
|
+
if (tokens.some((token) => ["expired", "invoiceexpired"].includes(token))) return "expired";
|
|
346
|
+
if (tokens.some((token) => [
|
|
347
|
+
"invalid",
|
|
348
|
+
"failed",
|
|
349
|
+
"invoiceinvalid"
|
|
350
|
+
].includes(token))) return "failed";
|
|
351
|
+
if (tokens.some((token) => [
|
|
352
|
+
"processing",
|
|
353
|
+
"paid",
|
|
354
|
+
"paidlate",
|
|
355
|
+
"paidpartial",
|
|
356
|
+
"invoiceprocessing",
|
|
357
|
+
"invoicereceivedpayment",
|
|
358
|
+
"invoicepaymentsettling"
|
|
359
|
+
].includes(token))) return "processing";
|
|
360
|
+
return "pending";
|
|
614
361
|
}
|
|
615
362
|
function normalizeBtcpayToken(value) {
|
|
616
|
-
|
|
363
|
+
return value?.trim().toLowerCase().replace(/[^a-z0-9]/g, "") ?? "";
|
|
617
364
|
}
|
|
618
365
|
function enforceBtcConfirmations(status, confirmations, requiredConfirmations) {
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
return status;
|
|
624
|
-
}
|
|
625
|
-
if (confirmations === void 0) {
|
|
626
|
-
return "processing";
|
|
627
|
-
}
|
|
628
|
-
return confirmations >= requiredConfirmations ? "confirmed" : "processing";
|
|
366
|
+
if (status !== "confirmed") return status;
|
|
367
|
+
if (requiredConfirmations <= 0) return status;
|
|
368
|
+
if (confirmations === void 0) return "processing";
|
|
369
|
+
return confirmations >= requiredConfirmations ? "confirmed" : "processing";
|
|
629
370
|
}
|
|
630
371
|
function normalizeConfirmationCount(value, label) {
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
`${label} must be a non-negative integer, received ${String(value)}.`
|
|
634
|
-
);
|
|
635
|
-
}
|
|
636
|
-
return value;
|
|
372
|
+
if (!Number.isInteger(value) || value < 0) throw new PaymentConfigurationError(`${label} must be a non-negative integer, received ${String(value)}.`);
|
|
373
|
+
return value;
|
|
637
374
|
}
|
|
638
375
|
function normalizeCurrencyCode(value) {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
}
|
|
644
|
-
const normalized = value.trim().toUpperCase();
|
|
645
|
-
if (!/^[A-Z]{3}$/.test(normalized)) {
|
|
646
|
-
throw new PaymentConfigurationError(
|
|
647
|
-
`BtcAdapter currency must be a three-letter currency code, received ${String(value)}.`
|
|
648
|
-
);
|
|
649
|
-
}
|
|
650
|
-
return normalized;
|
|
376
|
+
if (typeof value !== "string") throw new PaymentConfigurationError(`BtcAdapter currency must be a three-letter currency code, received ${String(value)}.`);
|
|
377
|
+
const normalized = value.trim().toUpperCase();
|
|
378
|
+
if (!/^[A-Z]{3}$/.test(normalized)) throw new PaymentConfigurationError(`BtcAdapter currency must be a three-letter currency code, received ${String(value)}.`);
|
|
379
|
+
return normalized;
|
|
651
380
|
}
|
|
652
381
|
function normalizePaymentMethod(value) {
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
}
|
|
658
|
-
const normalized = value.trim();
|
|
659
|
-
if (!normalized) {
|
|
660
|
-
throw new PaymentConfigurationError(
|
|
661
|
-
"BtcAdapter paymentMethod must not be empty when configured."
|
|
662
|
-
);
|
|
663
|
-
}
|
|
664
|
-
return normalized;
|
|
382
|
+
if (typeof value !== "string") throw new PaymentConfigurationError("BtcAdapter paymentMethod must be a string.");
|
|
383
|
+
const normalized = value.trim();
|
|
384
|
+
if (!normalized) throw new PaymentConfigurationError("BtcAdapter paymentMethod must not be empty when configured.");
|
|
385
|
+
return normalized;
|
|
665
386
|
}
|
|
666
387
|
function normalizeBtcpayBaseUrl(value) {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
"BtcAdapter baseUrl must be an origin URL without path, query, or fragment."
|
|
672
|
-
);
|
|
673
|
-
}
|
|
674
|
-
return parsed.origin;
|
|
388
|
+
const normalized = normalizeUrlString(value, "BtcAdapter baseUrl").trim().replace(/\/$/, "");
|
|
389
|
+
const parsed = new URL(normalized);
|
|
390
|
+
if (parsed.search || parsed.hash || !["", "/"].includes(parsed.pathname)) throw new PaymentConfigurationError("BtcAdapter baseUrl must be an origin URL without path, query, or fragment.");
|
|
391
|
+
return parsed.origin;
|
|
675
392
|
}
|
|
676
393
|
function extractPaymentMethods(value, requireUsable = true) {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
paymentMethod: method.paymentMethod ?? key
|
|
701
|
-
};
|
|
702
|
-
});
|
|
703
|
-
return requireUsable ? methods.filter(isUsablePaymentMethod) : methods;
|
|
394
|
+
if (!value) return;
|
|
395
|
+
if (Array.isArray(value)) {
|
|
396
|
+
const methods = value.filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item)).map(readPaymentMethod);
|
|
397
|
+
return requireUsable ? methods.filter(isUsablePaymentMethod) : methods;
|
|
398
|
+
}
|
|
399
|
+
if (typeof value !== "object") return;
|
|
400
|
+
const record = value;
|
|
401
|
+
for (const key of [
|
|
402
|
+
"paymentMethods",
|
|
403
|
+
"paymentMethodDetails",
|
|
404
|
+
"methods"
|
|
405
|
+
]) {
|
|
406
|
+
const nested = extractPaymentMethods(record[key], requireUsable);
|
|
407
|
+
if (nested?.length) return nested;
|
|
408
|
+
}
|
|
409
|
+
const methods = Object.entries(record).filter(([key, item]) => key.toUpperCase().includes("BTC") && item).map(([key, item]) => {
|
|
410
|
+
const method = readPaymentMethod(item);
|
|
411
|
+
return {
|
|
412
|
+
...method,
|
|
413
|
+
paymentMethod: method.paymentMethod ?? key
|
|
414
|
+
};
|
|
415
|
+
});
|
|
416
|
+
return requireUsable ? methods.filter(isUsablePaymentMethod) : methods;
|
|
704
417
|
}
|
|
705
418
|
function selectBtcPaymentMethod(methods, paymentMethod) {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
419
|
+
const target = paymentMethod.toUpperCase();
|
|
420
|
+
const aliases = target === "BTC" || target === "BTC-CHAIN" ? [
|
|
421
|
+
"BTC-CHAIN",
|
|
422
|
+
"BTC",
|
|
423
|
+
"BITCOIN"
|
|
424
|
+
] : [target];
|
|
425
|
+
return methods.find((method) => paymentMethodMatchesAlias(method, aliases));
|
|
709
426
|
}
|
|
710
427
|
function paymentMethodMatchesAlias(method, aliases) {
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
const cryptoCode = method.cryptoCode?.trim().toUpperCase();
|
|
716
|
-
return cryptoCode !== void 0 && aliases.includes(cryptoCode);
|
|
428
|
+
const methodIdentifiers = [method.paymentMethod, method.paymentMethodId].map((value) => value?.trim().toUpperCase()).filter((value) => Boolean(value));
|
|
429
|
+
if (methodIdentifiers.length > 0) return methodIdentifiers.some((value) => aliases.includes(value));
|
|
430
|
+
const cryptoCode = method.cryptoCode?.trim().toUpperCase();
|
|
431
|
+
return cryptoCode !== void 0 && aliases.includes(cryptoCode);
|
|
717
432
|
}
|
|
718
433
|
function readPaymentMethod(value) {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
"confirmationCount"
|
|
731
|
-
])
|
|
732
|
-
};
|
|
434
|
+
return {
|
|
435
|
+
decimalAmount: readDecimalString(value, "amount") ?? readDecimalString(value, "due"),
|
|
436
|
+
destination: readProviderString(value, "destination") ?? readProviderString(value, "address") ?? readProviderString(value, "paymentAddress"),
|
|
437
|
+
paymentLink: readProviderString(value, "paymentLink") ?? readProviderString(value, "paymentUrl"),
|
|
438
|
+
paymentMethod: readProviderString(value, "paymentMethod") ?? readProviderString(value, "paymentMethodId"),
|
|
439
|
+
paymentMethodId: readProviderString(value, "paymentMethodId"),
|
|
440
|
+
cryptoCode: readProviderString(value, "cryptoCode"),
|
|
441
|
+
rate: readDecimalString(value, "rate"),
|
|
442
|
+
networkFee: readDecimalString(value, "networkFee"),
|
|
443
|
+
confirmations: findNestedNonNegativeInteger(value, ["confirmations", "confirmationCount"])
|
|
444
|
+
};
|
|
733
445
|
}
|
|
734
446
|
function isUsablePaymentMethod(method) {
|
|
735
|
-
|
|
447
|
+
return Boolean(method.destination && method.decimalAmount);
|
|
736
448
|
}
|
|
737
449
|
function maxConfirmationCount(methods) {
|
|
738
|
-
|
|
739
|
-
|
|
450
|
+
const confirmations = methods.map((method) => method.confirmations).filter((value) => value !== void 0);
|
|
451
|
+
return confirmations.length === 0 ? void 0 : Math.max(...confirmations);
|
|
740
452
|
}
|
|
741
453
|
function readString(value, key) {
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
if (typeof item === "string") {
|
|
747
|
-
return item;
|
|
748
|
-
}
|
|
749
|
-
if (typeof item === "number") {
|
|
750
|
-
return String(item);
|
|
751
|
-
}
|
|
752
|
-
return void 0;
|
|
454
|
+
if (!value || typeof value !== "object") return;
|
|
455
|
+
const item = value[key];
|
|
456
|
+
if (typeof item === "string") return item;
|
|
457
|
+
if (typeof item === "number") return String(item);
|
|
753
458
|
}
|
|
754
459
|
function readProviderString(value, key) {
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
const item = value[key];
|
|
759
|
-
return typeof item === "string" ? item : void 0;
|
|
460
|
+
if (!value || typeof value !== "object") return;
|
|
461
|
+
const item = value[key];
|
|
462
|
+
return typeof item === "string" ? item : void 0;
|
|
760
463
|
}
|
|
761
464
|
function findNestedString(value, keys, depth = 0) {
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
for (const child of Object.values(record)) {
|
|
773
|
-
const match = findNestedString(child, keys, depth + 1);
|
|
774
|
-
if (match) {
|
|
775
|
-
return match;
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
return void 0;
|
|
465
|
+
if (!value || typeof value !== "object" || depth > 16) return;
|
|
466
|
+
const record = value;
|
|
467
|
+
for (const key of keys) {
|
|
468
|
+
const direct = readString(record, key);
|
|
469
|
+
if (direct) return direct;
|
|
470
|
+
}
|
|
471
|
+
for (const child of Object.values(record)) {
|
|
472
|
+
const match = findNestedString(child, keys, depth + 1);
|
|
473
|
+
if (match) return match;
|
|
474
|
+
}
|
|
779
475
|
}
|
|
780
476
|
function findNestedProviderString(value, keys, depth = 0) {
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
for (const child of Object.values(record)) {
|
|
792
|
-
const match = findNestedProviderString(child, keys, depth + 1);
|
|
793
|
-
if (match) {
|
|
794
|
-
return match;
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
return void 0;
|
|
477
|
+
if (!value || typeof value !== "object" || depth > 16) return;
|
|
478
|
+
const record = value;
|
|
479
|
+
for (const key of keys) {
|
|
480
|
+
const direct = readProviderString(record, key);
|
|
481
|
+
if (direct) return direct;
|
|
482
|
+
}
|
|
483
|
+
for (const child of Object.values(record)) {
|
|
484
|
+
const match = findNestedProviderString(child, keys, depth + 1);
|
|
485
|
+
if (match) return match;
|
|
486
|
+
}
|
|
798
487
|
}
|
|
799
488
|
function findNestedNonNegativeInteger(value, keys) {
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
const parsed = Number(raw);
|
|
805
|
-
return Number.isSafeInteger(parsed) ? parsed : void 0;
|
|
489
|
+
const raw = findNestedString(value, keys);
|
|
490
|
+
if (raw === void 0 || !/^\d+$/.test(raw)) return;
|
|
491
|
+
const parsed = Number(raw);
|
|
492
|
+
return Number.isSafeInteger(parsed) ? parsed : void 0;
|
|
806
493
|
}
|
|
807
494
|
function findNestedDecimalString(value, keys, depth = 0) {
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
for (const child of Object.values(record)) {
|
|
819
|
-
const match = findNestedDecimalString(child, keys, depth + 1);
|
|
820
|
-
if (match) {
|
|
821
|
-
return match;
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
return void 0;
|
|
495
|
+
if (!value || typeof value !== "object" || depth > 16) return;
|
|
496
|
+
const record = value;
|
|
497
|
+
for (const key of keys) {
|
|
498
|
+
const direct = readDecimalString(record, key);
|
|
499
|
+
if (direct) return direct;
|
|
500
|
+
}
|
|
501
|
+
for (const child of Object.values(record)) {
|
|
502
|
+
const match = findNestedDecimalString(child, keys, depth + 1);
|
|
503
|
+
if (match) return match;
|
|
504
|
+
}
|
|
825
505
|
}
|
|
826
506
|
function readDecimalString(value, key) {
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
return void 0;
|
|
835
|
-
}
|
|
507
|
+
const item = value[key];
|
|
508
|
+
if (typeof item !== "string") return;
|
|
509
|
+
try {
|
|
510
|
+
return normalizeAmount(item);
|
|
511
|
+
} catch {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
836
514
|
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
verifyBtcpayWebhookSignature
|
|
842
|
-
};
|
|
843
|
-
//# sourceMappingURL=btc.js.map
|
|
515
|
+
//#endregion
|
|
516
|
+
export { BTC_BACKEND_ID, BtcAdapter, defaultBtcConfirmationPolicy, verifyBtcpayWebhookSignature };
|
|
517
|
+
|
|
518
|
+
//# sourceMappingURL=btc.js.map
|