@happyvertical/payments 0.92.0 → 0.93.0
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/README.md +47 -0
- package/dist/adapters/btcpay-client.d.ts +175 -0
- package/dist/adapters/btcpay-client.d.ts.map +1 -0
- package/dist/adapters/btcpay-gateway.d.ts +26 -0
- package/dist/adapters/btcpay-gateway.d.ts.map +1 -0
- package/dist/adapters/btcpay.d.ts +4 -167
- package/dist/adapters/btcpay.d.ts.map +1 -1
- package/dist/adapters/btcpay.js +310 -5
- package/dist/adapters/btcpay.js.map +1 -1
- package/dist/checkout-gateway.d.ts +119 -0
- package/dist/checkout-gateway.d.ts.map +1 -0
- package/dist/chunks/checkout-gateway-DG-LY0oG.js +42 -0
- package/dist/chunks/checkout-gateway-DG-LY0oG.js.map +1 -0
- package/dist/chunks/errors-feZrwnTy.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/adapters/btcpay.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { r as PaymentProviderError, t as PaymentConfigurationError } from "../chunks/errors-feZrwnTy.js";
|
|
2
|
-
import {
|
|
1
|
+
import { a as PaymentVerificationError, r as PaymentProviderError, t as PaymentConfigurationError } from "../chunks/errors-feZrwnTy.js";
|
|
2
|
+
import { t as CryptoCheckoutNotOwnedError } from "../chunks/checkout-gateway-DG-LY0oG.js";
|
|
3
|
+
import { n as currencyMinorUnitDecimals, o as getFetch } from "../chunks/shared-DVl6UzD9.js";
|
|
3
4
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
4
|
-
//#region src/adapters/btcpay.ts
|
|
5
|
+
//#region src/adapters/btcpay-client.ts
|
|
5
6
|
/**
|
|
6
7
|
* A small, stateless client for the BTCPay Server Greenfield API (2.x).
|
|
7
8
|
*
|
|
@@ -83,6 +84,7 @@ var BtcpayClient = class {
|
|
|
83
84
|
for (const orderId of toList(input.orderId)) query.append("orderId", requireString(orderId, "BTCPay orderId"));
|
|
84
85
|
for (const status of toList(input.status)) query.append("status", status);
|
|
85
86
|
if (input.textSearch) query.set("textSearch", input.textSearch);
|
|
87
|
+
if (input.includeArchived) query.set("includeArchived", "true");
|
|
86
88
|
if (input.skip !== void 0) query.set("skip", String(requireCount(input.skip, "skip")));
|
|
87
89
|
if (input.take !== void 0) query.set("take", String(requireCount(input.take, "take")));
|
|
88
90
|
const search = query.toString();
|
|
@@ -210,7 +212,7 @@ function toPaymentMethod(value) {
|
|
|
210
212
|
paymentLink: readString(value, "paymentLink"),
|
|
211
213
|
rate: readDecimal(value.rate),
|
|
212
214
|
amount: readDecimal(value.amount) ?? "0",
|
|
213
|
-
paymentMethodPaid: readDecimal(value.paymentMethodPaid) ??
|
|
215
|
+
paymentMethodPaid: readDecimal(value.paymentMethodPaid) ?? readDecimal(value.paid),
|
|
214
216
|
totalPaid: readDecimal(value.totalPaid) ?? "0",
|
|
215
217
|
due: readDecimal(value.due),
|
|
216
218
|
payments: Array.isArray(value.payments) ? value.payments.filter(isRecord).map((payment) => ({
|
|
@@ -303,6 +305,309 @@ function readDate(value) {
|
|
|
303
305
|
}
|
|
304
306
|
}
|
|
305
307
|
//#endregion
|
|
306
|
-
|
|
308
|
+
//#region src/adapters/btcpay-gateway.ts
|
|
309
|
+
/**
|
|
310
|
+
* The BTCPay Server implementation of {@link CryptoCheckoutGateway}.
|
|
311
|
+
*
|
|
312
|
+
* Each checkout is a BTCPay invoice priced in fiat. BTCPay locks the rate for
|
|
313
|
+
* `expirationMinutes` and settles by the invoice `speedPolicy`; this adapter
|
|
314
|
+
* only maps BTCPay's states and never counts confirmations.
|
|
315
|
+
*/
|
|
316
|
+
var BTCPAY_GATEWAY_ID = "btcpay";
|
|
317
|
+
var LIVE_STATUSES = /* @__PURE__ */ new Set([
|
|
318
|
+
"New",
|
|
319
|
+
"Processing",
|
|
320
|
+
"Settled"
|
|
321
|
+
]);
|
|
322
|
+
var STATUS = /* @__PURE__ */ new Map([
|
|
323
|
+
["New", "open"],
|
|
324
|
+
["Processing", "confirming"],
|
|
325
|
+
["Settled", "settled"],
|
|
326
|
+
["Expired", "expired"],
|
|
327
|
+
["Invalid", "invalid"]
|
|
328
|
+
]);
|
|
329
|
+
var EXCEPTION = /* @__PURE__ */ new Map([
|
|
330
|
+
["None", "none"],
|
|
331
|
+
["Invalid", "none"],
|
|
332
|
+
["PaidPartial", "underpaid"],
|
|
333
|
+
["PaidOver", "overpaid"],
|
|
334
|
+
["PaidLate", "paid_late"],
|
|
335
|
+
["Marked", "manually_marked"]
|
|
336
|
+
]);
|
|
337
|
+
/**
|
|
338
|
+
* Marks invoices this adapter created. Reuse and listing consider only those,
|
|
339
|
+
* so an invoice made by hand (or by another integration) that happens to
|
|
340
|
+
* share an order id is never reused or read as one of this port's checkouts.
|
|
341
|
+
*/
|
|
342
|
+
var OWNER_KEY = "hvCheckoutGateway";
|
|
343
|
+
var OWNER_VALUE = "crypto-checkout:v1";
|
|
344
|
+
/** Keys this adapter writes into BTCPay invoice metadata itself. */
|
|
345
|
+
var RESERVED_METADATA = /* @__PURE__ */ new Set([
|
|
346
|
+
"orderId",
|
|
347
|
+
"itemDesc",
|
|
348
|
+
"buyerEmail",
|
|
349
|
+
OWNER_KEY
|
|
350
|
+
]);
|
|
351
|
+
var PAYMENT_STATUS = /* @__PURE__ */ new Map([
|
|
352
|
+
["Processing", "confirming"],
|
|
353
|
+
["Settled", "settled"],
|
|
354
|
+
["Invalid", "invalid"]
|
|
355
|
+
]);
|
|
356
|
+
function createBtcpayCheckoutGateway(options) {
|
|
357
|
+
const { client } = options;
|
|
358
|
+
if (!client) throw new PaymentConfigurationError("BTCPay gateway requires a client.");
|
|
359
|
+
if (typeof options.webhookSecret !== "string" || !options.webhookSecret) throw new PaymentConfigurationError("BTCPay gateway requires a webhookSecret.");
|
|
360
|
+
if (![
|
|
361
|
+
"HighSpeed",
|
|
362
|
+
"MediumSpeed",
|
|
363
|
+
"LowMediumSpeed",
|
|
364
|
+
"LowSpeed"
|
|
365
|
+
].includes(options.speedPolicy)) throw new PaymentConfigurationError("BTCPay gateway speedPolicy must be HighSpeed, MediumSpeed, LowMediumSpeed, or LowSpeed.");
|
|
366
|
+
const paymentMethods = options.paymentMethods ?? ["BTC-CHAIN"];
|
|
367
|
+
if (!Array.isArray(paymentMethods) || paymentMethods.length === 0 || paymentMethods.some((method) => typeof method !== "string" || !method)) throw new PaymentConfigurationError("BTCPay gateway paymentMethods must be a non-empty list of ids.");
|
|
368
|
+
if (new Set(paymentMethods.map((method) => assetOfMethodId(method))).size !== 1) throw new PaymentConfigurationError("BTCPay gateway paymentMethods must all settle in one asset (for example BTC-CHAIN and BTC-LN).");
|
|
369
|
+
const expirationMinutes = positiveInteger(options.expirationMinutes ?? 15, "expirationMinutes");
|
|
370
|
+
const monitoringMinutes = positiveInteger(options.monitoringMinutes ?? 1440, "monitoringMinutes");
|
|
371
|
+
const paymentTolerance = options.paymentTolerance ?? 0;
|
|
372
|
+
if (!Number.isFinite(paymentTolerance) || paymentTolerance < 0 || paymentTolerance > 100) throw new PaymentConfigurationError("BTCPay gateway paymentTolerance must be a percentage from 0 to 100.");
|
|
373
|
+
async function load(invoice) {
|
|
374
|
+
return toCheckout(invoice, await client.getInvoicePaymentMethods(invoice.id), options.rateSource);
|
|
375
|
+
}
|
|
376
|
+
async function liveInvoices(orderId) {
|
|
377
|
+
return (await client.listInvoices({
|
|
378
|
+
orderId,
|
|
379
|
+
includeArchived: true
|
|
380
|
+
})).filter((invoice) => ownedBy(invoice, orderId) && LIVE_STATUSES.has(invoice.status)).sort((a, b) => (a.createdTime?.getTime() ?? 0) - (b.createdTime?.getTime() ?? 0) || a.id.localeCompare(b.id));
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
id: BTCPAY_GATEWAY_ID,
|
|
384
|
+
async createCheckout(input) {
|
|
385
|
+
const orderId = requireText(input.orderId, "orderId");
|
|
386
|
+
const currency = requireText(input.currency, "currency").toUpperCase();
|
|
387
|
+
if (!Number.isSafeInteger(input.amount) || input.amount <= 0) throw new PaymentConfigurationError("Checkout amount must be positive integer minor units.");
|
|
388
|
+
const [existing] = await liveInvoices(orderId);
|
|
389
|
+
if (existing) {
|
|
390
|
+
const checkout = await load(existing);
|
|
391
|
+
if (checkout.currency !== currency || checkout.amount !== input.amount) throw new PaymentConfigurationError(`Order ${orderId} already has a checkout for ${checkout.amount} ${checkout.currency}; refusing to reuse it for ${input.amount} ${currency}.`);
|
|
392
|
+
return checkout;
|
|
393
|
+
}
|
|
394
|
+
const metadata = {};
|
|
395
|
+
for (const [key, value] of Object.entries(input.metadata ?? {})) {
|
|
396
|
+
if (RESERVED_METADATA.has(key)) throw new PaymentConfigurationError(`Checkout metadata key '${key}' is reserved.`);
|
|
397
|
+
if (typeof value !== "string") throw new PaymentConfigurationError(`Checkout metadata '${key}' must be a string.`);
|
|
398
|
+
metadata[key] = value;
|
|
399
|
+
}
|
|
400
|
+
metadata[OWNER_KEY] = OWNER_VALUE;
|
|
401
|
+
if (input.description) metadata.itemDesc = input.description;
|
|
402
|
+
if (input.buyerEmail) metadata.buyerEmail = input.buyerEmail;
|
|
403
|
+
const checkout = await load(await client.createInvoice({
|
|
404
|
+
amount: formatMinorUnits(input.amount, currencyMinorUnitDecimals(currency)),
|
|
405
|
+
currency,
|
|
406
|
+
orderId,
|
|
407
|
+
metadata,
|
|
408
|
+
checkout: {
|
|
409
|
+
speedPolicy: options.speedPolicy,
|
|
410
|
+
paymentMethods,
|
|
411
|
+
expirationMinutes,
|
|
412
|
+
monitoringMinutes,
|
|
413
|
+
paymentTolerance,
|
|
414
|
+
redirectURL: input.redirectUrl,
|
|
415
|
+
redirectAutomatically: input.redirectUrl ? true : void 0
|
|
416
|
+
}
|
|
417
|
+
}));
|
|
418
|
+
if (checkout.currency !== currency || checkout.amount !== input.amount) throw new PaymentProviderError(`BTCPay created invoice ${checkout.id} for ${checkout.amount} ${checkout.currency}; requested ${input.amount} ${currency}.`);
|
|
419
|
+
return checkout;
|
|
420
|
+
},
|
|
421
|
+
async getCheckout(checkoutId) {
|
|
422
|
+
const invoice = await client.getInvoice(requireText(checkoutId, "id"));
|
|
423
|
+
if (!isOwned(invoice)) throw new CryptoCheckoutNotOwnedError(invoice.id);
|
|
424
|
+
return load(invoice);
|
|
425
|
+
},
|
|
426
|
+
async listCheckouts(input) {
|
|
427
|
+
const orderId = requireText(input.orderId, "orderId");
|
|
428
|
+
const invoices = (await client.listInvoices({
|
|
429
|
+
orderId,
|
|
430
|
+
includeArchived: true
|
|
431
|
+
})).filter((invoice) => ownedBy(invoice, orderId));
|
|
432
|
+
return Promise.all(invoices.map(load));
|
|
433
|
+
},
|
|
434
|
+
verifyWebhook(rawBody, headers) {
|
|
435
|
+
if (!isValidBtcpayWebhookSignature(rawBody, readHeader(headers, "btcpay-sig"), options.webhookSecret)) throw new PaymentVerificationError("BTCPay webhook signature is invalid.");
|
|
436
|
+
let delivery;
|
|
437
|
+
try {
|
|
438
|
+
delivery = parseBtcpayWebhook(rawBody);
|
|
439
|
+
} catch (error) {
|
|
440
|
+
throw new PaymentVerificationError("BTCPay webhook body is invalid.", { cause: error });
|
|
441
|
+
}
|
|
442
|
+
if (delivery.storeId !== client.storeId) throw new PaymentVerificationError("BTCPay webhook belongs to another store.");
|
|
443
|
+
return {
|
|
444
|
+
eventId: delivery.originalDeliveryId ?? delivery.deliveryId,
|
|
445
|
+
checkoutId: delivery.invoiceId ?? null,
|
|
446
|
+
type: delivery.type,
|
|
447
|
+
redelivery: delivery.isRedelivery
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
function toCheckout(invoice, methods, rateSource) {
|
|
453
|
+
const status = STATUS.get(invoice.status);
|
|
454
|
+
if (!status) throw new PaymentProviderError(`BTCPay invoice ${invoice.id} has unknown status '${invoice.status}'.`);
|
|
455
|
+
const exception = EXCEPTION.get(invoice.additionalStatus);
|
|
456
|
+
if (!exception) throw new PaymentProviderError(`BTCPay invoice ${invoice.id} has unknown additionalStatus '${invoice.additionalStatus}'.`);
|
|
457
|
+
const currency = invoice.currency.toUpperCase();
|
|
458
|
+
const fiatDecimals = currencyMinorUnitDecimals(currency);
|
|
459
|
+
const amount = decimalToMinor(invoice.amount, fiatDecimals, "invoice");
|
|
460
|
+
let paidScaled = Decimal.zero();
|
|
461
|
+
for (const method of methods) {
|
|
462
|
+
const hasPayments = method.payments.some((payment) => paymentStatus(invoice.id, payment.status) !== "invalid");
|
|
463
|
+
if (method.paymentMethodPaid === void 0) {
|
|
464
|
+
if (hasPayments) throw new PaymentProviderError(`BTCPay invoice ${invoice.id} payment method ${method.paymentMethodId} has payments but no paid amount.`);
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
const paid = Decimal.parse(method.paymentMethodPaid, "paymentMethodPaid");
|
|
468
|
+
if (paid.isZero()) {
|
|
469
|
+
if (hasPayments) throw new PaymentProviderError(`BTCPay invoice ${invoice.id} payment method ${method.paymentMethodId} reports payments but zero paid.`);
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
if (method.rate === void 0) throw new PaymentProviderError(`BTCPay invoice ${invoice.id} payment method ${method.paymentMethodId} has payments but no rate.`);
|
|
473
|
+
paidScaled = paidScaled.add(paid.mul(Decimal.parse(method.rate, "rate")));
|
|
474
|
+
}
|
|
475
|
+
const primary = methods.find((method) => railOf(method.paymentMethodId) === "onchain") ?? methods[0];
|
|
476
|
+
const settlementAsset = primary ? assetOf(primary.currency, primary.paymentMethodId) : void 0;
|
|
477
|
+
const payments = [];
|
|
478
|
+
let nativePaid = Decimal.zero();
|
|
479
|
+
for (const method of methods) {
|
|
480
|
+
const asset = assetOf(method.currency, method.paymentMethodId);
|
|
481
|
+
for (const payment of method.payments) {
|
|
482
|
+
const mapped = {
|
|
483
|
+
id: payment.id,
|
|
484
|
+
asset,
|
|
485
|
+
rail: railOf(method.paymentMethodId),
|
|
486
|
+
amount: payment.value,
|
|
487
|
+
fee: Decimal.parse(payment.fee, "fee").isZero() ? void 0 : payment.fee,
|
|
488
|
+
status: paymentStatus(invoice.id, payment.status),
|
|
489
|
+
transactionId: payment.transactionId,
|
|
490
|
+
receivedAt: payment.receivedDate
|
|
491
|
+
};
|
|
492
|
+
payments.push(mapped);
|
|
493
|
+
if (mapped.status !== "invalid" && asset === settlementAsset) nativePaid = nativePaid.add(Decimal.parse(payment.value, "payment"));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
const metadata = {};
|
|
497
|
+
for (const [key, value] of Object.entries(invoice.metadata)) if (typeof value === "string" && !RESERVED_METADATA.has(key)) metadata[key] = value;
|
|
498
|
+
return {
|
|
499
|
+
gateway: BTCPAY_GATEWAY_ID,
|
|
500
|
+
id: invoice.id,
|
|
501
|
+
orderId: typeof invoice.metadata.orderId === "string" ? invoice.metadata.orderId : void 0,
|
|
502
|
+
status,
|
|
503
|
+
exception,
|
|
504
|
+
amount,
|
|
505
|
+
currency,
|
|
506
|
+
amountPaid: paidScaled.floorToScale(fiatDecimals),
|
|
507
|
+
settlementAsset: settlementAsset || void 0,
|
|
508
|
+
nativeAmountDue: primary?.amount,
|
|
509
|
+
nativeAmountPaid: methods.length ? nativePaid.toString() : void 0,
|
|
510
|
+
rate: primary?.rate,
|
|
511
|
+
rateSource,
|
|
512
|
+
checkoutUrl: invoice.checkoutLink,
|
|
513
|
+
createdAt: invoice.createdTime,
|
|
514
|
+
expiresAt: invoice.expirationTime,
|
|
515
|
+
metadata,
|
|
516
|
+
payments,
|
|
517
|
+
raw: invoice.raw
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
/** `2500, 2` → `'25.00'`: fixed decimals, exact. */
|
|
521
|
+
function formatMinorUnits(amount, decimals) {
|
|
522
|
+
const text = String(amount).padStart(decimals + 1, "0");
|
|
523
|
+
return decimals === 0 ? text : `${text.slice(0, -decimals)}.${text.slice(-decimals)}`;
|
|
524
|
+
}
|
|
525
|
+
function isOwned(invoice) {
|
|
526
|
+
const marker = invoice.metadata[OWNER_KEY];
|
|
527
|
+
return typeof marker === "string" && marker.startsWith("crypto-checkout:");
|
|
528
|
+
}
|
|
529
|
+
function ownedBy(invoice, orderId) {
|
|
530
|
+
return invoice.metadata.orderId === orderId && isOwned(invoice);
|
|
531
|
+
}
|
|
532
|
+
function paymentStatus(invoiceId, status) {
|
|
533
|
+
const mapped = PAYMENT_STATUS.get(status);
|
|
534
|
+
if (!mapped) throw new PaymentProviderError(`BTCPay invoice ${invoiceId} has a payment with unknown status '${status}'.`);
|
|
535
|
+
return mapped;
|
|
536
|
+
}
|
|
537
|
+
function assetOfMethodId(paymentMethodId) {
|
|
538
|
+
return (paymentMethodId.split("-")[0] ?? "").toUpperCase();
|
|
539
|
+
}
|
|
540
|
+
function assetOf(currency, paymentMethodId) {
|
|
541
|
+
return (currency ?? assetOfMethodId(paymentMethodId)).toUpperCase();
|
|
542
|
+
}
|
|
543
|
+
function railOf(paymentMethodId) {
|
|
544
|
+
const id = paymentMethodId.toUpperCase();
|
|
545
|
+
if (id === "BTC" || id.endsWith("-CHAIN")) return "onchain";
|
|
546
|
+
if (id.endsWith("-LN") || id.endsWith("-LNURL")) return "lightning";
|
|
547
|
+
return paymentMethodId.toLowerCase();
|
|
548
|
+
}
|
|
549
|
+
function decimalToMinor(value, decimals, label) {
|
|
550
|
+
const parsed = Decimal.parse(value, label);
|
|
551
|
+
const minor = parsed.floorToScale(decimals);
|
|
552
|
+
if (!parsed.equalsScaled(minor, decimals)) throw new PaymentProviderError(`BTCPay ${label} amount ${value} has more precision than its currency.`);
|
|
553
|
+
return minor;
|
|
554
|
+
}
|
|
555
|
+
/** Exact non-negative decimal arithmetic on BigInt (value = units / 10^scale). */
|
|
556
|
+
var Decimal = class Decimal {
|
|
557
|
+
units;
|
|
558
|
+
scale;
|
|
559
|
+
constructor(units, scale) {
|
|
560
|
+
this.units = units;
|
|
561
|
+
this.scale = scale;
|
|
562
|
+
}
|
|
563
|
+
static zero() {
|
|
564
|
+
return new Decimal(0n, 0);
|
|
565
|
+
}
|
|
566
|
+
static parse(value, label) {
|
|
567
|
+
const match = /^(\d+)(?:\.(\d+))?$/.exec(value?.trim?.() ?? "");
|
|
568
|
+
if (!match) throw new PaymentProviderError(`BTCPay ${label} '${String(value)}' is not a non-negative decimal.`);
|
|
569
|
+
const fraction = match[2] ?? "";
|
|
570
|
+
return new Decimal(BigInt(`${match[1]}${fraction}`), fraction.length);
|
|
571
|
+
}
|
|
572
|
+
isZero() {
|
|
573
|
+
return this.units === 0n;
|
|
574
|
+
}
|
|
575
|
+
add(other) {
|
|
576
|
+
const scale = Math.max(this.scale, other.scale);
|
|
577
|
+
return new Decimal(this.units * 10n ** BigInt(scale - this.scale) + other.units * 10n ** BigInt(scale - other.scale), scale);
|
|
578
|
+
}
|
|
579
|
+
mul(other) {
|
|
580
|
+
return new Decimal(this.units * other.units, this.scale + other.scale);
|
|
581
|
+
}
|
|
582
|
+
/** Integer count of 10^-decimals units, rounded down. */
|
|
583
|
+
floorToScale(decimals) {
|
|
584
|
+
const shifted = decimals >= this.scale ? this.units * 10n ** BigInt(decimals - this.scale) : this.units / 10n ** BigInt(this.scale - decimals);
|
|
585
|
+
if (shifted > BigInt(Number.MAX_SAFE_INTEGER)) throw new PaymentProviderError("BTCPay amount exceeds the safe integer range.");
|
|
586
|
+
return Number(shifted);
|
|
587
|
+
}
|
|
588
|
+
equalsScaled(minor, decimals) {
|
|
589
|
+
const scale = Math.max(this.scale, decimals);
|
|
590
|
+
return this.units * 10n ** BigInt(scale - this.scale) === BigInt(minor) * 10n ** BigInt(scale - decimals);
|
|
591
|
+
}
|
|
592
|
+
toString() {
|
|
593
|
+
if (this.scale === 0) return this.units.toString();
|
|
594
|
+
const text = this.units.toString().padStart(this.scale + 1, "0");
|
|
595
|
+
return `${text.slice(0, -this.scale)}.${text.slice(-this.scale)}`;
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
function readHeader(headers, name) {
|
|
599
|
+
if (typeof headers?.get === "function") return headers.get(name) ?? void 0;
|
|
600
|
+
for (const [key, value] of Object.entries(headers ?? {})) if (key.toLowerCase() === name) return value;
|
|
601
|
+
}
|
|
602
|
+
function requireText(value, label) {
|
|
603
|
+
if (typeof value !== "string" || !value.trim()) throw new PaymentConfigurationError(`Checkout ${label} is required.`);
|
|
604
|
+
return value.trim();
|
|
605
|
+
}
|
|
606
|
+
function positiveInteger(value, label) {
|
|
607
|
+
if (!Number.isSafeInteger(value) || value <= 0) throw new PaymentConfigurationError(`BTCPay gateway ${label} must be a positive integer.`);
|
|
608
|
+
return value;
|
|
609
|
+
}
|
|
610
|
+
//#endregion
|
|
611
|
+
export { BTCPAY_GATEWAY_ID, BtcpayApiError, BtcpayClient, createBtcpayCheckoutGateway, isValidBtcpayWebhookSignature, parseBtcpayWebhook };
|
|
307
612
|
|
|
308
613
|
//# sourceMappingURL=btcpay.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"btcpay.js","names":[],"sources":["../../src/adapters/btcpay.ts"],"sourcesContent":["/**\n * A small, stateless client for the BTCPay Server Greenfield API (2.x).\n *\n * It covers what billing integrations need: store invoices (create, read,\n * list by `orderId`), an invoice's payment methods, and webhook signature\n * verification and parsing. It keeps no state between calls — idempotency and\n * webhook de-duplication belong to the caller's durable storage.\n *\n * Decimal amounts stay strings end to end; callers convert them to their own\n * minor units.\n */\nimport { createHmac, timingSafeEqual } from 'node:crypto';\nimport { PaymentConfigurationError, PaymentProviderError } from '../errors.js';\nimport { type FetchLike, getFetch } from '../shared.js';\n\n/** Invoice status as reported by Greenfield. */\nexport type BtcpayInvoiceStatus =\n | 'New'\n | 'Processing'\n | 'Expired'\n | 'Invalid'\n | 'Settled';\n\n/** Greenfield's `additionalStatus` qualifier on an invoice. */\nexport type BtcpayInvoiceAdditionalStatus =\n | 'None'\n | 'PaidLate'\n | 'PaidPartial'\n | 'Marked'\n | 'Invalid'\n | 'PaidOver';\n\n/**\n * Confirmations before BTCPay settles an on-chain payment: `HighSpeed` 0\n * (1 for RBF-signalling transactions), `MediumSpeed` 1, `LowMediumSpeed` 2,\n * `LowSpeed` 6. These are the only values BTCPay supports.\n */\nexport type BtcpaySpeedPolicy =\n | 'HighSpeed'\n | 'MediumSpeed'\n | 'LowMediumSpeed'\n | 'LowSpeed';\n\nexport interface BtcpayInvoiceCheckoutOptions {\n speedPolicy?: BtcpaySpeedPolicy;\n /** Payment method ids, for example `['BTC-CHAIN']` or `['BTC-CHAIN', 'BTC-LN']`. */\n paymentMethods?: string[];\n defaultPaymentMethod?: string;\n /** Minutes the rate is locked and the invoice accepts new payments. */\n expirationMinutes?: number;\n /** Minutes after expiry during which late payments are still tracked. */\n monitoringMinutes?: number;\n /** Percentage underpayment BTCPay treats as paid (0–100). */\n paymentTolerance?: number;\n redirectURL?: string;\n redirectAutomatically?: boolean;\n defaultLanguage?: string;\n}\n\nexport interface BtcpayCreateInvoiceInput {\n /** Decimal amount in `currency`, for example `'25.00'`. */\n amount: string;\n currency: string;\n /** Merchant order id; stored as `metadata.orderId` and filterable. */\n orderId?: string;\n metadata?: Record<string, unknown>;\n checkout?: BtcpayInvoiceCheckoutOptions;\n additionalSearchTerms?: string[];\n}\n\nexport interface BtcpayInvoice {\n id: string;\n storeId?: string;\n status: BtcpayInvoiceStatus | string;\n additionalStatus: BtcpayInvoiceAdditionalStatus | string;\n /** Decimal amount in `currency`. */\n amount: string;\n currency: string;\n checkoutLink?: string;\n createdTime?: Date;\n expirationTime?: Date;\n monitoringExpiration?: Date;\n archived: boolean;\n metadata: Record<string, unknown>;\n checkout: Record<string, unknown>;\n raw: Record<string, unknown>;\n}\n\nexport interface BtcpayListInvoicesInput {\n orderId?: string | string[];\n status?: BtcpayInvoiceStatus | BtcpayInvoiceStatus[];\n textSearch?: string;\n skip?: number;\n take?: number;\n}\n\nexport interface BtcpayInvoicePayment {\n /** On-chain payments use `<txid>-<vout>`. */\n id: string;\n /** The transaction id of an on-chain payment (parsed from `id`). */\n transactionId?: string;\n receivedDate?: Date;\n /** Decimal amount in the payment method's currency (BTC). */\n value: string;\n /** Decimal network/payment-method fee. */\n fee: string;\n status: 'Invalid' | 'Processing' | 'Settled' | string;\n destination?: string;\n}\n\nexport interface BtcpayInvoicePaymentMethod {\n /** `BTC-CHAIN`, `BTC-LN`, … (Greenfield 2.x `paymentMethodId`). */\n paymentMethodId: string;\n /** The payment method's currency, for example `BTC`. */\n currency?: string;\n destination?: string;\n paymentLink?: string;\n /** Invoice-currency price of one unit of the payment currency, if reported. */\n rate?: string;\n /** Decimal amount due in the payment currency when created. */\n amount: string;\n /** Decimal amount paid through this method. */\n paymentMethodPaid: string;\n /** Decimal amount paid through all methods, in this method's currency. */\n totalPaid: string;\n /**\n * Decimal amount still due, in this method's currency. Undefined when\n * BTCPay did not report it — never read a missing value as \"nothing due\".\n */\n due?: string;\n payments: BtcpayInvoicePayment[];\n raw: Record<string, unknown>;\n}\n\nexport interface BtcpayClientOptions {\n /** Server origin, for example `https://btcpay.example.com` (no path). */\n baseUrl: string;\n /** Greenfield API key (`Authorization: token …`). */\n apiKey: string;\n storeId: string;\n fetch?: FetchLike;\n /** Per-request timeout (default 30 seconds). */\n timeoutMs?: number;\n}\n\n/**\n * A Greenfield request failed. Never carries the API key.\n *\n * `retryable` is true only when repeating the same call cannot duplicate a\n * side effect: a read (`GET`) that failed on the network, with 429, or with\n * 5xx, or any request refused with 429. A `createInvoice` that failed on the\n * network or with 5xx may still have created the invoice, so it is not\n * retryable: resolve it with `listInvoices({ orderId })` instead.\n */\nexport class BtcpayApiError extends PaymentProviderError {\n readonly status: number;\n readonly apiCode?: string;\n\n constructor(\n message: string,\n status: number,\n apiCode?: string,\n options: { cause?: unknown; method?: 'GET' | 'POST' } = {},\n ) {\n const { method = 'GET', ...rest } = options;\n super(message, {\n ...rest,\n retryable:\n status === 429 || (method === 'GET' && (status === 0 || status >= 500)),\n });\n this.name = 'BtcpayApiError';\n this.status = status;\n this.apiCode = apiCode;\n }\n}\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\nexport class BtcpayClient {\n readonly baseUrl: string;\n readonly storeId: string;\n private readonly apiKey: string;\n private readonly fetch: FetchLike;\n private readonly timeoutMs: number;\n\n constructor(options: BtcpayClientOptions) {\n this.baseUrl = normalizeBaseUrl(options.baseUrl);\n this.apiKey = requireString(options.apiKey, 'BtcpayClient apiKey');\n this.storeId = requireString(options.storeId, 'BtcpayClient storeId');\n this.fetch = getFetch(options.fetch);\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {\n throw new PaymentConfigurationError(\n 'BtcpayClient timeoutMs must be a positive number.',\n );\n }\n this.timeoutMs = timeoutMs;\n }\n\n /**\n * `POST /api/v1/stores/{storeId}/invoices`. Not idempotent at BTCPay: after\n * a network or 5xx failure the invoice may exist, so the error is not\n * `retryable` — look it up with `listInvoices({ orderId })` before creating\n * another.\n */\n async createInvoice(input: BtcpayCreateInvoiceInput): Promise<BtcpayInvoice> {\n const amount = requirePositiveDecimal(\n input.amount,\n 'BTCPay invoice amount',\n );\n const currency = requireString(input.currency, 'BTCPay invoice currency');\n const metadata: Record<string, unknown> = { ...(input.metadata ?? {}) };\n if (input.orderId !== undefined) {\n metadata.orderId = requireString(input.orderId, 'BTCPay orderId');\n }\n const body: Record<string, unknown> = { amount, currency, metadata };\n if (input.checkout) body.checkout = compact({ ...input.checkout });\n if (input.additionalSearchTerms?.length) {\n body.additionalSearchTerms = input.additionalSearchTerms;\n }\n const raw = await this.request('POST', this.storePath('/invoices'), body);\n return toInvoice(raw);\n }\n\n /** `GET /api/v1/stores/{storeId}/invoices/{invoiceId}`. */\n async getInvoice(invoiceId: string): Promise<BtcpayInvoice> {\n const id = requireString(invoiceId, 'BTCPay invoiceId');\n const raw = await this.request(\n 'GET',\n this.storePath(`/invoices/${encodeURIComponent(id)}`),\n );\n return toInvoice(raw);\n }\n\n /** `GET /api/v1/stores/{storeId}/invoices` with Greenfield filters. */\n async listInvoices(\n input: BtcpayListInvoicesInput = {},\n ): Promise<BtcpayInvoice[]> {\n const query = new URLSearchParams();\n for (const orderId of toList(input.orderId)) {\n query.append('orderId', requireString(orderId, 'BTCPay orderId'));\n }\n for (const status of toList(input.status)) query.append('status', status);\n if (input.textSearch) query.set('textSearch', input.textSearch);\n if (input.skip !== undefined) {\n query.set('skip', String(requireCount(input.skip, 'skip')));\n }\n if (input.take !== undefined) {\n query.set('take', String(requireCount(input.take, 'take')));\n }\n const search = query.toString();\n const suffix = search ? `?${search}` : '';\n const raw = await this.request('GET', this.storePath(`/invoices${suffix}`));\n const items = Array.isArray(raw)\n ? raw\n : Array.isArray((raw as { items?: unknown })?.items)\n ? (raw as { items: unknown[] }).items\n : null;\n if (!items) {\n throw new BtcpayApiError(\n 'BTCPay invoice list response was not an array.',\n 200,\n );\n }\n return items.map(toInvoice);\n }\n\n /** `GET /api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods`. */\n async getInvoicePaymentMethods(\n invoiceId: string,\n ): Promise<BtcpayInvoicePaymentMethod[]> {\n const id = requireString(invoiceId, 'BTCPay invoiceId');\n const raw = await this.request(\n 'GET',\n this.storePath(`/invoices/${encodeURIComponent(id)}/payment-methods`),\n );\n if (!Array.isArray(raw)) {\n throw new BtcpayApiError(\n 'BTCPay payment-methods response was not an array.',\n 200,\n );\n }\n return raw.filter(isRecord).map(toPaymentMethod);\n }\n\n private storePath(suffix: string): string {\n return `/api/v1/stores/${encodeURIComponent(this.storeId)}${suffix}`;\n }\n\n private async request(\n method: 'GET' | 'POST',\n path: string,\n body?: unknown,\n ): Promise<unknown> {\n const headers = new Headers();\n headers.set('Authorization', `token ${this.apiKey}`);\n headers.set('Accept', 'application/json');\n if (body !== undefined) headers.set('Content-Type', 'application/json');\n let response: Response;\n try {\n response = await this.fetch(`${this.baseUrl}${path}`, {\n method,\n headers,\n body: body === undefined ? undefined : JSON.stringify(body),\n signal: AbortSignal.timeout(this.timeoutMs),\n });\n } catch (error) {\n throw new BtcpayApiError(\n `BTCPay ${method} ${redactPath(path)} failed: ${errorName(error)}.`,\n 0,\n undefined,\n { cause: error, method },\n );\n }\n let text: string;\n try {\n text = await response.text();\n } catch (error) {\n // The server answered, so a POST may already have taken effect.\n throw new BtcpayApiError(\n `BTCPay ${method} ${redactPath(path)} failed reading the response: ${errorName(error)}.`,\n 0,\n undefined,\n { cause: error, method },\n );\n }\n let parsed: unknown;\n if (text) {\n try {\n parsed = JSON.parse(text);\n } catch (error) {\n if (response.ok) {\n throw new BtcpayApiError(\n `BTCPay ${method} ${redactPath(path)} returned invalid JSON.`,\n response.status,\n undefined,\n { cause: error },\n );\n }\n }\n }\n if (!response.ok) {\n const { code, message } = readGreenfieldError(parsed);\n throw new BtcpayApiError(\n `BTCPay ${method} ${redactPath(path)} failed with HTTP ${response.status}` +\n (message ? `: ${message}` : '.'),\n response.status,\n code,\n { method },\n );\n }\n return parsed;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Webhooks\n// ---------------------------------------------------------------------------\n\n/** A parsed BTCPay webhook delivery (signature must be verified first). */\nexport interface BtcpayWebhookDelivery {\n deliveryId: string;\n webhookId?: string;\n /** Set on a redelivery: the id of the delivery it repeats. */\n originalDeliveryId?: string;\n isRedelivery: boolean;\n /** `InvoiceSettled`, `InvoiceExpired`, … */\n type: string;\n timestamp?: Date;\n storeId?: string;\n invoiceId?: string;\n metadata?: Record<string, unknown>;\n raw: Record<string, unknown>;\n}\n\n/**\n * Check a `BTCPay-Sig` header (`sha256=<hex>`) against the raw request body\n * in constant time. Returns false for any missing or malformed input.\n */\nexport function isValidBtcpayWebhookSignature(\n rawBody: string,\n header: string | null | undefined,\n secret: string,\n): boolean {\n if (typeof rawBody !== 'string' || typeof header !== 'string') return false;\n if (typeof secret !== 'string' || !secret) return false;\n const match = /^sha256=([a-f0-9]{64})$/i.exec(header.trim());\n if (!match) return false;\n const expected = createHmac('sha256', secret).update(rawBody).digest();\n const actual = Buffer.from(match[1], 'hex');\n return actual.length === expected.length && timingSafeEqual(actual, expected);\n}\n\n/** Parse a webhook body. Throws `PaymentProviderError` when it is malformed. */\nexport function parseBtcpayWebhook(rawBody: string): BtcpayWebhookDelivery {\n let raw: unknown;\n try {\n raw = JSON.parse(rawBody);\n } catch (error) {\n throw new PaymentProviderError('BTCPay webhook body is not JSON.', {\n cause: error,\n });\n }\n if (!isRecord(raw)) {\n throw new PaymentProviderError('BTCPay webhook body is not an object.');\n }\n const deliveryId = readString(raw, 'deliveryId');\n const type = readString(raw, 'type');\n if (!deliveryId || !type) {\n throw new PaymentProviderError(\n 'BTCPay webhook body needs deliveryId and type.',\n );\n }\n return {\n deliveryId,\n webhookId: readString(raw, 'webhookId'),\n originalDeliveryId: readString(raw, 'originalDeliveryId'),\n isRedelivery: raw.isRedelivery === true,\n type,\n timestamp: readDate(raw.timestamp),\n storeId: readString(raw, 'storeId'),\n invoiceId: readString(raw, 'invoiceId'),\n metadata: isRecord(raw.metadata) ? raw.metadata : undefined,\n raw,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction toInvoice(value: unknown): BtcpayInvoice {\n if (!isRecord(value)) {\n throw new BtcpayApiError('BTCPay invoice response was not an object.', 200);\n }\n const id = readString(value, 'id');\n if (!id) {\n throw new BtcpayApiError('BTCPay invoice response has no id.', 200);\n }\n return {\n id,\n storeId: readString(value, 'storeId'),\n status: readString(value, 'status') ?? 'New',\n additionalStatus: readString(value, 'additionalStatus') ?? 'None',\n amount: readDecimal(value.amount) ?? '0',\n currency: readString(value, 'currency') ?? '',\n checkoutLink: readString(value, 'checkoutLink'),\n createdTime: readDate(value.createdTime),\n expirationTime: readDate(value.expirationTime),\n monitoringExpiration: readDate(value.monitoringExpiration),\n archived: value.archived === true,\n metadata: isRecord(value.metadata) ? value.metadata : {},\n checkout: isRecord(value.checkout) ? value.checkout : {},\n raw: value,\n };\n}\n\nfunction toPaymentMethod(\n value: Record<string, unknown>,\n): BtcpayInvoicePaymentMethod {\n // Greenfield 1.x used `paymentMethod` / `cryptoCode`; 2.x uses\n // `paymentMethodId` / `currency`.\n const paymentMethodId =\n readString(value, 'paymentMethodId') ??\n readString(value, 'paymentMethod') ??\n '';\n return {\n paymentMethodId,\n currency: readString(value, 'currency') ?? readString(value, 'cryptoCode'),\n destination: readString(value, 'destination'),\n paymentLink: readString(value, 'paymentLink'),\n rate: readDecimal(value.rate),\n amount: readDecimal(value.amount) ?? '0',\n paymentMethodPaid: readDecimal(value.paymentMethodPaid) ?? '0',\n totalPaid: readDecimal(value.totalPaid) ?? '0',\n due: readDecimal(value.due),\n payments: Array.isArray(value.payments)\n ? value.payments.filter(isRecord).map((payment) => ({\n id: readString(payment, 'id') ?? '',\n transactionId: onChainTransactionId(readString(payment, 'id')),\n receivedDate: readDate(payment.receivedDate),\n value: readDecimal(payment.value) ?? '0',\n fee: readDecimal(payment.fee) ?? '0',\n status: readString(payment, 'status') ?? '',\n destination: readString(payment, 'destination'),\n }))\n : [],\n raw: value,\n };\n}\n\nfunction readGreenfieldError(body: unknown): {\n code?: string;\n message?: string;\n} {\n if (Array.isArray(body)) {\n // Validation errors: [{ path, message }]\n const messages = body\n .filter(isRecord)\n .map((item) =>\n [readString(item, 'path'), readString(item, 'message')]\n .filter(Boolean)\n .join(': '),\n )\n .filter(Boolean);\n return { code: 'validation-error', message: messages.join('; ') };\n }\n if (isRecord(body)) {\n return {\n code: readString(body, 'code'),\n message: readString(body, 'message'),\n };\n }\n return {};\n}\n\n/** Greenfield paths carry only store/invoice ids; strip the query string. */\nfunction redactPath(path: string): string {\n return path.split('?')[0];\n}\n\nfunction errorName(error: unknown): string {\n if (error instanceof Error) {\n return error.name === 'TimeoutError' ? 'timed out' : error.name;\n }\n return 'network error';\n}\n\nfunction normalizeBaseUrl(value: string): string {\n const raw = requireString(value, 'BtcpayClient baseUrl');\n let parsed: URL;\n try {\n parsed = new URL(raw);\n } catch (error) {\n throw new PaymentConfigurationError(\n 'BtcpayClient baseUrl must be an absolute URL.',\n { cause: error },\n );\n }\n if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {\n throw new PaymentConfigurationError(\n 'BtcpayClient baseUrl must use http or https.',\n );\n }\n if (\n parsed.search ||\n parsed.hash ||\n parsed.username ||\n parsed.password ||\n !['', '/'].includes(parsed.pathname)\n ) {\n throw new PaymentConfigurationError(\n 'BtcpayClient baseUrl must be an origin without path, query, fragment, or credentials.',\n );\n }\n return parsed.origin;\n}\n\nfunction requireString(value: unknown, label: string): string {\n if (typeof value !== 'string' || !value.trim()) {\n throw new PaymentConfigurationError(`${label} is required.`);\n }\n return value.trim();\n}\n\nfunction requirePositiveDecimal(value: unknown, label: string): string {\n if (\n typeof value !== 'string' ||\n !/^\\d+(\\.\\d+)?$/.test(value.trim()) ||\n !/[1-9]/.test(value)\n ) {\n throw new PaymentConfigurationError(\n `${label} must be a positive decimal string.`,\n );\n }\n return value.trim();\n}\n\nfunction requireCount(value: number, label: string): number {\n if (!Number.isSafeInteger(value) || value < 0) {\n throw new PaymentConfigurationError(\n `BTCPay ${label} must be a non-negative integer.`,\n );\n }\n return value;\n}\n\nfunction toList<T>(value: T | T[] | undefined): T[] {\n if (value === undefined) return [];\n return Array.isArray(value) ? value : [value];\n}\n\nfunction compact<T extends Record<string, unknown>>(value: T): Partial<T> {\n return Object.fromEntries(\n Object.entries(value).filter(([, item]) => item !== undefined),\n ) as Partial<T>;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === 'object' && !Array.isArray(value);\n}\n\nfunction readString(\n record: Record<string, unknown>,\n key: string,\n): string | undefined {\n const value = record[key];\n if (typeof value === 'string' && value.trim()) return value.trim();\n return undefined;\n}\n\n/** Keep decimals as strings; accept numbers Greenfield may emit. */\nfunction readDecimal(value: unknown): string | undefined {\n if (typeof value === 'string' && value.trim()) return value.trim();\n if (typeof value === 'number' && Number.isFinite(value)) {\n const text = String(value);\n return /e/i.test(text)\n ? value.toFixed(20).replace(/0+$/, '').replace(/\\.$/, '')\n : text;\n }\n return undefined;\n}\n\nfunction onChainTransactionId(id: string | undefined): string | undefined {\n const match = id ? /^([0-9a-f]{64})-\\d+$/i.exec(id) : null;\n return match ? match[1].toLowerCase() : undefined;\n}\n\n/** Greenfield timestamps are unix seconds; ISO strings are accepted too. */\nfunction readDate(value: unknown): Date | undefined {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return new Date(value * 1000);\n }\n if (typeof value === 'string' && value.trim()) {\n const date = new Date(value);\n return Number.isNaN(date.getTime()) ? undefined : date;\n }\n return undefined;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"btcpay.js","names":[],"sources":["../../src/adapters/btcpay-client.ts","../../src/adapters/btcpay-gateway.ts"],"sourcesContent":["/**\n * A small, stateless client for the BTCPay Server Greenfield API (2.x).\n *\n * It covers what billing integrations need: store invoices (create, read,\n * list by `orderId`), an invoice's payment methods, and webhook signature\n * verification and parsing. It keeps no state between calls — idempotency and\n * webhook de-duplication belong to the caller's durable storage.\n *\n * Decimal amounts stay strings end to end; callers convert them to their own\n * minor units.\n */\nimport { createHmac, timingSafeEqual } from 'node:crypto';\nimport { PaymentConfigurationError, PaymentProviderError } from '../errors.js';\nimport { type FetchLike, getFetch } from '../shared.js';\n\n/** Invoice status as reported by Greenfield. */\nexport type BtcpayInvoiceStatus =\n | 'New'\n | 'Processing'\n | 'Expired'\n | 'Invalid'\n | 'Settled';\n\n/** Greenfield's `additionalStatus` qualifier on an invoice. */\nexport type BtcpayInvoiceAdditionalStatus =\n | 'None'\n | 'PaidLate'\n | 'PaidPartial'\n | 'Marked'\n | 'Invalid'\n | 'PaidOver';\n\n/**\n * Confirmations before BTCPay settles an on-chain payment: `HighSpeed` 0\n * (1 for RBF-signalling transactions), `MediumSpeed` 1, `LowMediumSpeed` 2,\n * `LowSpeed` 6. These are the only values BTCPay supports.\n */\nexport type BtcpaySpeedPolicy =\n | 'HighSpeed'\n | 'MediumSpeed'\n | 'LowMediumSpeed'\n | 'LowSpeed';\n\nexport interface BtcpayInvoiceCheckoutOptions {\n speedPolicy?: BtcpaySpeedPolicy;\n /** Payment method ids, for example `['BTC-CHAIN']` or `['BTC-CHAIN', 'BTC-LN']`. */\n paymentMethods?: string[];\n defaultPaymentMethod?: string;\n /** Minutes the rate is locked and the invoice accepts new payments. */\n expirationMinutes?: number;\n /** Minutes after expiry during which late payments are still tracked. */\n monitoringMinutes?: number;\n /** Percentage underpayment BTCPay treats as paid (0–100). */\n paymentTolerance?: number;\n redirectURL?: string;\n redirectAutomatically?: boolean;\n defaultLanguage?: string;\n}\n\nexport interface BtcpayCreateInvoiceInput {\n /** Decimal amount in `currency`, for example `'25.00'`. */\n amount: string;\n currency: string;\n /** Merchant order id; stored as `metadata.orderId` and filterable. */\n orderId?: string;\n metadata?: Record<string, unknown>;\n checkout?: BtcpayInvoiceCheckoutOptions;\n additionalSearchTerms?: string[];\n}\n\nexport interface BtcpayInvoice {\n id: string;\n storeId?: string;\n status: BtcpayInvoiceStatus | string;\n additionalStatus: BtcpayInvoiceAdditionalStatus | string;\n /** Decimal amount in `currency`. */\n amount: string;\n currency: string;\n checkoutLink?: string;\n createdTime?: Date;\n expirationTime?: Date;\n monitoringExpiration?: Date;\n archived: boolean;\n metadata: Record<string, unknown>;\n checkout: Record<string, unknown>;\n raw: Record<string, unknown>;\n}\n\nexport interface BtcpayListInvoicesInput {\n orderId?: string | string[];\n status?: BtcpayInvoiceStatus | BtcpayInvoiceStatus[];\n textSearch?: string;\n /** Include archived invoices (Greenfield omits them by default). */\n includeArchived?: boolean;\n skip?: number;\n take?: number;\n}\n\nexport interface BtcpayInvoicePayment {\n /** On-chain payments use `<txid>-<vout>`. */\n id: string;\n /** The transaction id of an on-chain payment (parsed from `id`). */\n transactionId?: string;\n receivedDate?: Date;\n /** Decimal amount in the payment method's currency (BTC). */\n value: string;\n /** Decimal network/payment-method fee. */\n fee: string;\n status: 'Invalid' | 'Processing' | 'Settled' | string;\n destination?: string;\n}\n\nexport interface BtcpayInvoicePaymentMethod {\n /** `BTC-CHAIN`, `BTC-LN`, … (Greenfield 2.x `paymentMethodId`). */\n paymentMethodId: string;\n /** The payment method's currency, for example `BTC`. */\n currency?: string;\n destination?: string;\n paymentLink?: string;\n /** Invoice-currency price of one unit of the payment currency, if reported. */\n rate?: string;\n /** Decimal amount due in the payment currency when created. */\n amount: string;\n /**\n * Decimal amount paid through this method (Greenfield 1.x: `paid`).\n * Undefined when BTCPay did not report it — never read absence as zero.\n */\n paymentMethodPaid?: string;\n /** Decimal amount paid through all methods, in this method's currency. */\n totalPaid: string;\n /**\n * Decimal amount still due, in this method's currency. Undefined when\n * BTCPay did not report it — never read a missing value as \"nothing due\".\n */\n due?: string;\n payments: BtcpayInvoicePayment[];\n raw: Record<string, unknown>;\n}\n\nexport interface BtcpayClientOptions {\n /** Server origin, for example `https://btcpay.example.com` (no path). */\n baseUrl: string;\n /** Greenfield API key (`Authorization: token …`). */\n apiKey: string;\n storeId: string;\n fetch?: FetchLike;\n /** Per-request timeout (default 30 seconds). */\n timeoutMs?: number;\n}\n\n/**\n * A Greenfield request failed. Never carries the API key.\n *\n * `retryable` is true only when repeating the same call cannot duplicate a\n * side effect: a read (`GET`) that failed on the network, with 429, or with\n * 5xx, or any request refused with 429. A `createInvoice` that failed on the\n * network or with 5xx may still have created the invoice, so it is not\n * retryable: resolve it with `listInvoices({ orderId })` instead.\n */\nexport class BtcpayApiError extends PaymentProviderError {\n readonly status: number;\n readonly apiCode?: string;\n\n constructor(\n message: string,\n status: number,\n apiCode?: string,\n options: { cause?: unknown; method?: 'GET' | 'POST' } = {},\n ) {\n const { method = 'GET', ...rest } = options;\n super(message, {\n ...rest,\n retryable:\n status === 429 || (method === 'GET' && (status === 0 || status >= 500)),\n });\n this.name = 'BtcpayApiError';\n this.status = status;\n this.apiCode = apiCode;\n }\n}\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\nexport class BtcpayClient {\n readonly baseUrl: string;\n readonly storeId: string;\n private readonly apiKey: string;\n private readonly fetch: FetchLike;\n private readonly timeoutMs: number;\n\n constructor(options: BtcpayClientOptions) {\n this.baseUrl = normalizeBaseUrl(options.baseUrl);\n this.apiKey = requireString(options.apiKey, 'BtcpayClient apiKey');\n this.storeId = requireString(options.storeId, 'BtcpayClient storeId');\n this.fetch = getFetch(options.fetch);\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {\n throw new PaymentConfigurationError(\n 'BtcpayClient timeoutMs must be a positive number.',\n );\n }\n this.timeoutMs = timeoutMs;\n }\n\n /**\n * `POST /api/v1/stores/{storeId}/invoices`. Not idempotent at BTCPay: after\n * a network or 5xx failure the invoice may exist, so the error is not\n * `retryable` — look it up with `listInvoices({ orderId })` before creating\n * another.\n */\n async createInvoice(input: BtcpayCreateInvoiceInput): Promise<BtcpayInvoice> {\n const amount = requirePositiveDecimal(\n input.amount,\n 'BTCPay invoice amount',\n );\n const currency = requireString(input.currency, 'BTCPay invoice currency');\n const metadata: Record<string, unknown> = { ...(input.metadata ?? {}) };\n if (input.orderId !== undefined) {\n metadata.orderId = requireString(input.orderId, 'BTCPay orderId');\n }\n const body: Record<string, unknown> = { amount, currency, metadata };\n if (input.checkout) body.checkout = compact({ ...input.checkout });\n if (input.additionalSearchTerms?.length) {\n body.additionalSearchTerms = input.additionalSearchTerms;\n }\n const raw = await this.request('POST', this.storePath('/invoices'), body);\n return toInvoice(raw);\n }\n\n /** `GET /api/v1/stores/{storeId}/invoices/{invoiceId}`. */\n async getInvoice(invoiceId: string): Promise<BtcpayInvoice> {\n const id = requireString(invoiceId, 'BTCPay invoiceId');\n const raw = await this.request(\n 'GET',\n this.storePath(`/invoices/${encodeURIComponent(id)}`),\n );\n return toInvoice(raw);\n }\n\n /** `GET /api/v1/stores/{storeId}/invoices` with Greenfield filters. */\n async listInvoices(\n input: BtcpayListInvoicesInput = {},\n ): Promise<BtcpayInvoice[]> {\n const query = new URLSearchParams();\n for (const orderId of toList(input.orderId)) {\n query.append('orderId', requireString(orderId, 'BTCPay orderId'));\n }\n for (const status of toList(input.status)) query.append('status', status);\n if (input.textSearch) query.set('textSearch', input.textSearch);\n if (input.includeArchived) query.set('includeArchived', 'true');\n if (input.skip !== undefined) {\n query.set('skip', String(requireCount(input.skip, 'skip')));\n }\n if (input.take !== undefined) {\n query.set('take', String(requireCount(input.take, 'take')));\n }\n const search = query.toString();\n const suffix = search ? `?${search}` : '';\n const raw = await this.request('GET', this.storePath(`/invoices${suffix}`));\n const items = Array.isArray(raw)\n ? raw\n : Array.isArray((raw as { items?: unknown })?.items)\n ? (raw as { items: unknown[] }).items\n : null;\n if (!items) {\n throw new BtcpayApiError(\n 'BTCPay invoice list response was not an array.',\n 200,\n );\n }\n return items.map(toInvoice);\n }\n\n /** `GET /api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods`. */\n async getInvoicePaymentMethods(\n invoiceId: string,\n ): Promise<BtcpayInvoicePaymentMethod[]> {\n const id = requireString(invoiceId, 'BTCPay invoiceId');\n const raw = await this.request(\n 'GET',\n this.storePath(`/invoices/${encodeURIComponent(id)}/payment-methods`),\n );\n if (!Array.isArray(raw)) {\n throw new BtcpayApiError(\n 'BTCPay payment-methods response was not an array.',\n 200,\n );\n }\n return raw.filter(isRecord).map(toPaymentMethod);\n }\n\n private storePath(suffix: string): string {\n return `/api/v1/stores/${encodeURIComponent(this.storeId)}${suffix}`;\n }\n\n private async request(\n method: 'GET' | 'POST',\n path: string,\n body?: unknown,\n ): Promise<unknown> {\n const headers = new Headers();\n headers.set('Authorization', `token ${this.apiKey}`);\n headers.set('Accept', 'application/json');\n if (body !== undefined) headers.set('Content-Type', 'application/json');\n let response: Response;\n try {\n response = await this.fetch(`${this.baseUrl}${path}`, {\n method,\n headers,\n body: body === undefined ? undefined : JSON.stringify(body),\n signal: AbortSignal.timeout(this.timeoutMs),\n });\n } catch (error) {\n throw new BtcpayApiError(\n `BTCPay ${method} ${redactPath(path)} failed: ${errorName(error)}.`,\n 0,\n undefined,\n { cause: error, method },\n );\n }\n let text: string;\n try {\n text = await response.text();\n } catch (error) {\n // The server answered, so a POST may already have taken effect.\n throw new BtcpayApiError(\n `BTCPay ${method} ${redactPath(path)} failed reading the response: ${errorName(error)}.`,\n 0,\n undefined,\n { cause: error, method },\n );\n }\n let parsed: unknown;\n if (text) {\n try {\n parsed = JSON.parse(text);\n } catch (error) {\n if (response.ok) {\n throw new BtcpayApiError(\n `BTCPay ${method} ${redactPath(path)} returned invalid JSON.`,\n response.status,\n undefined,\n { cause: error },\n );\n }\n }\n }\n if (!response.ok) {\n const { code, message } = readGreenfieldError(parsed);\n throw new BtcpayApiError(\n `BTCPay ${method} ${redactPath(path)} failed with HTTP ${response.status}` +\n (message ? `: ${message}` : '.'),\n response.status,\n code,\n { method },\n );\n }\n return parsed;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Webhooks\n// ---------------------------------------------------------------------------\n\n/** A parsed BTCPay webhook delivery (signature must be verified first). */\nexport interface BtcpayWebhookDelivery {\n deliveryId: string;\n webhookId?: string;\n /** Set on a redelivery: the id of the delivery it repeats. */\n originalDeliveryId?: string;\n isRedelivery: boolean;\n /** `InvoiceSettled`, `InvoiceExpired`, … */\n type: string;\n timestamp?: Date;\n storeId?: string;\n invoiceId?: string;\n metadata?: Record<string, unknown>;\n raw: Record<string, unknown>;\n}\n\n/**\n * Check a `BTCPay-Sig` header (`sha256=<hex>`) against the raw request body\n * in constant time. Returns false for any missing or malformed input.\n */\nexport function isValidBtcpayWebhookSignature(\n rawBody: string,\n header: string | null | undefined,\n secret: string,\n): boolean {\n if (typeof rawBody !== 'string' || typeof header !== 'string') return false;\n if (typeof secret !== 'string' || !secret) return false;\n const match = /^sha256=([a-f0-9]{64})$/i.exec(header.trim());\n if (!match) return false;\n const expected = createHmac('sha256', secret).update(rawBody).digest();\n const actual = Buffer.from(match[1], 'hex');\n return actual.length === expected.length && timingSafeEqual(actual, expected);\n}\n\n/** Parse a webhook body. Throws `PaymentProviderError` when it is malformed. */\nexport function parseBtcpayWebhook(rawBody: string): BtcpayWebhookDelivery {\n let raw: unknown;\n try {\n raw = JSON.parse(rawBody);\n } catch (error) {\n throw new PaymentProviderError('BTCPay webhook body is not JSON.', {\n cause: error,\n });\n }\n if (!isRecord(raw)) {\n throw new PaymentProviderError('BTCPay webhook body is not an object.');\n }\n const deliveryId = readString(raw, 'deliveryId');\n const type = readString(raw, 'type');\n if (!deliveryId || !type) {\n throw new PaymentProviderError(\n 'BTCPay webhook body needs deliveryId and type.',\n );\n }\n return {\n deliveryId,\n webhookId: readString(raw, 'webhookId'),\n originalDeliveryId: readString(raw, 'originalDeliveryId'),\n isRedelivery: raw.isRedelivery === true,\n type,\n timestamp: readDate(raw.timestamp),\n storeId: readString(raw, 'storeId'),\n invoiceId: readString(raw, 'invoiceId'),\n metadata: isRecord(raw.metadata) ? raw.metadata : undefined,\n raw,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction toInvoice(value: unknown): BtcpayInvoice {\n if (!isRecord(value)) {\n throw new BtcpayApiError('BTCPay invoice response was not an object.', 200);\n }\n const id = readString(value, 'id');\n if (!id) {\n throw new BtcpayApiError('BTCPay invoice response has no id.', 200);\n }\n return {\n id,\n storeId: readString(value, 'storeId'),\n status: readString(value, 'status') ?? 'New',\n additionalStatus: readString(value, 'additionalStatus') ?? 'None',\n amount: readDecimal(value.amount) ?? '0',\n currency: readString(value, 'currency') ?? '',\n checkoutLink: readString(value, 'checkoutLink'),\n createdTime: readDate(value.createdTime),\n expirationTime: readDate(value.expirationTime),\n monitoringExpiration: readDate(value.monitoringExpiration),\n archived: value.archived === true,\n metadata: isRecord(value.metadata) ? value.metadata : {},\n checkout: isRecord(value.checkout) ? value.checkout : {},\n raw: value,\n };\n}\n\nfunction toPaymentMethod(\n value: Record<string, unknown>,\n): BtcpayInvoicePaymentMethod {\n // Greenfield 1.x used `paymentMethod` / `cryptoCode`; 2.x uses\n // `paymentMethodId` / `currency`.\n const paymentMethodId =\n readString(value, 'paymentMethodId') ??\n readString(value, 'paymentMethod') ??\n '';\n return {\n paymentMethodId,\n currency: readString(value, 'currency') ?? readString(value, 'cryptoCode'),\n destination: readString(value, 'destination'),\n paymentLink: readString(value, 'paymentLink'),\n rate: readDecimal(value.rate),\n amount: readDecimal(value.amount) ?? '0',\n paymentMethodPaid:\n readDecimal(value.paymentMethodPaid) ?? readDecimal(value.paid),\n totalPaid: readDecimal(value.totalPaid) ?? '0',\n due: readDecimal(value.due),\n payments: Array.isArray(value.payments)\n ? value.payments.filter(isRecord).map((payment) => ({\n id: readString(payment, 'id') ?? '',\n transactionId: onChainTransactionId(readString(payment, 'id')),\n receivedDate: readDate(payment.receivedDate),\n value: readDecimal(payment.value) ?? '0',\n fee: readDecimal(payment.fee) ?? '0',\n status: readString(payment, 'status') ?? '',\n destination: readString(payment, 'destination'),\n }))\n : [],\n raw: value,\n };\n}\n\nfunction readGreenfieldError(body: unknown): {\n code?: string;\n message?: string;\n} {\n if (Array.isArray(body)) {\n // Validation errors: [{ path, message }]\n const messages = body\n .filter(isRecord)\n .map((item) =>\n [readString(item, 'path'), readString(item, 'message')]\n .filter(Boolean)\n .join(': '),\n )\n .filter(Boolean);\n return { code: 'validation-error', message: messages.join('; ') };\n }\n if (isRecord(body)) {\n return {\n code: readString(body, 'code'),\n message: readString(body, 'message'),\n };\n }\n return {};\n}\n\n/** Greenfield paths carry only store/invoice ids; strip the query string. */\nfunction redactPath(path: string): string {\n return path.split('?')[0];\n}\n\nfunction errorName(error: unknown): string {\n if (error instanceof Error) {\n return error.name === 'TimeoutError' ? 'timed out' : error.name;\n }\n return 'network error';\n}\n\nfunction normalizeBaseUrl(value: string): string {\n const raw = requireString(value, 'BtcpayClient baseUrl');\n let parsed: URL;\n try {\n parsed = new URL(raw);\n } catch (error) {\n throw new PaymentConfigurationError(\n 'BtcpayClient baseUrl must be an absolute URL.',\n { cause: error },\n );\n }\n if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {\n throw new PaymentConfigurationError(\n 'BtcpayClient baseUrl must use http or https.',\n );\n }\n if (\n parsed.search ||\n parsed.hash ||\n parsed.username ||\n parsed.password ||\n !['', '/'].includes(parsed.pathname)\n ) {\n throw new PaymentConfigurationError(\n 'BtcpayClient baseUrl must be an origin without path, query, fragment, or credentials.',\n );\n }\n return parsed.origin;\n}\n\nfunction requireString(value: unknown, label: string): string {\n if (typeof value !== 'string' || !value.trim()) {\n throw new PaymentConfigurationError(`${label} is required.`);\n }\n return value.trim();\n}\n\nfunction requirePositiveDecimal(value: unknown, label: string): string {\n if (\n typeof value !== 'string' ||\n !/^\\d+(\\.\\d+)?$/.test(value.trim()) ||\n !/[1-9]/.test(value)\n ) {\n throw new PaymentConfigurationError(\n `${label} must be a positive decimal string.`,\n );\n }\n return value.trim();\n}\n\nfunction requireCount(value: number, label: string): number {\n if (!Number.isSafeInteger(value) || value < 0) {\n throw new PaymentConfigurationError(\n `BTCPay ${label} must be a non-negative integer.`,\n );\n }\n return value;\n}\n\nfunction toList<T>(value: T | T[] | undefined): T[] {\n if (value === undefined) return [];\n return Array.isArray(value) ? value : [value];\n}\n\nfunction compact<T extends Record<string, unknown>>(value: T): Partial<T> {\n return Object.fromEntries(\n Object.entries(value).filter(([, item]) => item !== undefined),\n ) as Partial<T>;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === 'object' && !Array.isArray(value);\n}\n\nfunction readString(\n record: Record<string, unknown>,\n key: string,\n): string | undefined {\n const value = record[key];\n if (typeof value === 'string' && value.trim()) return value.trim();\n return undefined;\n}\n\n/** Keep decimals as strings; accept numbers Greenfield may emit. */\nfunction readDecimal(value: unknown): string | undefined {\n if (typeof value === 'string' && value.trim()) return value.trim();\n if (typeof value === 'number' && Number.isFinite(value)) {\n const text = String(value);\n return /e/i.test(text)\n ? value.toFixed(20).replace(/0+$/, '').replace(/\\.$/, '')\n : text;\n }\n return undefined;\n}\n\nfunction onChainTransactionId(id: string | undefined): string | undefined {\n const match = id ? /^([0-9a-f]{64})-\\d+$/i.exec(id) : null;\n return match ? match[1].toLowerCase() : undefined;\n}\n\n/** Greenfield timestamps are unix seconds; ISO strings are accepted too. */\nfunction readDate(value: unknown): Date | undefined {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return new Date(value * 1000);\n }\n if (typeof value === 'string' && value.trim()) {\n const date = new Date(value);\n return Number.isNaN(date.getTime()) ? undefined : date;\n }\n return undefined;\n}\n","/**\n * The BTCPay Server implementation of {@link CryptoCheckoutGateway}.\n *\n * Each checkout is a BTCPay invoice priced in fiat. BTCPay locks the rate for\n * `expirationMinutes` and settles by the invoice `speedPolicy`; this adapter\n * only maps BTCPay's states and never counts confirmations.\n */\nimport {\n type CreateCryptoCheckoutInput,\n type CryptoCheckout,\n type CryptoCheckoutEvent,\n type CryptoCheckoutException,\n type CryptoCheckoutGateway,\n CryptoCheckoutNotOwnedError,\n type CryptoCheckoutPayment,\n type CryptoCheckoutStatus,\n} from '../checkout-gateway.js';\nimport {\n PaymentConfigurationError,\n PaymentProviderError,\n PaymentVerificationError,\n} from '../errors.js';\nimport { currencyMinorUnitDecimals } from '../shared.js';\nimport {\n type BtcpayClient,\n type BtcpayInvoice,\n type BtcpayInvoicePaymentMethod,\n type BtcpaySpeedPolicy,\n isValidBtcpayWebhookSignature,\n parseBtcpayWebhook,\n} from './btcpay-client.js';\n\nexport const BTCPAY_GATEWAY_ID = 'btcpay';\n\nexport interface BtcpayCheckoutGatewayOptions {\n client: BtcpayClient;\n /** The store webhook's secret (`BTCPay-Sig` HMAC key). */\n webhookSecret: string;\n /**\n * Confirmations before BTCPay settles an on-chain payment: `HighSpeed` 0,\n * `MediumSpeed` 1, `LowMediumSpeed` 2, `LowSpeed` 6. Required: settlement\n * policy is an explicit decision, not a store default.\n */\n speedPolicy: BtcpaySpeedPolicy;\n /** BTCPay payment method ids (default `['BTC-CHAIN']`; add `BTC-LN` for Lightning). */\n paymentMethods?: string[];\n /** Rate lock in minutes (default 15). */\n expirationMinutes?: number;\n /** Minutes after expiry BTCPay still tracks late payments (default 1440). */\n monitoringMinutes?: number;\n /** Underpayment percentage BTCPay treats as paid (default 0). */\n paymentTolerance?: number;\n /** A label for the store's rate source, recorded on every checkout. */\n rateSource?: string;\n}\n\nconst LIVE_STATUSES = new Set(['New', 'Processing', 'Settled']);\n\nconst STATUS = new Map<string, CryptoCheckoutStatus>([\n ['New', 'open'],\n ['Processing', 'confirming'],\n ['Settled', 'settled'],\n ['Expired', 'expired'],\n ['Invalid', 'invalid'],\n]);\n\nconst EXCEPTION = new Map<string, CryptoCheckoutException>([\n ['None', 'none'],\n ['Invalid', 'none'],\n ['PaidPartial', 'underpaid'],\n ['PaidOver', 'overpaid'],\n ['PaidLate', 'paid_late'],\n ['Marked', 'manually_marked'],\n]);\n\n/**\n * Marks invoices this adapter created. Reuse and listing consider only those,\n * so an invoice made by hand (or by another integration) that happens to\n * share an order id is never reused or read as one of this port's checkouts.\n */\nconst OWNER_KEY = 'hvCheckoutGateway';\nconst OWNER_VALUE = 'crypto-checkout:v1';\n\n/** Keys this adapter writes into BTCPay invoice metadata itself. */\nconst RESERVED_METADATA = new Set([\n 'orderId',\n 'itemDesc',\n 'buyerEmail',\n OWNER_KEY,\n]);\n\nconst PAYMENT_STATUS = new Map<string, CryptoCheckoutPayment['status']>([\n ['Processing', 'confirming'],\n ['Settled', 'settled'],\n ['Invalid', 'invalid'],\n]);\n\nexport function createBtcpayCheckoutGateway(\n options: BtcpayCheckoutGatewayOptions,\n): CryptoCheckoutGateway {\n const { client } = options;\n if (!client) {\n throw new PaymentConfigurationError('BTCPay gateway requires a client.');\n }\n if (typeof options.webhookSecret !== 'string' || !options.webhookSecret) {\n throw new PaymentConfigurationError(\n 'BTCPay gateway requires a webhookSecret.',\n );\n }\n if (\n !['HighSpeed', 'MediumSpeed', 'LowMediumSpeed', 'LowSpeed'].includes(\n options.speedPolicy,\n )\n ) {\n throw new PaymentConfigurationError(\n 'BTCPay gateway speedPolicy must be HighSpeed, MediumSpeed, LowMediumSpeed, or LowSpeed.',\n );\n }\n const paymentMethods = options.paymentMethods ?? ['BTC-CHAIN'];\n if (\n !Array.isArray(paymentMethods) ||\n paymentMethods.length === 0 ||\n paymentMethods.some((method) => typeof method !== 'string' || !method)\n ) {\n throw new PaymentConfigurationError(\n 'BTCPay gateway paymentMethods must be a non-empty list of ids.',\n );\n }\n const assets = new Set(\n paymentMethods.map((method) => assetOfMethodId(method)),\n );\n if (assets.size !== 1) {\n throw new PaymentConfigurationError(\n 'BTCPay gateway paymentMethods must all settle in one asset (for example BTC-CHAIN and BTC-LN).',\n );\n }\n const expirationMinutes = positiveInteger(\n options.expirationMinutes ?? 15,\n 'expirationMinutes',\n );\n const monitoringMinutes = positiveInteger(\n options.monitoringMinutes ?? 1440,\n 'monitoringMinutes',\n );\n const paymentTolerance = options.paymentTolerance ?? 0;\n if (\n !Number.isFinite(paymentTolerance) ||\n paymentTolerance < 0 ||\n paymentTolerance > 100\n ) {\n throw new PaymentConfigurationError(\n 'BTCPay gateway paymentTolerance must be a percentage from 0 to 100.',\n );\n }\n\n async function load(invoice: BtcpayInvoice): Promise<CryptoCheckout> {\n const methods = await client.getInvoicePaymentMethods(invoice.id);\n return toCheckout(invoice, methods, options.rateSource);\n }\n\n async function liveInvoices(orderId: string): Promise<BtcpayInvoice[]> {\n const invoices = await client.listInvoices({\n orderId,\n includeArchived: true,\n });\n return invoices\n .filter(\n (invoice) =>\n ownedBy(invoice, orderId) && LIVE_STATUSES.has(invoice.status),\n )\n .sort(\n (a, b) =>\n (a.createdTime?.getTime() ?? 0) - (b.createdTime?.getTime() ?? 0) ||\n a.id.localeCompare(b.id),\n );\n }\n\n return {\n id: BTCPAY_GATEWAY_ID,\n\n async createCheckout(input: CreateCryptoCheckoutInput) {\n const orderId = requireText(input.orderId, 'orderId');\n const currency = requireText(input.currency, 'currency').toUpperCase();\n if (!Number.isSafeInteger(input.amount) || input.amount <= 0) {\n throw new PaymentConfigurationError(\n 'Checkout amount must be positive integer minor units.',\n );\n }\n const [existing] = await liveInvoices(orderId);\n if (existing) {\n const checkout = await load(existing);\n if (\n checkout.currency !== currency ||\n checkout.amount !== input.amount\n ) {\n throw new PaymentConfigurationError(\n `Order ${orderId} already has a checkout for ${checkout.amount} ${checkout.currency}; ` +\n `refusing to reuse it for ${input.amount} ${currency}.`,\n );\n }\n return checkout;\n }\n const metadata: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(input.metadata ?? {})) {\n if (RESERVED_METADATA.has(key)) {\n throw new PaymentConfigurationError(\n `Checkout metadata key '${key}' is reserved.`,\n );\n }\n if (typeof value !== 'string') {\n throw new PaymentConfigurationError(\n `Checkout metadata '${key}' must be a string.`,\n );\n }\n metadata[key] = value;\n }\n metadata[OWNER_KEY] = OWNER_VALUE;\n if (input.description) metadata.itemDesc = input.description;\n if (input.buyerEmail) metadata.buyerEmail = input.buyerEmail;\n const invoice = await client.createInvoice({\n amount: formatMinorUnits(\n input.amount,\n currencyMinorUnitDecimals(currency),\n ),\n currency,\n orderId,\n metadata,\n checkout: {\n speedPolicy: options.speedPolicy,\n paymentMethods,\n expirationMinutes,\n monitoringMinutes,\n paymentTolerance,\n redirectURL: input.redirectUrl,\n redirectAutomatically: input.redirectUrl ? true : undefined,\n },\n });\n const checkout = await load(invoice);\n if (checkout.currency !== currency || checkout.amount !== input.amount) {\n // The invoice exists (findable by orderId) but is not the price asked.\n throw new PaymentProviderError(\n `BTCPay created invoice ${checkout.id} for ${checkout.amount} ${checkout.currency}; ` +\n `requested ${input.amount} ${currency}.`,\n );\n }\n return checkout;\n },\n\n async getCheckout(checkoutId: string) {\n const invoice = await client.getInvoice(requireText(checkoutId, 'id'));\n if (!isOwned(invoice)) throw new CryptoCheckoutNotOwnedError(invoice.id);\n return load(invoice);\n },\n\n async listCheckouts(input: { orderId: string }) {\n const orderId = requireText(input.orderId, 'orderId');\n const invoices = (\n await client.listInvoices({ orderId, includeArchived: true })\n ).filter((invoice) => ownedBy(invoice, orderId));\n return Promise.all(invoices.map(load));\n },\n\n verifyWebhook(rawBody, headers): CryptoCheckoutEvent {\n const signature = readHeader(headers, 'btcpay-sig');\n if (\n !isValidBtcpayWebhookSignature(\n rawBody,\n signature,\n options.webhookSecret,\n )\n ) {\n throw new PaymentVerificationError(\n 'BTCPay webhook signature is invalid.',\n );\n }\n let delivery: ReturnType<typeof parseBtcpayWebhook>;\n try {\n delivery = parseBtcpayWebhook(rawBody);\n } catch (error) {\n throw new PaymentVerificationError('BTCPay webhook body is invalid.', {\n cause: error,\n });\n }\n if (delivery.storeId !== client.storeId) {\n throw new PaymentVerificationError(\n 'BTCPay webhook belongs to another store.',\n );\n }\n return {\n eventId: delivery.originalDeliveryId ?? delivery.deliveryId,\n checkoutId: delivery.invoiceId ?? null,\n type: delivery.type,\n redelivery: delivery.isRedelivery,\n };\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Mapping\n// ---------------------------------------------------------------------------\n\nfunction toCheckout(\n invoice: BtcpayInvoice,\n methods: BtcpayInvoicePaymentMethod[],\n rateSource: string | undefined,\n): CryptoCheckout {\n const status = STATUS.get(invoice.status);\n if (!status) {\n throw new PaymentProviderError(\n `BTCPay invoice ${invoice.id} has unknown status '${invoice.status}'.`,\n );\n }\n const exception = EXCEPTION.get(invoice.additionalStatus);\n if (!exception) {\n throw new PaymentProviderError(\n `BTCPay invoice ${invoice.id} has unknown additionalStatus '${invoice.additionalStatus}'.`,\n );\n }\n const currency = invoice.currency.toUpperCase();\n const fiatDecimals = currencyMinorUnitDecimals(currency);\n const amount = decimalToMinor(invoice.amount, fiatDecimals, 'invoice');\n\n // Fiat received: each method's own paid amount at its locked rate, summed\n // exactly and rounded down once.\n let paidScaled = Decimal.zero();\n for (const method of methods) {\n const hasPayments = method.payments.some(\n (payment) => paymentStatus(invoice.id, payment.status) !== 'invalid',\n );\n if (method.paymentMethodPaid === undefined) {\n if (hasPayments) {\n throw new PaymentProviderError(\n `BTCPay invoice ${invoice.id} payment method ${method.paymentMethodId} has payments but no paid amount.`,\n );\n }\n continue;\n }\n const paid = Decimal.parse(method.paymentMethodPaid, 'paymentMethodPaid');\n if (paid.isZero()) {\n if (hasPayments) {\n throw new PaymentProviderError(\n `BTCPay invoice ${invoice.id} payment method ${method.paymentMethodId} reports payments but zero paid.`,\n );\n }\n continue;\n }\n if (method.rate === undefined) {\n throw new PaymentProviderError(\n `BTCPay invoice ${invoice.id} payment method ${method.paymentMethodId} has payments but no rate.`,\n );\n }\n paidScaled = paidScaled.add(paid.mul(Decimal.parse(method.rate, 'rate')));\n }\n\n const primary =\n methods.find((method) => railOf(method.paymentMethodId) === 'onchain') ??\n methods[0];\n const settlementAsset = primary\n ? assetOf(primary.currency, primary.paymentMethodId)\n : undefined;\n const payments: CryptoCheckoutPayment[] = [];\n let nativePaid = Decimal.zero();\n for (const method of methods) {\n const asset = assetOf(method.currency, method.paymentMethodId);\n for (const payment of method.payments) {\n const mapped: CryptoCheckoutPayment = {\n id: payment.id,\n asset,\n rail: railOf(method.paymentMethodId),\n amount: payment.value,\n fee: Decimal.parse(payment.fee, 'fee').isZero()\n ? undefined\n : payment.fee,\n status: paymentStatus(invoice.id, payment.status),\n transactionId: payment.transactionId,\n receivedAt: payment.receivedDate,\n };\n payments.push(mapped);\n if (mapped.status !== 'invalid' && asset === settlementAsset) {\n nativePaid = nativePaid.add(Decimal.parse(payment.value, 'payment'));\n }\n }\n }\n\n const metadata: Record<string, string> = {};\n for (const [key, value] of Object.entries(invoice.metadata)) {\n // Only the caller's own keys; the adapter's are surfaced as fields.\n if (typeof value === 'string' && !RESERVED_METADATA.has(key)) {\n metadata[key] = value;\n }\n }\n\n return {\n gateway: BTCPAY_GATEWAY_ID,\n id: invoice.id,\n orderId:\n typeof invoice.metadata.orderId === 'string'\n ? invoice.metadata.orderId\n : undefined,\n status,\n exception,\n amount,\n currency,\n amountPaid: paidScaled.floorToScale(fiatDecimals),\n settlementAsset: settlementAsset || undefined,\n nativeAmountDue: primary?.amount,\n nativeAmountPaid: methods.length ? nativePaid.toString() : undefined,\n rate: primary?.rate,\n rateSource,\n checkoutUrl: invoice.checkoutLink,\n createdAt: invoice.createdTime,\n expiresAt: invoice.expirationTime,\n metadata,\n payments,\n raw: invoice.raw,\n };\n}\n\n/** `2500, 2` → `'25.00'`: fixed decimals, exact. */\nfunction formatMinorUnits(amount: number, decimals: number): string {\n const text = String(amount).padStart(decimals + 1, '0');\n return decimals === 0\n ? text\n : `${text.slice(0, -decimals)}.${text.slice(-decimals)}`;\n}\n\nfunction isOwned(invoice: BtcpayInvoice): boolean {\n // Any version of the marker: bumping OWNER_VALUE must not orphan live\n // invoices written by an earlier version.\n const marker = invoice.metadata[OWNER_KEY];\n return typeof marker === 'string' && marker.startsWith('crypto-checkout:');\n}\n\nfunction ownedBy(invoice: BtcpayInvoice, orderId: string): boolean {\n return invoice.metadata.orderId === orderId && isOwned(invoice);\n}\n\nfunction paymentStatus(\n invoiceId: string,\n status: string,\n): CryptoCheckoutPayment['status'] {\n const mapped = PAYMENT_STATUS.get(status);\n if (!mapped) {\n throw new PaymentProviderError(\n `BTCPay invoice ${invoiceId} has a payment with unknown status '${status}'.`,\n );\n }\n return mapped;\n}\n\nfunction assetOfMethodId(paymentMethodId: string): string {\n return (paymentMethodId.split('-')[0] ?? '').toUpperCase();\n}\n\nfunction assetOf(currency: string | undefined, paymentMethodId: string) {\n return (currency ?? assetOfMethodId(paymentMethodId)).toUpperCase();\n}\n\nfunction railOf(paymentMethodId: string): string {\n const id = paymentMethodId.toUpperCase();\n if (id === 'BTC' || id.endsWith('-CHAIN')) return 'onchain';\n if (id.endsWith('-LN') || id.endsWith('-LNURL')) return 'lightning';\n return paymentMethodId.toLowerCase();\n}\n\nfunction decimalToMinor(value: string, decimals: number, label: string) {\n const parsed = Decimal.parse(value, label);\n const minor = parsed.floorToScale(decimals);\n if (!parsed.equalsScaled(minor, decimals)) {\n throw new PaymentProviderError(\n `BTCPay ${label} amount ${value} has more precision than its currency.`,\n );\n }\n return minor;\n}\n\n/** Exact non-negative decimal arithmetic on BigInt (value = units / 10^scale). */\nclass Decimal {\n private constructor(\n readonly units: bigint,\n readonly scale: number,\n ) {}\n\n static zero(): Decimal {\n return new Decimal(0n, 0);\n }\n\n static parse(value: string, label: string): Decimal {\n const match = /^(\\d+)(?:\\.(\\d+))?$/.exec(value?.trim?.() ?? '');\n if (!match) {\n throw new PaymentProviderError(\n `BTCPay ${label} '${String(value)}' is not a non-negative decimal.`,\n );\n }\n const fraction = match[2] ?? '';\n return new Decimal(BigInt(`${match[1]}${fraction}`), fraction.length);\n }\n\n isZero(): boolean {\n return this.units === 0n;\n }\n\n add(other: Decimal): Decimal {\n const scale = Math.max(this.scale, other.scale);\n return new Decimal(\n this.units * 10n ** BigInt(scale - this.scale) +\n other.units * 10n ** BigInt(scale - other.scale),\n scale,\n );\n }\n\n mul(other: Decimal): Decimal {\n return new Decimal(this.units * other.units, this.scale + other.scale);\n }\n\n /** Integer count of 10^-decimals units, rounded down. */\n floorToScale(decimals: number): number {\n const shifted =\n decimals >= this.scale\n ? this.units * 10n ** BigInt(decimals - this.scale)\n : this.units / 10n ** BigInt(this.scale - decimals);\n if (shifted > BigInt(Number.MAX_SAFE_INTEGER)) {\n throw new PaymentProviderError(\n 'BTCPay amount exceeds the safe integer range.',\n );\n }\n return Number(shifted);\n }\n\n equalsScaled(minor: number, decimals: number): boolean {\n const scale = Math.max(this.scale, decimals);\n return (\n this.units * 10n ** BigInt(scale - this.scale) ===\n BigInt(minor) * 10n ** BigInt(scale - decimals)\n );\n }\n\n toString(): string {\n if (this.scale === 0) return this.units.toString();\n const text = this.units.toString().padStart(this.scale + 1, '0');\n return `${text.slice(0, -this.scale)}.${text.slice(-this.scale)}`;\n }\n}\n\nfunction readHeader(\n headers: Headers | Record<string, string | undefined>,\n name: string,\n): string | undefined {\n if (typeof (headers as Headers)?.get === 'function') {\n return (headers as Headers).get(name) ?? undefined;\n }\n for (const [key, value] of Object.entries(headers ?? {})) {\n if (key.toLowerCase() === name) return value;\n }\n return undefined;\n}\n\nfunction requireText(value: unknown, label: string): string {\n if (typeof value !== 'string' || !value.trim()) {\n throw new PaymentConfigurationError(`Checkout ${label} is required.`);\n }\n return value.trim();\n}\n\nfunction positiveInteger(value: number, label: string): number {\n if (!Number.isSafeInteger(value) || value <= 0) {\n throw new PaymentConfigurationError(\n `BTCPay gateway ${label} must be a positive integer.`,\n );\n }\n return value;\n}\n"],"mappings":"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"}
|