@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.
@@ -0,0 +1,843 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ import { P as PaymentConfigurationError, b as PaymentProviderError } from "../chunks/errors-BgFC46qQ.js";
3
+ import { g as getFetch, a as normalizeMaxStoredPaymentOptions, n as normalizeNonEmptyString, c as normalizeFutureDate, o as normalizePositivePaymentAmount, m as minorUnitsToDecimal, k as currencyMinorUnitDecimals, q as normalizeAmount, f as decimalToMinorUnitAmount, r as rememberPaymentOption, p as pollPaymentStatus, i as normalizeMinorUnitAmount, h as normalizeDate, e as normalizePositiveMinorUnitAmount, j as readJsonResponse, s as applyExpiryToPendingStatus, b as normalizeUrlString } from "../chunks/shared-DGHSqDQT.js";
4
+ const BTC_BACKEND_ID = "btc";
5
+ const DEFAULT_MAX_STORED_WEBHOOK_DELIVERY_IDS = 5e4;
6
+ class BtcAdapter {
7
+ constructor(options) {
8
+ this.options = options;
9
+ if (typeof options.baseUrl !== "string" || !options.baseUrl.trim()) {
10
+ throw new PaymentConfigurationError("BtcAdapter requires baseUrl.");
11
+ }
12
+ if (typeof options.apiKey !== "string" || !options.apiKey.trim()) {
13
+ throw new PaymentConfigurationError("BtcAdapter requires apiKey.");
14
+ }
15
+ if (typeof options.storeId !== "string" || !options.storeId.trim()) {
16
+ throw new PaymentConfigurationError("BtcAdapter requires storeId.");
17
+ }
18
+ if (options.webhookSecret !== void 0) {
19
+ if (typeof options.webhookSecret !== "string") {
20
+ throw new PaymentConfigurationError(
21
+ "BtcAdapter webhookSecret must be a string when configured."
22
+ );
23
+ }
24
+ if (!options.webhookSecret.trim()) {
25
+ throw new PaymentConfigurationError(
26
+ "BtcAdapter webhookSecret must not be empty when configured."
27
+ );
28
+ }
29
+ }
30
+ this.fetch = getFetch(options.fetch);
31
+ this.maxStoredPaymentOptions = normalizeMaxStoredPaymentOptions(
32
+ options.maxStoredPaymentOptions,
33
+ "BtcAdapter maxStoredPaymentOptions"
34
+ );
35
+ this.maxStoredWebhookDeliveryIds = normalizeMaxStoredPaymentOptions(
36
+ options.maxStoredWebhookDeliveryIds ?? DEFAULT_MAX_STORED_WEBHOOK_DELIVERY_IDS,
37
+ "BtcAdapter maxStoredWebhookDeliveryIds"
38
+ );
39
+ this.baseUrl = normalizeBtcpayBaseUrl(options.baseUrl);
40
+ this.apiKey = options.apiKey.trim();
41
+ this.storeId = options.storeId.trim();
42
+ this.currency = normalizeCurrencyCode(options.currency ?? "USD");
43
+ this.paymentMethod = normalizePaymentMethod(
44
+ options.paymentMethod ?? "BTC-CHAIN"
45
+ );
46
+ this.webhookSecret = options.webhookSecret?.trim();
47
+ const managedProviderName = normalizeOptionalProviderString(
48
+ options.managedProviderName
49
+ );
50
+ this.capabilities = {
51
+ id: BTC_BACKEND_ID,
52
+ displayName: "Bitcoin via BTCPay Server",
53
+ settlementCurrency: "BTC",
54
+ supportedSettlementCurrencies: ["BTC"],
55
+ chain: "bitcoin",
56
+ settlementShape: "address",
57
+ x402Capable: false,
58
+ confirmationLatency: {
59
+ expectedSeconds: 600,
60
+ maxExpectedSeconds: 1800,
61
+ minConfirmations: 1,
62
+ description: "Tiered Bitcoin confirmation policy: 1 confirmation below $100, 2 below $1,000, 3 at $1,000 and above."
63
+ },
64
+ supportsRefunds: true,
65
+ supportsPayouts: true,
66
+ supportsWebhooks: true,
67
+ metadata: {
68
+ provider: "btcpay-server",
69
+ managedProviderName
70
+ }
71
+ };
72
+ }
73
+ capabilities;
74
+ fetch;
75
+ baseUrl;
76
+ apiKey;
77
+ storeId;
78
+ currency;
79
+ paymentMethod;
80
+ webhookSecret;
81
+ maxStoredPaymentOptions;
82
+ maxStoredWebhookDeliveryIds;
83
+ optionsByQuote = /* @__PURE__ */ new Map();
84
+ seenWebhookDeliveryIds = /* @__PURE__ */ new Set();
85
+ async createPaymentOption(input) {
86
+ const quoteId = normalizeNonEmptyString(input.quoteId, "BTC quoteId");
87
+ const expiresAt = normalizeFutureDate(
88
+ input.expiresAt,
89
+ "BTCPay invoice expiry"
90
+ );
91
+ const currency = normalizeCurrencyCode(input.currency);
92
+ const amount = normalizePositivePaymentAmount(
93
+ input.amount,
94
+ currency,
95
+ "BTCPay invoice amount"
96
+ );
97
+ const invoiceAmount = minorUnitsToDecimal(
98
+ amount,
99
+ currencyMinorUnitDecimals(currency)
100
+ );
101
+ const invoice = await this.request(
102
+ `/api/v1/stores/${encodeURIComponent(this.storeId)}/invoices`,
103
+ {
104
+ method: "POST",
105
+ body: JSON.stringify({
106
+ amount: invoiceAmount,
107
+ currency,
108
+ metadata: {
109
+ ...input.metadata,
110
+ quoteId,
111
+ buyerEmail: input.buyerEmail,
112
+ description: input.description
113
+ },
114
+ checkout: {
115
+ expirationMinutes: Math.max(
116
+ 1,
117
+ Math.ceil((expiresAt.getTime() - Date.now()) / 6e4)
118
+ )
119
+ },
120
+ additionalSearchTerms: [quoteId]
121
+ })
122
+ }
123
+ );
124
+ const invoiceId = normalizeOptionalProviderString(
125
+ readProviderString(invoice, "id")
126
+ );
127
+ const methods = extractPaymentMethods(invoice) ?? (invoiceId ? await this.getPaymentMethods(invoiceId) : []);
128
+ const method = selectBtcPaymentMethod(methods, this.paymentMethod);
129
+ const settlementAmountDecimal = method?.decimalAmount === void 0 ? void 0 : normalizeAmount(method.decimalAmount);
130
+ const settlementAmount = settlementAmountDecimal === void 0 ? void 0 : decimalToMinorUnitAmount(
131
+ settlementAmountDecimal,
132
+ currencyMinorUnitDecimals("BTC"),
133
+ "BTC settlement"
134
+ );
135
+ const payTo = method?.destination?.trim();
136
+ if (!invoiceId) {
137
+ throw new PaymentProviderError(
138
+ "BTCPay invoice response did not include an invoice id."
139
+ );
140
+ }
141
+ if (!method || !payTo || settlementAmount === void 0 || settlementAmount === 0) {
142
+ throw new PaymentProviderError(
143
+ `BTCPay invoice response did not include a ${this.paymentMethod} payment address and amount.`
144
+ );
145
+ }
146
+ const requiredConfirmations = this.getRequiredConfirmations({
147
+ quoteId,
148
+ amount,
149
+ currency
150
+ });
151
+ const option = {
152
+ backendId: this.capabilities.id,
153
+ quoteId,
154
+ payTo,
155
+ settlementShape: "address",
156
+ settlementCurrency: "BTC",
157
+ settlementAmount,
158
+ amount,
159
+ currency,
160
+ expiresAt,
161
+ providerPaymentId: invoiceId,
162
+ paymentUri: normalizeOptionalProviderString(method?.paymentLink),
163
+ metadata: {
164
+ invoiceId,
165
+ checkoutLink: normalizeOptionalProviderString(
166
+ readString(invoice, "checkoutLink")
167
+ ),
168
+ requiredConfirmations,
169
+ rate: method?.rate,
170
+ networkFee: method?.networkFee
171
+ },
172
+ requiredConfirmations
173
+ };
174
+ rememberPaymentOption(
175
+ this.optionsByQuote,
176
+ quoteId,
177
+ option,
178
+ this.maxStoredPaymentOptions
179
+ );
180
+ return option;
181
+ }
182
+ watchPayment(input) {
183
+ return pollPaymentStatus(
184
+ {
185
+ ...input,
186
+ pollIntervalMs: input.pollIntervalMs ?? this.options.pollIntervalMs ?? 3e4
187
+ },
188
+ () => this.getStatus(input.quoteId, input.payTo, input.statusContext)
189
+ );
190
+ }
191
+ async getStatus(quoteId, payTo, context = {}) {
192
+ const normalizedQuoteId = normalizeNonEmptyString(quoteId, "BTC quoteId");
193
+ const normalizedPayTo = normalizeNonEmptyString(payTo, "BTC payTo");
194
+ const contextProviderPaymentId = context.providerPaymentId === void 0 ? void 0 : normalizeNonEmptyString(
195
+ context.providerPaymentId,
196
+ "BTC providerPaymentId"
197
+ );
198
+ const option = this.optionsByQuote.get(normalizedQuoteId);
199
+ const contextAmount = context.amount === void 0 ? void 0 : normalizeMinorUnitAmount(context.amount, "BTC status");
200
+ const contextCurrency = context.currency === void 0 ? void 0 : normalizeCurrencyCode(context.currency);
201
+ const contextSettlementAmount = context.settlementAmount === void 0 ? void 0 : normalizeMinorUnitAmount(
202
+ context.settlementAmount,
203
+ "BTC settlement status"
204
+ );
205
+ const invoice = option?.providerPaymentId !== void 0 || contextProviderPaymentId !== void 0 ? await this.getInvoice(
206
+ option?.providerPaymentId ?? contextProviderPaymentId ?? ""
207
+ ) : await this.findInvoice(normalizedQuoteId);
208
+ const invoiceId = normalizeOptionalProviderString(readProviderString(invoice, "id")) ?? option?.providerPaymentId;
209
+ const status = mapBtcpayStatus(
210
+ readString(invoice, "status"),
211
+ readString(invoice, "additionalStatus")
212
+ );
213
+ const expiresAt = option?.expiresAt ?? (context.expiresAt === void 0 ? void 0 : normalizeDate(context.expiresAt));
214
+ const statusWithExpiry = applyExpiryToPendingStatus(status, expiresAt);
215
+ let confirmations = findNestedNonNegativeInteger(invoice, [
216
+ "confirmations",
217
+ "confirmationCount"
218
+ ]);
219
+ const contextRequiredConfirmations = context.requiredConfirmations === void 0 ? void 0 : normalizeConfirmationCount(
220
+ context.requiredConfirmations,
221
+ "BTC requiredConfirmations"
222
+ );
223
+ const requiredConfirmations = option?.requiredConfirmations ?? contextRequiredConfirmations ?? this.getRequiredConfirmations({
224
+ quoteId: normalizedQuoteId,
225
+ amount: option?.amount ?? contextAmount ?? decimalToMinorUnitAmount(
226
+ readDecimalString(invoice, "amount") ?? "0",
227
+ currencyMinorUnitDecimals(
228
+ option?.currency ?? contextCurrency ?? this.currency
229
+ ),
230
+ "BTCPay invoice"
231
+ ),
232
+ currency: option?.currency ?? contextCurrency ?? this.currency
233
+ });
234
+ if (statusWithExpiry === "confirmed" && confirmations === void 0 && invoiceId) {
235
+ try {
236
+ confirmations = maxConfirmationCount(
237
+ await this.getPaymentMethods(invoiceId, { requireUsable: false })
238
+ );
239
+ } catch (error) {
240
+ if (!(error instanceof PaymentProviderError)) {
241
+ throw error;
242
+ }
243
+ }
244
+ }
245
+ const effectiveStatus = enforceBtcConfirmations(
246
+ statusWithExpiry,
247
+ confirmations,
248
+ requiredConfirmations
249
+ );
250
+ const invoiceSettlementAmount = findNestedDecimalString(invoice, [
251
+ "btcDue",
252
+ "amountDue"
253
+ ]);
254
+ const receivedSettlementAmount = readDecimalString(invoice, "amountPaid") ?? findNestedDecimalString(invoice, ["paid"]);
255
+ return {
256
+ backendId: this.capabilities.id,
257
+ quoteId: normalizedQuoteId,
258
+ payTo: normalizedPayTo,
259
+ status: effectiveStatus,
260
+ settlementCurrency: "BTC",
261
+ settlementAmount: option?.settlementAmount ?? contextSettlementAmount ?? (invoiceSettlementAmount === void 0 ? void 0 : decimalToMinorUnitAmount(
262
+ invoiceSettlementAmount,
263
+ currencyMinorUnitDecimals("BTC"),
264
+ "BTC settlement"
265
+ )),
266
+ receivedAmount: receivedSettlementAmount === void 0 ? void 0 : decimalToMinorUnitAmount(
267
+ receivedSettlementAmount,
268
+ currencyMinorUnitDecimals("BTC"),
269
+ "BTC received"
270
+ ),
271
+ amount: option?.amount ?? contextAmount,
272
+ currency: option?.currency ?? contextCurrency,
273
+ requiredConfirmations,
274
+ confirmations,
275
+ providerPaymentId: invoiceId,
276
+ transactionId: normalizeOptionalProviderString(
277
+ findNestedProviderString(invoice, ["transactionId", "txid"])
278
+ ),
279
+ updatedAt: /* @__PURE__ */ new Date(),
280
+ raw: invoice
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
+ }
493
+ function invoiceMatchesQuote(invoice, quoteId) {
494
+ if (!invoice || typeof invoice !== "object") {
495
+ return false;
496
+ }
497
+ const record = invoice;
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;
503
+ }
504
+ function defaultBtcConfirmationPolicy(input) {
505
+ const decimals = currencyMinorUnitDecimals(input.currency);
506
+ const highValueThreshold = decimalToMinorUnitAmount(
507
+ "1000",
508
+ decimals,
509
+ "BTC confirmation threshold"
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;
523
+ }
524
+ function verifyBtcpayWebhookSignature(payload, signature, secret) {
525
+ if (typeof payload !== "string") {
526
+ throw new PaymentProviderError("BTCPay webhook payload must be a string.");
527
+ }
528
+ const normalizedSignature = normalizeNonEmptyString(
529
+ signature,
530
+ "BTCPay webhook signature"
531
+ );
532
+ const normalizedSecret = normalizeNonEmptyString(
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
+ }
546
+ }
547
+ function parseBtcpayWebhookPayload(payload) {
548
+ try {
549
+ const event = JSON.parse(payload);
550
+ if (!event || typeof event !== "object" || Array.isArray(event)) {
551
+ throw new PaymentProviderError("Invalid BTCPay webhook JSON payload.");
552
+ }
553
+ return event;
554
+ } catch (error) {
555
+ if (error instanceof PaymentProviderError) {
556
+ throw error;
557
+ }
558
+ throw new PaymentProviderError("Invalid BTCPay webhook JSON payload.", {
559
+ cause: error
560
+ });
561
+ }
562
+ }
563
+ function normalizeOptionalWebhookString(value) {
564
+ const normalized = value?.trim();
565
+ return normalized ? normalized : void 0;
566
+ }
567
+ function normalizeOptionalProviderString(value) {
568
+ const normalized = value?.trim();
569
+ return normalized ? normalized : void 0;
570
+ }
571
+ function mapBtcpayStatus(status, additionalStatus, type) {
572
+ const statusToken = normalizeBtcpayToken(status);
573
+ const additionalStatusToken = normalizeBtcpayToken(additionalStatus);
574
+ const typeToken = normalizeBtcpayToken(type);
575
+ const tokens = [statusToken, additionalStatusToken, typeToken];
576
+ if (tokens.some(
577
+ (token) => ["incomplete", "invoiceincomplete", "new", "unpaid"].includes(token)
578
+ )) {
579
+ return "pending";
580
+ }
581
+ if (tokens.some(
582
+ (token) => [
583
+ "settled",
584
+ "complete",
585
+ "confirmed",
586
+ "invoicesettled",
587
+ "invoicepaymentsettled"
588
+ ].includes(token)
589
+ )) {
590
+ return "confirmed";
591
+ }
592
+ if (tokens.some((token) => ["expired", "invoiceexpired"].includes(token))) {
593
+ return "expired";
594
+ }
595
+ if (tokens.some(
596
+ (token) => ["invalid", "failed", "invoiceinvalid"].includes(token)
597
+ )) {
598
+ return "failed";
599
+ }
600
+ if (tokens.some(
601
+ (token) => [
602
+ "processing",
603
+ "paid",
604
+ "paidlate",
605
+ "paidpartial",
606
+ "invoiceprocessing",
607
+ "invoicereceivedpayment",
608
+ "invoicepaymentsettling"
609
+ ].includes(token)
610
+ )) {
611
+ return "processing";
612
+ }
613
+ return "pending";
614
+ }
615
+ function normalizeBtcpayToken(value) {
616
+ return value?.trim().toLowerCase().replace(/[^a-z0-9]/g, "") ?? "";
617
+ }
618
+ function enforceBtcConfirmations(status, confirmations, requiredConfirmations) {
619
+ if (status !== "confirmed") {
620
+ return status;
621
+ }
622
+ if (requiredConfirmations <= 0) {
623
+ return status;
624
+ }
625
+ if (confirmations === void 0) {
626
+ return "processing";
627
+ }
628
+ return confirmations >= requiredConfirmations ? "confirmed" : "processing";
629
+ }
630
+ function normalizeConfirmationCount(value, label) {
631
+ if (!Number.isInteger(value) || value < 0) {
632
+ throw new PaymentConfigurationError(
633
+ `${label} must be a non-negative integer, received ${String(value)}.`
634
+ );
635
+ }
636
+ return value;
637
+ }
638
+ function normalizeCurrencyCode(value) {
639
+ if (typeof value !== "string") {
640
+ throw new PaymentConfigurationError(
641
+ `BtcAdapter currency must be a three-letter currency code, received ${String(value)}.`
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;
651
+ }
652
+ function normalizePaymentMethod(value) {
653
+ if (typeof value !== "string") {
654
+ throw new PaymentConfigurationError(
655
+ "BtcAdapter paymentMethod must be a string."
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;
665
+ }
666
+ function normalizeBtcpayBaseUrl(value) {
667
+ const normalized = normalizeUrlString(value, "BtcAdapter baseUrl").trim().replace(/\/$/, "");
668
+ const parsed = new URL(normalized);
669
+ if (parsed.search || parsed.hash || !["", "/"].includes(parsed.pathname)) {
670
+ throw new PaymentConfigurationError(
671
+ "BtcAdapter baseUrl must be an origin URL without path, query, or fragment."
672
+ );
673
+ }
674
+ return parsed.origin;
675
+ }
676
+ function extractPaymentMethods(value, requireUsable = true) {
677
+ if (!value) {
678
+ return void 0;
679
+ }
680
+ if (Array.isArray(value)) {
681
+ const methods2 = value.filter(
682
+ (item) => Boolean(item) && typeof item === "object" && !Array.isArray(item)
683
+ ).map(readPaymentMethod);
684
+ return requireUsable ? methods2.filter(isUsablePaymentMethod) : methods2;
685
+ }
686
+ if (typeof value !== "object") {
687
+ return void 0;
688
+ }
689
+ const record = value;
690
+ for (const key of ["paymentMethods", "paymentMethodDetails", "methods"]) {
691
+ const nested = extractPaymentMethods(record[key], requireUsable);
692
+ if (nested?.length) {
693
+ return nested;
694
+ }
695
+ }
696
+ const methods = Object.entries(record).filter(([key, item]) => key.toUpperCase().includes("BTC") && item).map(([key, item]) => {
697
+ const method = readPaymentMethod(item);
698
+ return {
699
+ ...method,
700
+ paymentMethod: method.paymentMethod ?? key
701
+ };
702
+ });
703
+ return requireUsable ? methods.filter(isUsablePaymentMethod) : methods;
704
+ }
705
+ function selectBtcPaymentMethod(methods, paymentMethod) {
706
+ const target = paymentMethod.toUpperCase();
707
+ const aliases = target === "BTC" || target === "BTC-CHAIN" ? ["BTC-CHAIN", "BTC", "BITCOIN"] : [target];
708
+ return methods.find((method) => paymentMethodMatchesAlias(method, aliases));
709
+ }
710
+ function paymentMethodMatchesAlias(method, aliases) {
711
+ const methodIdentifiers = [method.paymentMethod, method.paymentMethodId].map((value) => value?.trim().toUpperCase()).filter((value) => Boolean(value));
712
+ if (methodIdentifiers.length > 0) {
713
+ return methodIdentifiers.some((value) => aliases.includes(value));
714
+ }
715
+ const cryptoCode = method.cryptoCode?.trim().toUpperCase();
716
+ return cryptoCode !== void 0 && aliases.includes(cryptoCode);
717
+ }
718
+ function readPaymentMethod(value) {
719
+ return {
720
+ decimalAmount: readDecimalString(value, "amount") ?? readDecimalString(value, "due"),
721
+ destination: readProviderString(value, "destination") ?? readProviderString(value, "address") ?? readProviderString(value, "paymentAddress"),
722
+ paymentLink: readProviderString(value, "paymentLink") ?? readProviderString(value, "paymentUrl"),
723
+ paymentMethod: readProviderString(value, "paymentMethod") ?? readProviderString(value, "paymentMethodId"),
724
+ paymentMethodId: readProviderString(value, "paymentMethodId"),
725
+ cryptoCode: readProviderString(value, "cryptoCode"),
726
+ rate: readDecimalString(value, "rate"),
727
+ networkFee: readDecimalString(value, "networkFee"),
728
+ confirmations: findNestedNonNegativeInteger(value, [
729
+ "confirmations",
730
+ "confirmationCount"
731
+ ])
732
+ };
733
+ }
734
+ function isUsablePaymentMethod(method) {
735
+ return Boolean(method.destination && method.decimalAmount);
736
+ }
737
+ function maxConfirmationCount(methods) {
738
+ const confirmations = methods.map((method) => method.confirmations).filter((value) => value !== void 0);
739
+ return confirmations.length === 0 ? void 0 : Math.max(...confirmations);
740
+ }
741
+ function readString(value, key) {
742
+ if (!value || typeof value !== "object") {
743
+ return void 0;
744
+ }
745
+ const item = value[key];
746
+ if (typeof item === "string") {
747
+ return item;
748
+ }
749
+ if (typeof item === "number") {
750
+ return String(item);
751
+ }
752
+ return void 0;
753
+ }
754
+ function readProviderString(value, key) {
755
+ if (!value || typeof value !== "object") {
756
+ return void 0;
757
+ }
758
+ const item = value[key];
759
+ return typeof item === "string" ? item : void 0;
760
+ }
761
+ function findNestedString(value, keys, depth = 0) {
762
+ if (!value || typeof value !== "object" || depth > 16) {
763
+ return void 0;
764
+ }
765
+ const record = value;
766
+ for (const key of keys) {
767
+ const direct = readString(record, key);
768
+ if (direct) {
769
+ return direct;
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;
779
+ }
780
+ function findNestedProviderString(value, keys, depth = 0) {
781
+ if (!value || typeof value !== "object" || depth > 16) {
782
+ return void 0;
783
+ }
784
+ const record = value;
785
+ for (const key of keys) {
786
+ const direct = readProviderString(record, key);
787
+ if (direct) {
788
+ return direct;
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;
798
+ }
799
+ function findNestedNonNegativeInteger(value, keys) {
800
+ const raw = findNestedString(value, keys);
801
+ if (raw === void 0 || !/^\d+$/.test(raw)) {
802
+ return void 0;
803
+ }
804
+ const parsed = Number(raw);
805
+ return Number.isSafeInteger(parsed) ? parsed : void 0;
806
+ }
807
+ function findNestedDecimalString(value, keys, depth = 0) {
808
+ if (!value || typeof value !== "object" || depth > 16) {
809
+ return void 0;
810
+ }
811
+ const record = value;
812
+ for (const key of keys) {
813
+ const direct = readDecimalString(record, key);
814
+ if (direct) {
815
+ return direct;
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;
825
+ }
826
+ function readDecimalString(value, key) {
827
+ const item = value[key];
828
+ if (typeof item !== "string") {
829
+ return void 0;
830
+ }
831
+ try {
832
+ return normalizeAmount(item);
833
+ } catch {
834
+ return void 0;
835
+ }
836
+ }
837
+ export {
838
+ BTC_BACKEND_ID,
839
+ BtcAdapter,
840
+ defaultBtcConfirmationPolicy,
841
+ verifyBtcpayWebhookSignature
842
+ };
843
+ //# sourceMappingURL=btc.js.map