thunder-bridge 2.2.0 → 3.0.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 +93 -412
- package/dist/bank.cjs +13 -3
- package/dist/bank.d.cts +9 -3
- package/dist/bank.d.ts +9 -3
- package/dist/bank.js +18 -3
- package/dist/index.cjs +254 -113
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +259 -112
- package/dist/{bank-B5MxT_6u.d.ts → nwc-BivXGSPY.d.ts} +80 -118
- package/dist/{bank-Bg6RjuO-.d.cts → nwc-h_-cGvMb.d.cts} +80 -118
- package/dist/nwc.cjs +10 -424
- package/dist/nwc.d.cts +2 -18
- package/dist/nwc.d.ts +2 -18
- package/dist/nwc.js +10 -422
- package/openapi.yaml +1 -1
- package/package.json +8 -2
package/dist/nwc.js
CHANGED
|
@@ -349,8 +349,6 @@ function asBuffer(bytes) {
|
|
|
349
349
|
}
|
|
350
350
|
|
|
351
351
|
// ../core/sealed.ts
|
|
352
|
-
var VERSION = "v2";
|
|
353
|
-
var UNPADDED_VERSION = "v1";
|
|
354
352
|
var PURPOSE_VERSION = "v3";
|
|
355
353
|
var IV_BYTES = 12;
|
|
356
354
|
var MIN_SECRET_CHARS = 32;
|
|
@@ -358,15 +356,6 @@ var MAX_PLAIN_BYTES = 3e3;
|
|
|
358
356
|
var SIZE_CLASSES = [256, 1024, MAX_PLAIN_BYTES];
|
|
359
357
|
var END_OF_TEXT = 128;
|
|
360
358
|
var INFO = "thunder-bridge/sealed";
|
|
361
|
-
async function seal(secret, plaintext, paymentHash) {
|
|
362
|
-
return await sealUnder(
|
|
363
|
-
await keyFor(secret, INFO),
|
|
364
|
-
plaintext,
|
|
365
|
-
crypto.getRandomValues(new Uint8Array(IV_BYTES)),
|
|
366
|
-
VERSION,
|
|
367
|
-
boundTo(paymentHash)
|
|
368
|
-
);
|
|
369
|
-
}
|
|
370
359
|
async function sealFor(purpose, secret, plaintext, { stable = false } = {}) {
|
|
371
360
|
return await sealUnder(
|
|
372
361
|
await keyFor(secret, `${INFO}/${purpose}`),
|
|
@@ -376,16 +365,6 @@ async function sealFor(purpose, secret, plaintext, { stable = false } = {}) {
|
|
|
376
365
|
new TextEncoder().encode(PURPOSE_VERSION)
|
|
377
366
|
);
|
|
378
367
|
}
|
|
379
|
-
async function unsealFor(purpose, secret, sealed) {
|
|
380
|
-
if (!sealed.startsWith(`${PURPOSE_VERSION}.`)) {
|
|
381
|
-
return await unseal(secret, sealed);
|
|
382
|
-
}
|
|
383
|
-
return await opened(
|
|
384
|
-
await keyFor(secret, `${INFO}/${purpose}`),
|
|
385
|
-
sealed.slice(PURPOSE_VERSION.length + 1),
|
|
386
|
-
new TextEncoder().encode(PURPOSE_VERSION)
|
|
387
|
-
);
|
|
388
|
-
}
|
|
389
368
|
async function nonceOf(secret, plaintext) {
|
|
390
369
|
const derived = await hmacHex(secret, `sealed-nonce|${plaintext}`);
|
|
391
370
|
const nonce = new Uint8Array(IV_BYTES);
|
|
@@ -404,48 +383,11 @@ async function sealUnder(key, plaintext, iv, version, additionalData) {
|
|
|
404
383
|
joined2.set(cipher, iv.length);
|
|
405
384
|
return `${version}.${toBase64Url(joined2)}`;
|
|
406
385
|
}
|
|
407
|
-
async function unseal(secret, sealed, paymentHash) {
|
|
408
|
-
const key = await keyFor(secret, INFO);
|
|
409
|
-
const padded2 = sealed.startsWith(`${VERSION}.`);
|
|
410
|
-
const unbound = !padded2 && sealed.startsWith(`${UNPADDED_VERSION}.`);
|
|
411
|
-
if (!padded2 && (!unbound || paymentHash !== void 0)) {
|
|
412
|
-
return null;
|
|
413
|
-
}
|
|
414
|
-
return await opened(
|
|
415
|
-
key,
|
|
416
|
-
sealed.slice(VERSION.length + 1),
|
|
417
|
-
padded2 ? boundTo(paymentHash) : void 0
|
|
418
|
-
);
|
|
419
|
-
}
|
|
420
|
-
async function opened(key, encoded, additionalData) {
|
|
421
|
-
const joined2 = fromBase64Url(encoded);
|
|
422
|
-
if (joined2 === null || joined2.length <= IV_BYTES) {
|
|
423
|
-
return null;
|
|
424
|
-
}
|
|
425
|
-
try {
|
|
426
|
-
const body = new Uint8Array(
|
|
427
|
-
await crypto.subtle.decrypt(
|
|
428
|
-
{ name: "AES-GCM", iv: joined2.slice(0, IV_BYTES), additionalData },
|
|
429
|
-
key,
|
|
430
|
-
joined2.slice(IV_BYTES)
|
|
431
|
-
)
|
|
432
|
-
);
|
|
433
|
-
const text = additionalData === void 0 ? body : unpad(body);
|
|
434
|
-
return text === null ? null : new TextDecoder().decode(text);
|
|
435
|
-
} catch {
|
|
436
|
-
return null;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
386
|
function refuseAWeakSecret(secret) {
|
|
440
387
|
if (secret.length < MIN_SECRET_CHARS) {
|
|
441
388
|
throw new Error(`the sealing secret needs ${MIN_SECRET_CHARS} characters of randomness`);
|
|
442
389
|
}
|
|
443
390
|
}
|
|
444
|
-
function boundTo(paymentHash) {
|
|
445
|
-
return new TextEncoder().encode(
|
|
446
|
-
paymentHash === void 0 ? VERSION : `${VERSION}|${paymentHash.toLowerCase()}`
|
|
447
|
-
);
|
|
448
|
-
}
|
|
449
391
|
function padToClass(plaintext) {
|
|
450
392
|
const text = new TextEncoder().encode(plaintext);
|
|
451
393
|
const size = SIZE_CLASSES.find((limit) => text.length <= limit);
|
|
@@ -457,10 +399,6 @@ function padToClass(plaintext) {
|
|
|
457
399
|
body[text.length] = END_OF_TEXT;
|
|
458
400
|
return body;
|
|
459
401
|
}
|
|
460
|
-
function unpad(body) {
|
|
461
|
-
const end = body.lastIndexOf(END_OF_TEXT);
|
|
462
|
-
return end === -1 ? null : body.subarray(0, end);
|
|
463
|
-
}
|
|
464
402
|
async function keyFor(secret, info) {
|
|
465
403
|
refuseAWeakSecret(secret);
|
|
466
404
|
const material = await crypto.subtle.importKey(
|
|
@@ -490,21 +428,6 @@ function toBase64Url(bytes) {
|
|
|
490
428
|
}
|
|
491
429
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
492
430
|
}
|
|
493
|
-
function fromBase64Url(text) {
|
|
494
|
-
if (text.length === 0 || !/^[A-Za-z0-9_-]+$/.test(text)) {
|
|
495
|
-
return null;
|
|
496
|
-
}
|
|
497
|
-
try {
|
|
498
|
-
const binary = atob(text.replace(/-/g, "+").replace(/_/g, "/"));
|
|
499
|
-
const bytes = new Uint8Array(binary.length);
|
|
500
|
-
for (let at = 0; at < bytes.length; at++) {
|
|
501
|
-
bytes[at] = binary.charCodeAt(at);
|
|
502
|
-
}
|
|
503
|
-
return bytes;
|
|
504
|
-
} catch {
|
|
505
|
-
return null;
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
431
|
|
|
509
432
|
// src/nostr.ts
|
|
510
433
|
import { chacha20 } from "@noble/ciphers/chacha.js";
|
|
@@ -661,274 +584,6 @@ function fromBase64(text) {
|
|
|
661
584
|
return bytes;
|
|
662
585
|
}
|
|
663
586
|
|
|
664
|
-
// src/qr.ts
|
|
665
|
-
import { encode } from "uqr";
|
|
666
|
-
function toLightningUri(destination) {
|
|
667
|
-
const scanned = isLnAddress(destination) ? destination : destination.toUpperCase();
|
|
668
|
-
return `LIGHTNING:${scanned}`;
|
|
669
|
-
}
|
|
670
|
-
function isLnAddress(destination) {
|
|
671
|
-
const at = destination.indexOf("@");
|
|
672
|
-
return at > 0 && destination.slice(at + 1).includes(".");
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
// src/errors.ts
|
|
676
|
-
var AmountError = class extends Error {
|
|
677
|
-
/**
|
|
678
|
-
* Whether a failure is one of these, without asking whether it is this exact
|
|
679
|
-
* class. Every entry point carries its own copy of the class, so a price
|
|
680
|
-
* refused inside `thunder-bridge/price` is not `instanceof` the `AmountError`
|
|
681
|
-
* imported from `thunder-bridge`. The name and the code are the same in every
|
|
682
|
-
* copy, so this holds where `instanceof` does not
|
|
683
|
-
*/
|
|
684
|
-
static is(failure) {
|
|
685
|
-
return failure instanceof Error && failure.name === "AmountError" && "code" in failure;
|
|
686
|
-
}
|
|
687
|
-
code;
|
|
688
|
-
constructor(code, detail) {
|
|
689
|
-
super(`${code}: ${detail}`);
|
|
690
|
-
this.name = "AmountError";
|
|
691
|
-
this.code = code;
|
|
692
|
-
}
|
|
693
|
-
};
|
|
694
|
-
|
|
695
|
-
// src/currency.ts
|
|
696
|
-
var ISO_4217_MINOR_UNITS = {
|
|
697
|
-
BHD: 3,
|
|
698
|
-
BIF: 0,
|
|
699
|
-
CLP: 0,
|
|
700
|
-
CZK: 2,
|
|
701
|
-
DJF: 0,
|
|
702
|
-
EUR: 2,
|
|
703
|
-
GBP: 2,
|
|
704
|
-
ISK: 0,
|
|
705
|
-
IQD: 3,
|
|
706
|
-
JOD: 3,
|
|
707
|
-
JPY: 0,
|
|
708
|
-
KRW: 0,
|
|
709
|
-
KWD: 3,
|
|
710
|
-
LYD: 3,
|
|
711
|
-
OMR: 3,
|
|
712
|
-
PLN: 2,
|
|
713
|
-
PYG: 0,
|
|
714
|
-
RWF: 0,
|
|
715
|
-
TND: 3,
|
|
716
|
-
UGX: 0,
|
|
717
|
-
USD: 2,
|
|
718
|
-
VND: 0,
|
|
719
|
-
VUV: 0,
|
|
720
|
-
XAF: 0,
|
|
721
|
-
XOF: 0,
|
|
722
|
-
XPF: 0
|
|
723
|
-
};
|
|
724
|
-
function minorUnitsOf(currency) {
|
|
725
|
-
const digits = ISO_4217_MINOR_UNITS[currency.toUpperCase()];
|
|
726
|
-
if (digits === void 0) {
|
|
727
|
-
throw new AmountError(
|
|
728
|
-
"unknown-currency",
|
|
729
|
-
`${currency.toUpperCase()} is not a currency this knows the ISO 4217 minor unit of`
|
|
730
|
-
);
|
|
731
|
-
}
|
|
732
|
-
return digits;
|
|
733
|
-
}
|
|
734
|
-
function minorScaleOf(currency) {
|
|
735
|
-
return 10 ** minorUnitsOf(currency);
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
// src/price.ts
|
|
739
|
-
var MSAT_PER_BTC2 = 1e11;
|
|
740
|
-
var BASIS_POINTS = 1e4;
|
|
741
|
-
var DEFAULT_HOLD_SECS = 60;
|
|
742
|
-
var DEFAULT_MIN_VENUES = 2;
|
|
743
|
-
var DEFAULT_MAX_SPREAD_BPS = 200;
|
|
744
|
-
var MILLIS = 1e3;
|
|
745
|
-
function coinbase(baseUrl = "https://api.coinbase.com") {
|
|
746
|
-
return async (currency) => {
|
|
747
|
-
const asked = currency.toUpperCase();
|
|
748
|
-
const answer = await asJson(`${baseUrl}/v2/prices/BTC-${asked}/spot`, "coinbase");
|
|
749
|
-
const quoted = answer.data;
|
|
750
|
-
if (quoted?.currency !== asked) {
|
|
751
|
-
throw new Error(`coinbase quoted ${quoted?.currency} when asked for ${asked}`);
|
|
752
|
-
}
|
|
753
|
-
return asMinor(quoted.amount, "coinbase", asked);
|
|
754
|
-
};
|
|
755
|
-
}
|
|
756
|
-
function kraken(baseUrl = "https://api.kraken.com") {
|
|
757
|
-
return async (currency) => {
|
|
758
|
-
const asked = currency.toUpperCase();
|
|
759
|
-
const answer = await asJson(`${baseUrl}/0/public/Ticker?pair=XBT${asked}`, "kraken");
|
|
760
|
-
if (answer.error?.length) {
|
|
761
|
-
throw new Error(`kraken refused XBT${asked}: ${answer.error[0]}`);
|
|
762
|
-
}
|
|
763
|
-
const pairs = Object.values(answer.result ?? {});
|
|
764
|
-
if (pairs.length !== 1) {
|
|
765
|
-
throw new Error(`kraken answered ${pairs.length} pairs for XBT${asked}`);
|
|
766
|
-
}
|
|
767
|
-
return asMinor(pairs[0]?.c?.[0], "kraken", asked);
|
|
768
|
-
};
|
|
769
|
-
}
|
|
770
|
-
function bitstamp(baseUrl = "https://www.bitstamp.net") {
|
|
771
|
-
return async (currency) => {
|
|
772
|
-
const pair = `btc${currency.toLowerCase()}`;
|
|
773
|
-
const answer = await asJson(`${baseUrl}/api/v2/ticker/${pair}/`, "bitstamp");
|
|
774
|
-
if (Array.isArray(answer)) {
|
|
775
|
-
throw new Error(`bitstamp has no ${pair} pair`);
|
|
776
|
-
}
|
|
777
|
-
return asMinor(answer.last, "bitstamp", currency);
|
|
778
|
-
};
|
|
779
|
-
}
|
|
780
|
-
function coinmate(baseUrl = "https://coinmate.io") {
|
|
781
|
-
return async (currency) => {
|
|
782
|
-
const pair = `BTC_${currency.toUpperCase()}`;
|
|
783
|
-
const answer = await asJson(`${baseUrl}/api/ticker?currencyPair=${pair}`, "coinmate");
|
|
784
|
-
if (answer.error !== false) {
|
|
785
|
-
throw new Error(`coinmate refused ${pair}: ${answer.errorMessage ?? "no reason"}`);
|
|
786
|
-
}
|
|
787
|
-
return asMinor(answer.data?.last, "coinmate", currency);
|
|
788
|
-
};
|
|
789
|
-
}
|
|
790
|
-
function medianOf(tickers = [coinbase(), kraken(), bitstamp(), coinmate()], options = {}) {
|
|
791
|
-
const holdFor = (options.holdForSecs ?? DEFAULT_HOLD_SECS) * MILLIS;
|
|
792
|
-
const minVenues = options.minVenues ?? DEFAULT_MIN_VENUES;
|
|
793
|
-
const maxSpread = options.maxSpreadBps ?? DEFAULT_MAX_SPREAD_BPS;
|
|
794
|
-
const held = /* @__PURE__ */ new Map();
|
|
795
|
-
return async (currency) => {
|
|
796
|
-
const asked = currency.toUpperCase();
|
|
797
|
-
const remembered = held.get(asked);
|
|
798
|
-
if (remembered && Date.now() - remembered.at < holdFor) {
|
|
799
|
-
return remembered.price;
|
|
800
|
-
}
|
|
801
|
-
const answers = await Promise.allSettled(tickers.map((ticker) => ticker(asked)));
|
|
802
|
-
const quoted = answers.filter((answer) => answer.status === "fulfilled").map((answer) => answer.value).sort((one, other) => one - other);
|
|
803
|
-
if (quoted.length < minVenues) {
|
|
804
|
-
throw new Error(
|
|
805
|
-
`${quoted.length} of ${tickers.length} venues quoted ${asked}, ${minVenues} wanted: ${refusals(answers)}`
|
|
806
|
-
);
|
|
807
|
-
}
|
|
808
|
-
const spread = spreadBpsOf(quoted);
|
|
809
|
-
if (spread > maxSpread) {
|
|
810
|
-
throw new Error(
|
|
811
|
-
`venues disagree on ${asked} by ${Math.round(spread)} bps, more than the ${maxSpread} allowed: ${quoted.join(", ")}`
|
|
812
|
-
);
|
|
813
|
-
}
|
|
814
|
-
const price = middleOf(quoted);
|
|
815
|
-
held.set(asked, { at: Date.now(), price });
|
|
816
|
-
return price;
|
|
817
|
-
};
|
|
818
|
-
}
|
|
819
|
-
function msatFor(amountMinor, priceMinorPerBtc, options = {}) {
|
|
820
|
-
if (!Number.isInteger(amountMinor) || amountMinor <= 0) {
|
|
821
|
-
throw new Error("amountMinor must be a whole number of minor units above zero");
|
|
822
|
-
}
|
|
823
|
-
if (!Number.isFinite(priceMinorPerBtc) || priceMinorPerBtc <= 0) {
|
|
824
|
-
throw new Error(`${priceMinorPerBtc} is not a usable price`);
|
|
825
|
-
}
|
|
826
|
-
const spreadBps = options.spreadBps ?? 0;
|
|
827
|
-
if (!Number.isInteger(spreadBps) || spreadBps < 0) {
|
|
828
|
-
throw new Error("spreadBps must be a whole number of basis points, none or more");
|
|
829
|
-
}
|
|
830
|
-
const wanted = BigInt(amountMinor) * BigInt(MSAT_PER_BTC2) * BigInt(BASIS_POINTS + spreadBps);
|
|
831
|
-
const per = BigInt(Math.round(priceMinorPerBtc)) * BigInt(BASIS_POINTS);
|
|
832
|
-
const msat2 = (wanted + per - 1n) / per;
|
|
833
|
-
if (msat2 > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
834
|
-
throw new Error(`${amountMinor} at ${priceMinorPerBtc} is more millisatoshi than exist`);
|
|
835
|
-
}
|
|
836
|
-
return Number(msat2);
|
|
837
|
-
}
|
|
838
|
-
async function asJson(url, venue) {
|
|
839
|
-
const answer = await fetch(url, { headers: { accept: "application/json" } });
|
|
840
|
-
if (!answer.ok) {
|
|
841
|
-
throw new Error(`${venue} answered ${answer.status}`);
|
|
842
|
-
}
|
|
843
|
-
return answer.json();
|
|
844
|
-
}
|
|
845
|
-
function asMinor(quoted, venue, currency) {
|
|
846
|
-
const major = Number(quoted);
|
|
847
|
-
if (!Number.isFinite(major) || major <= 0) {
|
|
848
|
-
throw new Error(`${venue} quoted ${String(quoted)} for ${currency.toUpperCase()}`);
|
|
849
|
-
}
|
|
850
|
-
return Math.round(major * minorScaleOf(currency));
|
|
851
|
-
}
|
|
852
|
-
function spreadBpsOf(sorted) {
|
|
853
|
-
const lowest = sorted[0] ?? 0;
|
|
854
|
-
const highest = sorted[sorted.length - 1] ?? 0;
|
|
855
|
-
return lowest === 0 ? Number.POSITIVE_INFINITY : (highest - lowest) / lowest * BASIS_POINTS;
|
|
856
|
-
}
|
|
857
|
-
function middleOf(sorted) {
|
|
858
|
-
const middle = Math.floor(sorted.length / 2);
|
|
859
|
-
if (sorted.length % 2 === 1) {
|
|
860
|
-
return sorted[middle] ?? 0;
|
|
861
|
-
}
|
|
862
|
-
return Math.round(((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2);
|
|
863
|
-
}
|
|
864
|
-
function refusals(answers) {
|
|
865
|
-
return answers.filter((answer) => answer.status === "rejected").map(
|
|
866
|
-
(answer) => answer.reason instanceof Error ? answer.reason.message : String(answer.reason)
|
|
867
|
-
).join("; ");
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
// src/amount.ts
|
|
871
|
-
var MSAT_PER_SAT = 1e3;
|
|
872
|
-
var MAX_SATS = Math.floor(Number.MAX_SAFE_INTEGER / MSAT_PER_SAT);
|
|
873
|
-
function msat(exact) {
|
|
874
|
-
if (!Number.isSafeInteger(exact) || exact < 1) {
|
|
875
|
-
throw new AmountError(
|
|
876
|
-
"not-whole-millisatoshi",
|
|
877
|
-
`${exact} is not a payable count of millisatoshi`
|
|
878
|
-
);
|
|
879
|
-
}
|
|
880
|
-
return exact;
|
|
881
|
-
}
|
|
882
|
-
async function amountNow(amount) {
|
|
883
|
-
return typeof amount === "function" ? msat(await amount()) : msat(amount);
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
// ../core/ed25519.ts
|
|
887
|
-
var PKCS8_HEADER = hexToBytes("302e020100300506032b657004220420");
|
|
888
|
-
var SMALL_ORDER = [
|
|
889
|
-
"00".repeat(32),
|
|
890
|
-
`01${"00".repeat(31)}`,
|
|
891
|
-
"26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05",
|
|
892
|
-
"c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a",
|
|
893
|
-
`ec${"ff".repeat(30)}7f`,
|
|
894
|
-
`ed${"ff".repeat(30)}7f`,
|
|
895
|
-
`ee${"ff".repeat(30)}7f`
|
|
896
|
-
];
|
|
897
|
-
|
|
898
|
-
// src/webhook.ts
|
|
899
|
-
var VERIFY_CHALLENGE = "verify-challenge";
|
|
900
|
-
async function answerVerifyChallenge(request) {
|
|
901
|
-
if (request.method !== "POST") {
|
|
902
|
-
return null;
|
|
903
|
-
}
|
|
904
|
-
const nonce = nonceOf2(await request.clone().text(), VERIFY_CHALLENGE);
|
|
905
|
-
return nonce === null ? null : answered(nonce);
|
|
906
|
-
}
|
|
907
|
-
function answered(nonce) {
|
|
908
|
-
return new Response(JSON.stringify({ nonce }), {
|
|
909
|
-
headers: { "content-type": "application/json" }
|
|
910
|
-
});
|
|
911
|
-
}
|
|
912
|
-
function nonceOf2(text, type) {
|
|
913
|
-
try {
|
|
914
|
-
const said = JSON.parse(text);
|
|
915
|
-
if (said["type"] !== type || typeof said["nonce"] !== "string") {
|
|
916
|
-
return null;
|
|
917
|
-
}
|
|
918
|
-
return said["nonce"];
|
|
919
|
-
} catch {
|
|
920
|
-
return null;
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
// src/rail.ts
|
|
925
|
-
async function msatForOrder(order, amount, rate) {
|
|
926
|
-
if (amount !== void 0) {
|
|
927
|
-
return await amountNow(amount(order));
|
|
928
|
-
}
|
|
929
|
-
return msat(msatFor(order.amountMinor, await (rate ?? medianOf())(order.currency)));
|
|
930
|
-
}
|
|
931
|
-
|
|
932
587
|
// src/nwc.ts
|
|
933
588
|
var REQUEST_KIND = 23194;
|
|
934
589
|
var RESPONSE_KIND = 23195;
|
|
@@ -937,7 +592,6 @@ var SCHEME = "nostr+walletconnect:";
|
|
|
937
592
|
var HEX_32_BYTES = /^[0-9a-f]{64}$/i;
|
|
938
593
|
var DEFAULT_ASK_TIMEOUT_MS = 1e4;
|
|
939
594
|
var PAY_TIMEOUT_MS = 9e4;
|
|
940
|
-
var DEFAULT_POLL_EVERY_SECS = 5;
|
|
941
595
|
var HASH = "h";
|
|
942
596
|
var REFUSAL_BY_CODE = {
|
|
943
597
|
INSUFFICIENT_BALANCE: "amount-not-accepted",
|
|
@@ -963,13 +617,13 @@ function nwcConnection(uri) {
|
|
|
963
617
|
return { walletPubkey: walletPubkey.toLowerCase(), relays, secret: secret.toLowerCase() };
|
|
964
618
|
}
|
|
965
619
|
async function nwcInvoice(connection, amountMsat, description, timeoutMs = DEFAULT_ASK_TIMEOUT_MS) {
|
|
966
|
-
const
|
|
620
|
+
const answered = await askWallet(
|
|
967
621
|
connection,
|
|
968
622
|
"make_invoice",
|
|
969
623
|
{ amount: amountMsat, description },
|
|
970
624
|
timeoutMs
|
|
971
625
|
);
|
|
972
|
-
const bolt11 = String(
|
|
626
|
+
const bolt11 = String(answered.invoice ?? "");
|
|
973
627
|
const issued = decodeInvoice(bolt11);
|
|
974
628
|
if (issued.paymentHash === null || issued.expiresAt === null) {
|
|
975
629
|
throw new WalletRefused("invoice-refused", "the nwc wallet issued an invoice we cannot decode");
|
|
@@ -983,7 +637,7 @@ async function nwcInvoice(connection, amountMsat, description, timeoutMs = DEFAU
|
|
|
983
637
|
return { bolt11, paymentHash: issued.paymentHash, expiresAt: issued.expiresAt };
|
|
984
638
|
}
|
|
985
639
|
async function nwcHoldInvoice(connection, held, timeoutMs = DEFAULT_ASK_TIMEOUT_MS) {
|
|
986
|
-
const
|
|
640
|
+
const answered = await askWallet(
|
|
987
641
|
connection,
|
|
988
642
|
"make_hold_invoice",
|
|
989
643
|
{
|
|
@@ -995,7 +649,7 @@ async function nwcHoldInvoice(connection, held, timeoutMs = DEFAULT_ASK_TIMEOUT_
|
|
|
995
649
|
},
|
|
996
650
|
timeoutMs
|
|
997
651
|
);
|
|
998
|
-
const bolt11 = String(
|
|
652
|
+
const bolt11 = String(answered.invoice ?? "");
|
|
999
653
|
const issued = decodeInvoice(bolt11);
|
|
1000
654
|
if (issued.paymentHash === null || issued.expiresAt === null) {
|
|
1001
655
|
throw new WalletRefused("invoice-refused", "the nwc wallet held an invoice we cannot decode");
|
|
@@ -1015,13 +669,13 @@ async function nwcHoldInvoice(connection, held, timeoutMs = DEFAULT_ASK_TIMEOUT_
|
|
|
1015
669
|
return { bolt11, paymentHash: issued.paymentHash, expiresAt: issued.expiresAt };
|
|
1016
670
|
}
|
|
1017
671
|
async function nwcSettlement(connection, paymentHash, timeoutMs = DEFAULT_ASK_TIMEOUT_MS) {
|
|
1018
|
-
const
|
|
672
|
+
const answered = await askWallet(
|
|
1019
673
|
connection,
|
|
1020
674
|
"lookup_invoice",
|
|
1021
675
|
{ payment_hash: paymentHash },
|
|
1022
676
|
timeoutMs
|
|
1023
677
|
);
|
|
1024
|
-
const preimage =
|
|
678
|
+
const preimage = answered.preimage;
|
|
1025
679
|
if (typeof preimage !== "string" || preimage.length === 0) {
|
|
1026
680
|
return null;
|
|
1027
681
|
}
|
|
@@ -1035,39 +689,13 @@ async function nwcPay(connection, bolt11, timeoutMs = PAY_TIMEOUT_MS) {
|
|
|
1035
689
|
if (owed.paymentHash === null) {
|
|
1036
690
|
throw new WalletRefused("invoice-refused", "the invoice to pay carries no payment hash");
|
|
1037
691
|
}
|
|
1038
|
-
const
|
|
1039
|
-
const preimage =
|
|
692
|
+
const answered = await askWallet(connection, "pay_invoice", { invoice: bolt11 }, timeoutMs);
|
|
693
|
+
const preimage = answered.preimage;
|
|
1040
694
|
if (typeof preimage !== "string" || !preimageMatchesHash(preimage, owed.paymentHash)) {
|
|
1041
695
|
throw new Error(`the nwc wallet paid and returned no preimage hashing to ${owed.paymentHash}`);
|
|
1042
696
|
}
|
|
1043
697
|
return preimage;
|
|
1044
698
|
}
|
|
1045
|
-
function nwcVerifyEndpoint(config) {
|
|
1046
|
-
const paced = {
|
|
1047
|
-
"cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS}`
|
|
1048
|
-
};
|
|
1049
|
-
return async (request) => {
|
|
1050
|
-
const consented = await answerVerifyChallenge(request);
|
|
1051
|
-
if (consented !== null) {
|
|
1052
|
-
return consented;
|
|
1053
|
-
}
|
|
1054
|
-
const sealed = new URL(request.url).searchParams.get(HASH);
|
|
1055
|
-
if (sealed === null) {
|
|
1056
|
-
return Response.json({ settled: false }, { status: 400 });
|
|
1057
|
-
}
|
|
1058
|
-
const paymentHash = await unsealFor("nwc-verify", config.secret, sealed);
|
|
1059
|
-
if (paymentHash === null) {
|
|
1060
|
-
return Response.json({ settled: false }, { status: 403 });
|
|
1061
|
-
}
|
|
1062
|
-
const preimage = await nwcSettlement(config.connection, paymentHash, config.askTimeoutMs).catch(
|
|
1063
|
-
() => void 0
|
|
1064
|
-
);
|
|
1065
|
-
if (preimage === void 0) {
|
|
1066
|
-
return Response.json({ settled: false }, { status: 502 });
|
|
1067
|
-
}
|
|
1068
|
-
return Response.json({ settled: preimage !== null, preimage }, { headers: paced });
|
|
1069
|
-
};
|
|
1070
|
-
}
|
|
1071
699
|
async function nwcVerifyUrl(endpoint, paymentHash, secret) {
|
|
1072
700
|
const sealed = new URL(endpoint);
|
|
1073
701
|
sealed.searchParams.set(HASH, await sealFor("nwc-verify", secret, paymentHash));
|
|
@@ -1117,7 +745,7 @@ async function overFirstRelayThatAnswers(connection, request, timeoutMs) {
|
|
|
1117
745
|
throw new WalletRefused("unreachable", `no nwc relay answered: ${String(lastFailure)}`);
|
|
1118
746
|
}
|
|
1119
747
|
function overRelay(relay, connection, request, deadline) {
|
|
1120
|
-
return new Promise((
|
|
748
|
+
return new Promise((resolve, reject) => {
|
|
1121
749
|
const socket = new WebSocket(relay);
|
|
1122
750
|
const subscription = request.id.slice(0, 16);
|
|
1123
751
|
const expired = () => {
|
|
@@ -1161,7 +789,7 @@ function overRelay(relay, connection, request, deadline) {
|
|
|
1161
789
|
return;
|
|
1162
790
|
}
|
|
1163
791
|
close();
|
|
1164
|
-
|
|
792
|
+
resolve(answer);
|
|
1165
793
|
});
|
|
1166
794
|
});
|
|
1167
795
|
}
|
|
@@ -1207,52 +835,12 @@ function isSecureRelay(relay) {
|
|
|
1207
835
|
return false;
|
|
1208
836
|
}
|
|
1209
837
|
}
|
|
1210
|
-
function nwcRail(gateway, config) {
|
|
1211
|
-
return async (order) => {
|
|
1212
|
-
const invoice = await nwcInvoice(
|
|
1213
|
-
config.connection,
|
|
1214
|
-
await msatForOrder(order, config.amount, config.rate),
|
|
1215
|
-
config.description?.(order) ?? order.reference
|
|
1216
|
-
);
|
|
1217
|
-
const watched = await gateway.watch({
|
|
1218
|
-
paymentHash: invoice.paymentHash,
|
|
1219
|
-
verifyUrl: await nwcVerifyUrl(
|
|
1220
|
-
config.verifyThrough.endpoint,
|
|
1221
|
-
invoice.paymentHash,
|
|
1222
|
-
config.verifyThrough.secret
|
|
1223
|
-
),
|
|
1224
|
-
expiresAt: invoice.expiresAt,
|
|
1225
|
-
trigger: config.trigger,
|
|
1226
|
-
replay: config.replay,
|
|
1227
|
-
sealed: config.sealed ? await seal(
|
|
1228
|
-
config.sealed.secret,
|
|
1229
|
-
JSON.stringify(config.sealed.data(order)),
|
|
1230
|
-
invoice.paymentHash
|
|
1231
|
-
) : void 0,
|
|
1232
|
-
webhookUrl: config.webhookUrl
|
|
1233
|
-
});
|
|
1234
|
-
return {
|
|
1235
|
-
id: watched.id,
|
|
1236
|
-
paymentHash: invoice.paymentHash,
|
|
1237
|
-
rail: config.name ?? "lightning",
|
|
1238
|
-
scan: invoice.bolt11,
|
|
1239
|
-
qr: toLightningUri(invoice.bolt11),
|
|
1240
|
-
expiresAt: invoice.expiresAt
|
|
1241
|
-
};
|
|
1242
|
-
};
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
// src/entry/nwc.ts
|
|
1246
|
-
var nwcRail2 = nwcRail;
|
|
1247
|
-
var nwcVerifyEndpoint2 = nwcVerifyEndpoint;
|
|
1248
838
|
export {
|
|
1249
839
|
askWallet,
|
|
1250
840
|
nwcConnection,
|
|
1251
841
|
nwcHoldInvoice,
|
|
1252
842
|
nwcInvoice,
|
|
1253
843
|
nwcPay,
|
|
1254
|
-
nwcRail2 as nwcRail,
|
|
1255
844
|
nwcSettlement,
|
|
1256
|
-
nwcVerifyEndpoint2 as nwcVerifyEndpoint,
|
|
1257
845
|
nwcVerifyUrl
|
|
1258
846
|
};
|
package/openapi.yaml
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thunder-bridge",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Take Bitcoin Lightning and bank payments in JavaScript, paid straight into your own wallet or account. No node to run, no custodian, and every payment proven rather than taken on the gateway's word.",
|
|
5
5
|
"author": "i-am-fatik",
|
|
6
6
|
"homepage": "https://agora.gripe/en/tools/thunder-bridge",
|
|
7
7
|
"repository": {
|
|
@@ -111,6 +111,11 @@
|
|
|
111
111
|
"bitcoin",
|
|
112
112
|
"lnurl",
|
|
113
113
|
"lud-21",
|
|
114
|
+
"lightning-address",
|
|
115
|
+
"nwc",
|
|
116
|
+
"bank-transfer",
|
|
117
|
+
"non-custodial",
|
|
118
|
+
"payment-gateway",
|
|
114
119
|
"trustless",
|
|
115
120
|
"payments",
|
|
116
121
|
"websocket"
|
|
@@ -120,6 +125,7 @@
|
|
|
120
125
|
"@noble/ciphers": "2.4.0",
|
|
121
126
|
"@noble/curves": "2.4.0",
|
|
122
127
|
"@noble/hashes": "2.4.0",
|
|
128
|
+
"structured-headers": "2.1.0",
|
|
123
129
|
"uqr": "^0.1.3"
|
|
124
130
|
},
|
|
125
131
|
"overrides": {
|