@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.
@@ -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
- 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
- }
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
- 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;
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
- 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;
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
- 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
- }
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
- 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
- }
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
- const normalized = value?.trim();
565
- return normalized ? normalized : void 0;
319
+ const normalized = value?.trim();
320
+ return normalized ? normalized : void 0;
566
321
  }
567
322
  function normalizeOptionalProviderString(value) {
568
- const normalized = value?.trim();
569
- return normalized ? normalized : void 0;
323
+ const normalized = value?.trim();
324
+ return normalized ? normalized : void 0;
570
325
  }
571
326
  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";
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
- return value?.trim().toLowerCase().replace(/[^a-z0-9]/g, "") ?? "";
363
+ return value?.trim().toLowerCase().replace(/[^a-z0-9]/g, "") ?? "";
617
364
  }
618
365
  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";
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
- 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;
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
- 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;
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
- 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;
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
- 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;
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
- 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;
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
- 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));
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
- 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);
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
- 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
- };
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
- return Boolean(method.destination && method.decimalAmount);
447
+ return Boolean(method.destination && method.decimalAmount);
736
448
  }
737
449
  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);
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
- 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;
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
- if (!value || typeof value !== "object") {
756
- return void 0;
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
- 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;
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
- 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;
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
- 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;
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
- 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;
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
- 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
- }
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
- export {
838
- BTC_BACKEND_ID,
839
- BtcAdapter,
840
- defaultBtcConfirmationPolicy,
841
- verifyBtcpayWebhookSignature
842
- };
843
- //# sourceMappingURL=btc.js.map
515
+ //#endregion
516
+ export { BTC_BACKEND_ID, BtcAdapter, defaultBtcConfirmationPolicy, verifyBtcpayWebhookSignature };
517
+
518
+ //# sourceMappingURL=btc.js.map