@getalby/lightning-tools
Version:
Collection of helpful building blocks and tools to develop Bitcoin Lightning web apps
1,373 lines (1,314 loc) • 64.7 kB
JavaScript
'use strict';
var lib = {};
var hasRequiredLib;
function requireLib () {
if (hasRequiredLib) return lib;
hasRequiredLib = 1;
(function (exports) {
/*! scure-base - MIT License (c) 2022 Paul Miller (paulmillr.com) */
Object.defineProperty(exports, "__esModule", { value: true });
exports.bytes = exports.stringToBytes = exports.str = exports.bytesToString = exports.hex = exports.utf8 = exports.bech32m = exports.bech32 = exports.base58check = exports.base58xmr = exports.base58xrp = exports.base58flickr = exports.base58 = exports.base64url = exports.base64 = exports.base32crockford = exports.base32hex = exports.base32 = exports.base16 = exports.utils = exports.assertNumber = void 0;
function assertNumber(n) {
if (!Number.isSafeInteger(n))
throw new Error(`Wrong integer: ${n}`);
}
exports.assertNumber = assertNumber;
function chain(...args) {
const wrap = (a, b) => (c) => a(b(c));
const encode = Array.from(args)
.reverse()
.reduce((acc, i) => (acc ? wrap(acc, i.encode) : i.encode), undefined);
const decode = args.reduce((acc, i) => (acc ? wrap(acc, i.decode) : i.decode), undefined);
return { encode, decode };
}
function alphabet(alphabet) {
return {
encode: (digits) => {
if (!Array.isArray(digits) || (digits.length && typeof digits[0] !== 'number'))
throw new Error('alphabet.encode input should be an array of numbers');
return digits.map((i) => {
assertNumber(i);
if (i < 0 || i >= alphabet.length)
throw new Error(`Digit index outside alphabet: ${i} (alphabet: ${alphabet.length})`);
return alphabet[i];
});
},
decode: (input) => {
if (!Array.isArray(input) || (input.length && typeof input[0] !== 'string'))
throw new Error('alphabet.decode input should be array of strings');
return input.map((letter) => {
if (typeof letter !== 'string')
throw new Error(`alphabet.decode: not string element=${letter}`);
const index = alphabet.indexOf(letter);
if (index === -1)
throw new Error(`Unknown letter: "${letter}". Allowed: ${alphabet}`);
return index;
});
},
};
}
function join(separator = '') {
if (typeof separator !== 'string')
throw new Error('join separator should be string');
return {
encode: (from) => {
if (!Array.isArray(from) || (from.length && typeof from[0] !== 'string'))
throw new Error('join.encode input should be array of strings');
for (let i of from)
if (typeof i !== 'string')
throw new Error(`join.encode: non-string input=${i}`);
return from.join(separator);
},
decode: (to) => {
if (typeof to !== 'string')
throw new Error('join.decode input should be string');
return to.split(separator);
},
};
}
function padding(bits, chr = '=') {
assertNumber(bits);
if (typeof chr !== 'string')
throw new Error('padding chr should be string');
return {
encode(data) {
if (!Array.isArray(data) || (data.length && typeof data[0] !== 'string'))
throw new Error('padding.encode input should be array of strings');
for (let i of data)
if (typeof i !== 'string')
throw new Error(`padding.encode: non-string input=${i}`);
while ((data.length * bits) % 8)
data.push(chr);
return data;
},
decode(input) {
if (!Array.isArray(input) || (input.length && typeof input[0] !== 'string'))
throw new Error('padding.encode input should be array of strings');
for (let i of input)
if (typeof i !== 'string')
throw new Error(`padding.decode: non-string input=${i}`);
let end = input.length;
if ((end * bits) % 8)
throw new Error('Invalid padding: string should have whole number of bytes');
for (; end > 0 && input[end - 1] === chr; end--) {
if (!(((end - 1) * bits) % 8))
throw new Error('Invalid padding: string has too much padding');
}
return input.slice(0, end);
},
};
}
function normalize(fn) {
if (typeof fn !== 'function')
throw new Error('normalize fn should be function');
return { encode: (from) => from, decode: (to) => fn(to) };
}
function convertRadix(data, from, to) {
if (from < 2)
throw new Error(`convertRadix: wrong from=${from}, base cannot be less than 2`);
if (to < 2)
throw new Error(`convertRadix: wrong to=${to}, base cannot be less than 2`);
if (!Array.isArray(data))
throw new Error('convertRadix: data should be array');
if (!data.length)
return [];
let pos = 0;
const res = [];
const digits = Array.from(data);
digits.forEach((d) => {
assertNumber(d);
if (d < 0 || d >= from)
throw new Error(`Wrong integer: ${d}`);
});
while (true) {
let carry = 0;
let done = true;
for (let i = pos; i < digits.length; i++) {
const digit = digits[i];
const digitBase = from * carry + digit;
if (!Number.isSafeInteger(digitBase) ||
(from * carry) / from !== carry ||
digitBase - digit !== from * carry) {
throw new Error('convertRadix: carry overflow');
}
carry = digitBase % to;
digits[i] = Math.floor(digitBase / to);
if (!Number.isSafeInteger(digits[i]) || digits[i] * to + carry !== digitBase)
throw new Error('convertRadix: carry overflow');
if (!done)
continue;
else if (!digits[i])
pos = i;
else
done = false;
}
res.push(carry);
if (done)
break;
}
for (let i = 0; i < data.length - 1 && data[i] === 0; i++)
res.push(0);
return res.reverse();
}
const gcd = (a, b) => (!b ? a : gcd(b, a % b));
const radix2carry = (from, to) => from + (to - gcd(from, to));
function convertRadix2(data, from, to, padding) {
if (!Array.isArray(data))
throw new Error('convertRadix2: data should be array');
if (from <= 0 || from > 32)
throw new Error(`convertRadix2: wrong from=${from}`);
if (to <= 0 || to > 32)
throw new Error(`convertRadix2: wrong to=${to}`);
if (radix2carry(from, to) > 32) {
throw new Error(`convertRadix2: carry overflow from=${from} to=${to} carryBits=${radix2carry(from, to)}`);
}
let carry = 0;
let pos = 0;
const mask = 2 ** to - 1;
const res = [];
for (const n of data) {
assertNumber(n);
if (n >= 2 ** from)
throw new Error(`convertRadix2: invalid data word=${n} from=${from}`);
carry = (carry << from) | n;
if (pos + from > 32)
throw new Error(`convertRadix2: carry overflow pos=${pos} from=${from}`);
pos += from;
for (; pos >= to; pos -= to)
res.push(((carry >> (pos - to)) & mask) >>> 0);
carry &= 2 ** pos - 1;
}
carry = (carry << (to - pos)) & mask;
if (!padding && pos >= from)
throw new Error('Excess padding');
if (!padding && carry)
throw new Error(`Non-zero padding: ${carry}`);
if (padding && pos > 0)
res.push(carry >>> 0);
return res;
}
function radix(num) {
assertNumber(num);
return {
encode: (bytes) => {
if (!(bytes instanceof Uint8Array))
throw new Error('radix.encode input should be Uint8Array');
return convertRadix(Array.from(bytes), 2 ** 8, num);
},
decode: (digits) => {
if (!Array.isArray(digits) || (digits.length && typeof digits[0] !== 'number'))
throw new Error('radix.decode input should be array of strings');
return Uint8Array.from(convertRadix(digits, num, 2 ** 8));
},
};
}
function radix2(bits, revPadding = false) {
assertNumber(bits);
if (bits <= 0 || bits > 32)
throw new Error('radix2: bits should be in (0..32]');
if (radix2carry(8, bits) > 32 || radix2carry(bits, 8) > 32)
throw new Error('radix2: carry overflow');
return {
encode: (bytes) => {
if (!(bytes instanceof Uint8Array))
throw new Error('radix2.encode input should be Uint8Array');
return convertRadix2(Array.from(bytes), 8, bits, !revPadding);
},
decode: (digits) => {
if (!Array.isArray(digits) || (digits.length && typeof digits[0] !== 'number'))
throw new Error('radix2.decode input should be array of strings');
return Uint8Array.from(convertRadix2(digits, bits, 8, revPadding));
},
};
}
function unsafeWrapper(fn) {
if (typeof fn !== 'function')
throw new Error('unsafeWrapper fn should be function');
return function (...args) {
try {
return fn.apply(null, args);
}
catch (e) { }
};
}
function checksum(len, fn) {
assertNumber(len);
if (typeof fn !== 'function')
throw new Error('checksum fn should be function');
return {
encode(data) {
if (!(data instanceof Uint8Array))
throw new Error('checksum.encode: input should be Uint8Array');
const checksum = fn(data).slice(0, len);
const res = new Uint8Array(data.length + len);
res.set(data);
res.set(checksum, data.length);
return res;
},
decode(data) {
if (!(data instanceof Uint8Array))
throw new Error('checksum.decode: input should be Uint8Array');
const payload = data.slice(0, -len);
const newChecksum = fn(payload).slice(0, len);
const oldChecksum = data.slice(-len);
for (let i = 0; i < len; i++)
if (newChecksum[i] !== oldChecksum[i])
throw new Error('Invalid checksum');
return payload;
},
};
}
exports.utils = { alphabet, chain, checksum, radix, radix2, join, padding };
exports.base16 = chain(radix2(4), alphabet('0123456789ABCDEF'), join(''));
exports.base32 = chain(radix2(5), alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'), padding(5), join(''));
exports.base32hex = chain(radix2(5), alphabet('0123456789ABCDEFGHIJKLMNOPQRSTUV'), padding(5), join(''));
exports.base32crockford = chain(radix2(5), alphabet('0123456789ABCDEFGHJKMNPQRSTVWXYZ'), join(''), normalize((s) => s.toUpperCase().replace(/O/g, '0').replace(/[IL]/g, '1')));
exports.base64 = chain(radix2(6), alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'), padding(6), join(''));
exports.base64url = chain(radix2(6), alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'), padding(6), join(''));
const genBase58 = (abc) => chain(radix(58), alphabet(abc), join(''));
exports.base58 = genBase58('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz');
exports.base58flickr = genBase58('123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ');
exports.base58xrp = genBase58('rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz');
const XMR_BLOCK_LEN = [0, 2, 3, 5, 6, 7, 9, 10, 11];
exports.base58xmr = {
encode(data) {
let res = '';
for (let i = 0; i < data.length; i += 8) {
const block = data.subarray(i, i + 8);
res += exports.base58.encode(block).padStart(XMR_BLOCK_LEN[block.length], '1');
}
return res;
},
decode(str) {
let res = [];
for (let i = 0; i < str.length; i += 11) {
const slice = str.slice(i, i + 11);
const blockLen = XMR_BLOCK_LEN.indexOf(slice.length);
const block = exports.base58.decode(slice);
for (let j = 0; j < block.length - blockLen; j++) {
if (block[j] !== 0)
throw new Error('base58xmr: wrong padding');
}
res = res.concat(Array.from(block.slice(block.length - blockLen)));
}
return Uint8Array.from(res);
},
};
const base58check = (sha256) => chain(checksum(4, (data) => sha256(sha256(data))), exports.base58);
exports.base58check = base58check;
const BECH_ALPHABET = chain(alphabet('qpzry9x8gf2tvdw0s3jn54khce6mua7l'), join(''));
const POLYMOD_GENERATORS = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
function bech32Polymod(pre) {
const b = pre >> 25;
let chk = (pre & 0x1ffffff) << 5;
for (let i = 0; i < POLYMOD_GENERATORS.length; i++) {
if (((b >> i) & 1) === 1)
chk ^= POLYMOD_GENERATORS[i];
}
return chk;
}
function bechChecksum(prefix, words, encodingConst = 1) {
const len = prefix.length;
let chk = 1;
for (let i = 0; i < len; i++) {
const c = prefix.charCodeAt(i);
if (c < 33 || c > 126)
throw new Error(`Invalid prefix (${prefix})`);
chk = bech32Polymod(chk) ^ (c >> 5);
}
chk = bech32Polymod(chk);
for (let i = 0; i < len; i++)
chk = bech32Polymod(chk) ^ (prefix.charCodeAt(i) & 0x1f);
for (let v of words)
chk = bech32Polymod(chk) ^ v;
for (let i = 0; i < 6; i++)
chk = bech32Polymod(chk);
chk ^= encodingConst;
return BECH_ALPHABET.encode(convertRadix2([chk % 2 ** 30], 30, 5, false));
}
function genBech32(encoding) {
const ENCODING_CONST = encoding === 'bech32' ? 1 : 0x2bc830a3;
const _words = radix2(5);
const fromWords = _words.decode;
const toWords = _words.encode;
const fromWordsUnsafe = unsafeWrapper(fromWords);
function encode(prefix, words, limit = 90) {
if (typeof prefix !== 'string')
throw new Error(`bech32.encode prefix should be string, not ${typeof prefix}`);
if (!Array.isArray(words) || (words.length && typeof words[0] !== 'number'))
throw new Error(`bech32.encode words should be array of numbers, not ${typeof words}`);
const actualLength = prefix.length + 7 + words.length;
if (limit !== false && actualLength > limit)
throw new TypeError(`Length ${actualLength} exceeds limit ${limit}`);
prefix = prefix.toLowerCase();
return `${prefix}1${BECH_ALPHABET.encode(words)}${bechChecksum(prefix, words, ENCODING_CONST)}`;
}
function decode(str, limit = 90) {
if (typeof str !== 'string')
throw new Error(`bech32.decode input should be string, not ${typeof str}`);
if (str.length < 8 || (limit !== false && str.length > limit))
throw new TypeError(`Wrong string length: ${str.length} (${str}). Expected (8..${limit})`);
const lowered = str.toLowerCase();
if (str !== lowered && str !== str.toUpperCase())
throw new Error(`String must be lowercase or uppercase`);
str = lowered;
const sepIndex = str.lastIndexOf('1');
if (sepIndex === 0 || sepIndex === -1)
throw new Error(`Letter "1" must be present between prefix and data only`);
const prefix = str.slice(0, sepIndex);
const _words = str.slice(sepIndex + 1);
if (_words.length < 6)
throw new Error('Data must be at least 6 characters long');
const words = BECH_ALPHABET.decode(_words).slice(0, -6);
const sum = bechChecksum(prefix, words, ENCODING_CONST);
if (!_words.endsWith(sum))
throw new Error(`Invalid checksum in ${str}: expected "${sum}"`);
return { prefix, words };
}
const decodeUnsafe = unsafeWrapper(decode);
function decodeToBytes(str) {
const { prefix, words } = decode(str, false);
return { prefix, words, bytes: fromWords(words) };
}
return { encode, decode, decodeToBytes, decodeUnsafe, fromWords, fromWordsUnsafe, toWords };
}
exports.bech32 = genBech32('bech32');
exports.bech32m = genBech32('bech32m');
exports.utf8 = {
encode: (data) => new TextDecoder().decode(data),
decode: (str) => new TextEncoder().encode(str),
};
exports.hex = chain(radix2(4), alphabet('0123456789abcdef'), join(''), normalize((s) => {
if (typeof s !== 'string' || s.length % 2)
throw new TypeError(`hex.decode: expected string, got ${typeof s} with length ${s.length}`);
return s.toLowerCase();
}));
const CODERS = {
utf8: exports.utf8, hex: exports.hex, base16: exports.base16, base32: exports.base32, base64: exports.base64, base64url: exports.base64url, base58: exports.base58, base58xmr: exports.base58xmr
};
const coderTypeError = `Invalid encoding type. Available types: ${Object.keys(CODERS).join(', ')}`;
const bytesToString = (type, bytes) => {
if (typeof type !== 'string' || !CODERS.hasOwnProperty(type))
throw new TypeError(coderTypeError);
if (!(bytes instanceof Uint8Array))
throw new TypeError('bytesToString() expects Uint8Array');
return CODERS[type].encode(bytes);
};
exports.bytesToString = bytesToString;
exports.str = exports.bytesToString;
const stringToBytes = (type, str) => {
if (!CODERS.hasOwnProperty(type))
throw new TypeError(coderTypeError);
if (typeof str !== 'string')
throw new TypeError('stringToBytes() expects string');
return CODERS[type].decode(str);
};
exports.stringToBytes = stringToBytes;
exports.bytes = exports.stringToBytes;
} (lib));
return lib;
}
var bolt11;
var hasRequiredBolt11;
function requireBolt11 () {
if (hasRequiredBolt11) return bolt11;
hasRequiredBolt11 = 1;
const {bech32, hex, utf8} = requireLib();
// defaults for encode; default timestamp is current time at call
const DEFAULTNETWORK = {
// default network is bitcoin
bech32: 'bc',
pubKeyHash: 0x00,
scriptHash: 0x05,
validWitnessVersions: [0]
};
const TESTNETWORK = {
bech32: 'tb',
pubKeyHash: 0x6f,
scriptHash: 0xc4,
validWitnessVersions: [0]
};
const SIGNETNETWORK = {
bech32: 'tbs',
pubKeyHash: 0x6f,
scriptHash: 0xc4,
validWitnessVersions: [0]
};
const REGTESTNETWORK = {
bech32: 'bcrt',
pubKeyHash: 0x6f,
scriptHash: 0xc4,
validWitnessVersions: [0]
};
const SIMNETWORK = {
bech32: 'sb',
pubKeyHash: 0x3f,
scriptHash: 0x7b,
validWitnessVersions: [0]
};
const FEATUREBIT_ORDER = [
'option_data_loss_protect',
'initial_routing_sync',
'option_upfront_shutdown_script',
'gossip_queries',
'var_onion_optin',
'gossip_queries_ex',
'option_static_remotekey',
'payment_secret',
'basic_mpp',
'option_support_large_channel'
];
const DIVISORS = {
m: BigInt(1e3),
u: BigInt(1e6),
n: BigInt(1e9),
p: BigInt(1e12)
};
const MAX_MILLISATS = BigInt('2100000000000000000');
const MILLISATS_PER_BTC = BigInt(1e11);
const TAGCODES = {
payment_hash: 1,
payment_secret: 16,
description: 13,
payee: 19,
description_hash: 23, // commit to longer descriptions (used by lnurl-pay)
expiry: 6, // default: 3600 (1 hour)
min_final_cltv_expiry: 24, // default: 9
fallback_address: 9,
route_hint: 3, // for extra routing info (private etc.)
feature_bits: 5,
metadata: 27
};
// reverse the keys and values of TAGCODES and insert into TAGNAMES
const TAGNAMES = {};
for (let i = 0, keys = Object.keys(TAGCODES); i < keys.length; i++) {
const currentName = keys[i];
const currentCode = TAGCODES[keys[i]].toString();
TAGNAMES[currentCode] = currentName;
}
const TAGPARSERS = {
1: words => hex.encode(bech32.fromWordsUnsafe(words)), // 256 bits
16: words => hex.encode(bech32.fromWordsUnsafe(words)), // 256 bits
13: words => utf8.encode(bech32.fromWordsUnsafe(words)), // string variable length
19: words => hex.encode(bech32.fromWordsUnsafe(words)), // 264 bits
23: words => hex.encode(bech32.fromWordsUnsafe(words)), // 256 bits
27: words => hex.encode(bech32.fromWordsUnsafe(words)), // variable
6: wordsToIntBE, // default: 3600 (1 hour)
24: wordsToIntBE, // default: 9
3: routingInfoParser, // for extra routing info (private etc.)
5: featureBitsParser // keep feature bits as array of 5 bit words
};
function getUnknownParser(tagCode) {
return words => ({
tagCode: parseInt(tagCode),
words: bech32.encode('unknown', words, Number.MAX_SAFE_INTEGER)
})
}
function wordsToIntBE(words) {
return words.reverse().reduce((total, item, index) => {
return total + item * Math.pow(32, index)
}, 0)
}
// first convert from words to buffer, trimming padding where necessary
// parse in 51 byte chunks. See encoder for details.
function routingInfoParser(words) {
const routes = [];
let pubkey,
shortChannelId,
feeBaseMSats,
feeProportionalMillionths,
cltvExpiryDelta;
let routesBuffer = bech32.fromWordsUnsafe(words);
while (routesBuffer.length > 0) {
pubkey = hex.encode(routesBuffer.slice(0, 33)); // 33 bytes
shortChannelId = hex.encode(routesBuffer.slice(33, 41)); // 8 bytes
feeBaseMSats = parseInt(hex.encode(routesBuffer.slice(41, 45)), 16); // 4 bytes
feeProportionalMillionths = parseInt(
hex.encode(routesBuffer.slice(45, 49)),
16
); // 4 bytes
cltvExpiryDelta = parseInt(hex.encode(routesBuffer.slice(49, 51)), 16); // 2 bytes
routesBuffer = routesBuffer.slice(51);
routes.push({
pubkey,
short_channel_id: shortChannelId,
fee_base_msat: feeBaseMSats,
fee_proportional_millionths: feeProportionalMillionths,
cltv_expiry_delta: cltvExpiryDelta
});
}
return routes
}
function featureBitsParser(words) {
const bools = words
.slice()
.reverse()
.map(word => [
!!(word & 0b1),
!!(word & 0b10),
!!(word & 0b100),
!!(word & 0b1000),
!!(word & 0b10000)
])
.reduce((finalArr, itemArr) => finalArr.concat(itemArr), []);
while (bools.length < FEATUREBIT_ORDER.length * 2) {
bools.push(false);
}
const featureBits = {};
FEATUREBIT_ORDER.forEach((featureName, index) => {
let status;
if (bools[index * 2]) {
status = 'required';
} else if (bools[index * 2 + 1]) {
status = 'supported';
} else {
status = 'unsupported';
}
featureBits[featureName] = status;
});
const extraBits = bools.slice(FEATUREBIT_ORDER.length * 2);
featureBits.extra_bits = {
start_bit: FEATUREBIT_ORDER.length * 2,
bits: extraBits,
has_required: extraBits.reduce(
(result, bit, index) =>
index % 2 !== 0 ? result || false : result || bit,
false
)
};
return featureBits
}
function hrpToMillisat(hrpString, outputString) {
let divisor, value;
if (hrpString.slice(-1).match(/^[munp]$/)) {
divisor = hrpString.slice(-1);
value = hrpString.slice(0, -1);
} else if (hrpString.slice(-1).match(/^[^munp0-9]$/)) {
throw new Error('Not a valid multiplier for the amount')
} else {
value = hrpString;
}
if (!value.match(/^\d+$/))
throw new Error('Not a valid human readable amount')
const valueBN = BigInt(value);
const millisatoshisBN = divisor
? (valueBN * MILLISATS_PER_BTC) / DIVISORS[divisor]
: valueBN * MILLISATS_PER_BTC;
if (
(divisor === 'p' && !(valueBN % BigInt(10) === BigInt(0))) ||
millisatoshisBN > MAX_MILLISATS
) {
throw new Error('Amount is outside of valid range')
}
return outputString ? millisatoshisBN.toString() : millisatoshisBN
}
// decode will only have extra comments that aren't covered in encode comments.
// also if anything is hard to read I'll comment.
function decode(paymentRequest, network) {
if (typeof paymentRequest !== 'string')
throw new Error('Lightning Payment Request must be string')
if (paymentRequest.slice(0, 2).toLowerCase() !== 'ln')
throw new Error('Not a proper lightning payment request')
const sections = [];
const decoded = bech32.decode(paymentRequest, Number.MAX_SAFE_INTEGER);
paymentRequest = paymentRequest.toLowerCase();
const prefix = decoded.prefix;
let words = decoded.words;
let letters = paymentRequest.slice(prefix.length + 1);
let sigWords = words.slice(-104);
words = words.slice(0, -104);
// Without reverse lookups, can't say that the multipier at the end must
// have a number before it, so instead we parse, and if the second group
// doesn't have anything, there's a good chance the last letter of the
// coin type got captured by the third group, so just re-regex without
// the number.
let prefixMatches = prefix.match(/^ln(\S+?)(\d*)([a-zA-Z]?)$/);
if (prefixMatches && !prefixMatches[2])
prefixMatches = prefix.match(/^ln(\S+)$/);
if (!prefixMatches) {
throw new Error('Not a proper lightning payment request')
}
// "ln" section
sections.push({
name: 'lightning_network',
letters: 'ln'
});
// "bc" section
const bech32Prefix = prefixMatches[1];
let coinNetwork;
if (!network) {
switch (bech32Prefix) {
case DEFAULTNETWORK.bech32:
coinNetwork = DEFAULTNETWORK;
break
case TESTNETWORK.bech32:
coinNetwork = TESTNETWORK;
break
case SIGNETNETWORK.bech32:
coinNetwork = SIGNETNETWORK;
break
case REGTESTNETWORK.bech32:
coinNetwork = REGTESTNETWORK;
break
case SIMNETWORK.bech32:
coinNetwork = SIMNETWORK;
break
}
} else {
if (
network.bech32 === undefined ||
network.pubKeyHash === undefined ||
network.scriptHash === undefined ||
!Array.isArray(network.validWitnessVersions)
)
throw new Error('Invalid network')
coinNetwork = network;
}
if (!coinNetwork || coinNetwork.bech32 !== bech32Prefix) {
throw new Error('Unknown coin bech32 prefix')
}
sections.push({
name: 'coin_network',
letters: bech32Prefix,
value: coinNetwork
});
// amount section
const value = prefixMatches[2];
let millisatoshis;
if (value) {
const divisor = prefixMatches[3];
millisatoshis = hrpToMillisat(value + divisor, true);
sections.push({
name: 'amount',
letters: prefixMatches[2] + prefixMatches[3],
value: millisatoshis
});
} else {
millisatoshis = null;
}
// "1" separator
sections.push({
name: 'separator',
letters: '1'
});
// timestamp
const timestamp = wordsToIntBE(words.slice(0, 7));
words = words.slice(7); // trim off the left 7 words
sections.push({
name: 'timestamp',
letters: letters.slice(0, 7),
value: timestamp
});
letters = letters.slice(7);
let tagName, parser, tagLength, tagWords;
// we have no tag count to go on, so just keep hacking off words
// until we have none.
while (words.length > 0) {
const tagCode = words[0].toString();
tagName = TAGNAMES[tagCode] || 'unknown_tag';
parser = TAGPARSERS[tagCode] || getUnknownParser(tagCode);
words = words.slice(1);
tagLength = wordsToIntBE(words.slice(0, 2));
words = words.slice(2);
tagWords = words.slice(0, tagLength);
words = words.slice(tagLength);
sections.push({
name: tagName,
tag: letters[0],
letters: letters.slice(0, 1 + 2 + tagLength),
value: parser(tagWords) // see: parsers for more comments
});
letters = letters.slice(1 + 2 + tagLength);
}
// signature
sections.push({
name: 'signature',
letters: letters.slice(0, 104),
value: hex.encode(bech32.fromWordsUnsafe(sigWords))
});
letters = letters.slice(104);
// checksum
sections.push({
name: 'checksum',
letters: letters
});
let result = {
paymentRequest,
sections,
get expiry() {
let exp = sections.find(s => s.name === 'expiry');
if (exp) return getValue('timestamp') + exp.value
},
get route_hints() {
return sections.filter(s => s.name === 'route_hint').map(s => s.value)
}
};
for (let name in TAGCODES) {
if (name === 'route_hint') {
// route hints can be multiple, so this won't work for them
continue
}
Object.defineProperty(result, name, {
get() {
return getValue(name)
}
});
}
return result
function getValue(name) {
let section = sections.find(s => s.name === name);
return section ? section.value : undefined
}
}
bolt11 = {
decode,
hrpToMillisat
};
return bolt11;
}
var bolt11Exports = requireBolt11();
// from https://stackoverflow.com/a/50868276
const fromHexString = (hexString) => Uint8Array.from(hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)));
const decodeInvoice = (paymentRequest) => {
if (!paymentRequest)
return null;
try {
const decoded = bolt11Exports.decode(paymentRequest);
if (!decoded || !decoded.sections)
return null;
const hashTag = decoded.sections.find((value) => value.name === "payment_hash");
if (hashTag?.name !== "payment_hash" || !hashTag.value)
return null;
const paymentHash = hashTag.value;
let satoshi = 0;
const amountTag = decoded.sections.find((value) => value.name === "amount");
if (amountTag?.name === "amount" && amountTag.value) {
satoshi = parseInt(amountTag.value) / 1000; // millisats
}
const timestampTag = decoded.sections.find((value) => value.name === "timestamp");
if (timestampTag?.name !== "timestamp" || !timestampTag.value)
return null;
const timestamp = timestampTag.value;
let expiry;
const expiryTag = decoded.sections.find((value) => value.name === "expiry");
if (expiryTag?.name === "expiry") {
expiry = expiryTag.value;
}
const descriptionTag = decoded.sections.find((value) => value.name === "description");
const description = descriptionTag?.name === "description"
? descriptionTag?.value
: undefined;
return {
paymentHash,
satoshi,
timestamp,
expiry,
description,
};
}
catch {
return null;
}
};
function bytes(b, ...lengths) {
if (!(b instanceof Uint8Array))
throw new Error('Expected Uint8Array');
if (lengths.length > 0 && !lengths.includes(b.length))
throw new Error(`Expected Uint8Array of length ${lengths}, not of length=${b.length}`);
}
function exists(instance, checkFinished = true) {
if (instance.destroyed)
throw new Error('Hash instance has been destroyed');
if (checkFinished && instance.finished)
throw new Error('Hash#digest() has already been called');
}
function output(out, instance) {
bytes(out);
const min = instance.outputLen;
if (out.length < min) {
throw new Error(`digestInto() expects output buffer of length at least ${min}`);
}
}
/*! noble-hashes - MIT License (c) 2022 Paul Miller (paulmillr.com) */
// We use WebCrypto aka globalThis.crypto, which exists in browsers and node.js 16+.
// node.js versions earlier than v19 don't declare it in global scope.
// For node.js, package.json#exports field mapping rewrites import
// from `crypto` to `cryptoNode`, which imports native module.
// Makes the utils un-importable in browsers without a bundler.
// Once node.js 18 is deprecated, we can just drop the import.
const u8a = (a) => a instanceof Uint8Array;
// Cast array to view
const createView = (arr) => new DataView(arr.buffer, arr.byteOffset, arr.byteLength);
// The rotate right (circular right shift) operation for uint32
const rotr = (word, shift) => (word << (32 - shift)) | (word >>> shift);
// big-endian hardware is rare. Just in case someone still decides to run hashes:
// early-throw an error because we don't support BE yet.
const isLE = new Uint8Array(new Uint32Array([0x11223344]).buffer)[0] === 0x44;
if (!isLE)
throw new Error('Non little-endian hardware is not supported');
const hexes = /* @__PURE__ */ Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, '0'));
/**
* @example bytesToHex(Uint8Array.from([0xca, 0xfe, 0x01, 0x23])) // 'cafe0123'
*/
function bytesToHex(bytes) {
if (!u8a(bytes))
throw new Error('Uint8Array expected');
// pre-caching improves the speed 6x
let hex = '';
for (let i = 0; i < bytes.length; i++) {
hex += hexes[bytes[i]];
}
return hex;
}
/**
* @example utf8ToBytes('abc') // new Uint8Array([97, 98, 99])
*/
function utf8ToBytes(str) {
if (typeof str !== 'string')
throw new Error(`utf8ToBytes expected string, got ${typeof str}`);
return new Uint8Array(new TextEncoder().encode(str)); // https://bugzil.la/1681809
}
/**
* Normalizes (non-hex) string or Uint8Array to Uint8Array.
* Warning: when Uint8Array is passed, it would NOT get copied.
* Keep in mind for future mutable operations.
*/
function toBytes(data) {
if (typeof data === 'string')
data = utf8ToBytes(data);
if (!u8a(data))
throw new Error(`expected Uint8Array, got ${typeof data}`);
return data;
}
// For runtime check if class implements interface
class Hash {
// Safe version that clones internal state
clone() {
return this._cloneInto();
}
}
function wrapConstructor(hashCons) {
const hashC = (msg) => hashCons().update(toBytes(msg)).digest();
const tmp = hashCons();
hashC.outputLen = tmp.outputLen;
hashC.blockLen = tmp.blockLen;
hashC.create = () => hashCons();
return hashC;
}
// Polyfill for Safari 14
function setBigUint64(view, byteOffset, value, isLE) {
if (typeof view.setBigUint64 === 'function')
return view.setBigUint64(byteOffset, value, isLE);
const _32n = BigInt(32);
const _u32_max = BigInt(0xffffffff);
const wh = Number((value >> _32n) & _u32_max);
const wl = Number(value & _u32_max);
const h = isLE ? 4 : 0;
const l = isLE ? 0 : 4;
view.setUint32(byteOffset + h, wh, isLE);
view.setUint32(byteOffset + l, wl, isLE);
}
// Base SHA2 class (RFC 6234)
class SHA2 extends Hash {
constructor(blockLen, outputLen, padOffset, isLE) {
super();
this.blockLen = blockLen;
this.outputLen = outputLen;
this.padOffset = padOffset;
this.isLE = isLE;
this.finished = false;
this.length = 0;
this.pos = 0;
this.destroyed = false;
this.buffer = new Uint8Array(blockLen);
this.view = createView(this.buffer);
}
update(data) {
exists(this);
const { view, buffer, blockLen } = this;
data = toBytes(data);
const len = data.length;
for (let pos = 0; pos < len;) {
const take = Math.min(blockLen - this.pos, len - pos);
// Fast path: we have at least one block in input, cast it to view and process
if (take === blockLen) {
const dataView = createView(data);
for (; blockLen <= len - pos; pos += blockLen)
this.process(dataView, pos);
continue;
}
buffer.set(data.subarray(pos, pos + take), this.pos);
this.pos += take;
pos += take;
if (this.pos === blockLen) {
this.process(view, 0);
this.pos = 0;
}
}
this.length += data.length;
this.roundClean();
return this;
}
digestInto(out) {
exists(this);
output(out, this);
this.finished = true;
// Padding
// We can avoid allocation of buffer for padding completely if it
// was previously not allocated here. But it won't change performance.
const { buffer, view, blockLen, isLE } = this;
let { pos } = this;
// append the bit '1' to the message
buffer[pos++] = 0b10000000;
this.buffer.subarray(pos).fill(0);
// we have less than padOffset left in buffer, so we cannot put length in current block, need process it and pad again
if (this.padOffset > blockLen - pos) {
this.process(view, 0);
pos = 0;
}
// Pad until full block byte with zeros
for (let i = pos; i < blockLen; i++)
buffer[i] = 0;
// Note: sha512 requires length to be 128bit integer, but length in JS will overflow before that
// You need to write around 2 exabytes (u64_max / 8 / (1024**6)) for this to happen.
// So we just write lowest 64 bits of that value.
setBigUint64(view, blockLen - 8, BigInt(this.length * 8), isLE);
this.process(view, 0);
const oview = createView(out);
const len = this.outputLen;
// NOTE: we do division by 4 later, which should be fused in single op with modulo by JIT
if (len % 4)
throw new Error('_sha2: outputLen should be aligned to 32bit');
const outLen = len / 4;
const state = this.get();
if (outLen > state.length)
throw new Error('_sha2: outputLen bigger than state');
for (let i = 0; i < outLen; i++)
oview.setUint32(4 * i, state[i], isLE);
}
digest() {
const { buffer, outputLen } = this;
this.digestInto(buffer);
const res = buffer.slice(0, outputLen);
this.destroy();
return res;
}
_cloneInto(to) {
to || (to = new this.constructor());
to.set(...this.get());
const { blockLen, buffer, length, finished, destroyed, pos } = this;
to.length = length;
to.pos = pos;
to.finished = finished;
to.destroyed = destroyed;
if (length % blockLen)
to.buffer.set(buffer);
return to;
}
}
// SHA2-256 need to try 2^128 hashes to execute birthday attack.
// BTC network is doing 2^67 hashes/sec as per early 2023.
// Choice: a ? b : c
const Chi = (a, b, c) => (a & b) ^ (~a & c);
// Majority function, true if any two inpust is true
const Maj = (a, b, c) => (a & b) ^ (a & c) ^ (b & c);
// Round constants:
// first 32 bits of the fractional parts of the cube roots of the first 64 primes 2..311)
// prettier-ignore
const SHA256_K = /* @__PURE__ */ new Uint32Array([
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
]);
// Initial state (first 32 bits of the fractional parts of the square roots of the first 8 primes 2..19):
// prettier-ignore
const IV = /* @__PURE__ */ new Uint32Array([
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
]);
// Temporary buffer, not used to store anything between runs
// Named this way because it matches specification.
const SHA256_W = /* @__PURE__ */ new Uint32Array(64);
class SHA256 extends SHA2 {
constructor() {
super(64, 32, 8, false);
// We cannot use array here since array allows indexing by variable
// which means optimizer/compiler cannot use registers.
this.A = IV[0] | 0;
this.B = IV[1] | 0;
this.C = IV[2] | 0;
this.D = IV[3] | 0;
this.E = IV[4] | 0;
this.F = IV[5] | 0;
this.G = IV[6] | 0;
this.H = IV[7] | 0;
}
get() {
const { A, B, C, D, E, F, G, H } = this;
return [A, B, C, D, E, F, G, H];
}
// prettier-ignore
set(A, B, C, D, E, F, G, H) {
this.A = A | 0;
this.B = B | 0;
this.C = C | 0;
this.D = D | 0;
this.E = E | 0;
this.F = F | 0;
this.G = G | 0;
this.H = H | 0;
}
process(view, offset) {
// Extend the first 16 words into the remaining 48 words w[16..63] of the message schedule array
for (let i = 0; i < 16; i++, offset += 4)
SHA256_W[i] = view.getUint32(offset, false);
for (let i = 16; i < 64; i++) {
const W15 = SHA256_W[i - 15];
const W2 = SHA256_W[i - 2];
const s0 = rotr(W15, 7) ^ rotr(W15, 18) ^ (W15 >>> 3);
const s1 = rotr(W2, 17) ^ rotr(W2, 19) ^ (W2 >>> 10);
SHA256_W[i] = (s1 + SHA256_W[i - 7] + s0 + SHA256_W[i - 16]) | 0;
}
// Compression function main loop, 64 rounds
let { A, B, C, D, E, F, G, H } = this;
for (let i = 0; i < 64; i++) {
const sigma1 = rotr(E, 6) ^ rotr(E, 11) ^ rotr(E, 25);
const T1 = (H + sigma1 + Chi(E, F, G) + SHA256_K[i] + SHA256_W[i]) | 0;
const sigma0 = rotr(A, 2) ^ rotr(A, 13) ^ rotr(A, 22);
const T2 = (sigma0 + Maj(A, B, C)) | 0;
H = G;
G = F;
F = E;
E = (D + T1) | 0;
D = C;
C = B;
B = A;
A = (T1 + T2) | 0;
}
// Add the compressed chunk to the current hash value
A = (A + this.A) | 0;
B = (B + this.B) | 0;
C = (C + this.C) | 0;
D = (D + this.D) | 0;
E = (E + this.E) | 0;
F = (F + this.F) | 0;
G = (G + this.G) | 0;
H = (H + this.H) | 0;
this.set(A, B, C, D, E, F, G, H);
}
roundClean() {
SHA256_W.fill(0);
}
destroy() {
this.set(0, 0, 0, 0, 0, 0, 0, 0);
this.buffer.fill(0);
}
}
/**
* SHA2-256 hash function
* @param message - data that would be hashed
*/
const sha256 = /* @__PURE__ */ wrapConstructor(() => new SHA256());
class Invoice {
constructor(args) {
this.paymentRequest = args.pr;
if (!this.paymentRequest) {
throw new Error("Invalid payment request");
}
const decodedInvoice = decodeInvoice(this.paymentRequest);
if (!decodedInvoice) {
throw new Error("Failed to decode payment request");
}
this.paymentHash = decodedInvoice.paymentHash;
this.satoshi = decodedInvoice.satoshi;
this.timestamp = decodedInvoice.timestamp;
this.expiry = decodedInvoice.expiry;
this.createdDate = new Date(this.timestamp * 1000);
this.expiryDate = this.expiry
? new Date((this.timestamp + this.expiry) * 1000)
: undefined;
this.description = decodedInvoice.description ?? null;
this.verify = args.verify ?? null;
this.preimage = args.preimage ?? null;
this.successAction = args.successAction ?? null;
}
async isPaid() {
if (this.preimage)
return this.validatePreimage(this.preimage);
else if (this.verify) {
return await this.verifyPayment();
}
else {
throw new Error("Could not verify payment");
}
}
validatePreimage(preimage) {
if (!preimage || !this.paymentHash)
return false;
try {
const preimageHash = bytesToHex(sha256(fromHexString(preimage)));
return this.paymentHash === preimageHash;
}
catch {
return false;
}
}
async verifyPayment() {
try {
if (!this.verify) {
throw new Error("LNURL verify not available");
}
const response = await fetch(this.verify);
if (!response.ok) {
throw new Error(`Verification request failed: ${response.status} ${response.statusText}`);
}
const json = await response.json();
if (json.preimage) {
this.preimage = json.preimage;
}
return json.settled;
}
catch (error) {
console.error("Failed to check LNURL-verify", error);
return false;
}
}
hasExpired() {
const { expiryDate } = this;
if (expiryDate) {
return expiryDate.getTime() < Date.now();
}
return false;
}
}
const TAG_KEYSEND = "keysend";
const parseKeysendResponse = (data) => {
if (data.tag !== TAG_KEYSEND)
throw new Error("Invalid keysend params");
if (data.status !== "OK")
throw new Error("Keysend status not OK");
if (!data.pubkey)
throw new Error("Pubkey does not exist");
const destination = data.pubkey;
let customKey, customValue;
if (data.customData && data.customData[0]) {
customKey = data.customData[0].customKey;
customValue = data.customData[0].customValue;
}
return {
destination,
customKey,
customValue,
};
};
async function generateZapEvent({ satoshi, comment, p, e, relays }, options = {}) {
const nostr = options.nostr || globalThis.nostr;
if (!nostr) {
throw new Error("nostr option or window.nostr is not available");
}
const nostrTags = [
["relays", ...relays],
["amount", satoshi.toString()],
];
if (p) {
nostrTags.push(["p", p]);
}
if (e) {
nostrTags.push(["e", e]);
}
const pubkey = await nostr.getPublicKey();
const nostrEvent = {
pubkey,
created_at: Math.floor(Date.now() / 1000),
kind: 9734,
tags: nostrTags,
content: comment ?? "",
};
nostrEvent.id = getEventHash(nostrEvent);
return await nostr.signEvent(nostrEvent);
}
function validateEvent(event) {
if (typeof event.content !== "string")
return false;
if (typeof event.created_at !== "number")
return false;
// ignore these checks because if the pubkey is not set we add it to the event. same for the ID.
// if (typeof event.pubkey !== "string") return false;
// if (!event.pubkey.match(/^[a-f0-9]{64}$/)) return false;
if (!Array.isArray(event.tags))
return false;
for (let i = 0; i < event.tags.length; i++) {
const tag = event.tags[i];
if (!Array.isArray(tag))
return false;
for (let j = 0; j < tag.length; j++) {
if (typeof tag[j] === "object")
return false;
}
}
return true;
}
function serializeEvent(evt) {
if (!validateEvent(evt))
throw new Error("can't serialize event with wrong or missing properties");
return JSON.stringify([
0,
evt.pubkey,
evt.created_at,
evt.kind,
evt.tags,
evt.content,
]);
}
function getEventHash(event) {
return bytesToHex(sha256(serializeEvent(event)));
}
function parseNostrResponse(nostrData, username) {
let nostrPubkey;
let nostrRelays;
if (username && nostrData) {
nostrPubkey = nostrData.names?.[username];
nostrRelays = nostrPubkey ? nostrData.relays?.[nostrPubkey] : undefined;
}
return [nostrData, nostrPubkey, nostrRelays];
}
const URL_REGEX = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[\w]*))?)/;
const isUrl = (url) => {
if (!url)
return false;
return URL_REGEX.test(url);
};
const isValidAmount = ({ amount, min, max, }) => {
return amount > 0 && amount >= min && amount <= max;
};
const TAG_PAY_REQUEST = "payRequest";
// From: https://github.com/dolcalmi/lnurl-pay/blob/main/src/request-pay-service-params.ts
const parseLnUrlPayResponse = async (data) => {
if (data.tag !== TAG_PAY_REQUEST)
throw new Error("Invalid pay service params");
const callback = (data.callback + "").trim();
if (!isUrl(callback))
throw new Error("Callback must be a valid url");
const min = Math.ceil(Number(data.minSendable || 0));
const max = Math.floor(Number(data.maxSendable));
if (!(min && max) || min > max)
throw new Error("Invalid pay service params");
let metadata;
let metadataHash;
try {
metadata = JSON.parse(data.metadata + "");
metadataHash = bytesToHex(sha256(data.metadata + ""));
}
catch {
metadata = [];
metadataHash = bytesToHex(sha256("[]"));
}
let email = "";
let image = "";
let description = "";
let identifier = "";
for (let i = 0; i < metadata.length; i++)