UNPKG

@getalby/lightning-tools

Version:

Collection of helpful building blocks and tools to develop Bitcoin Lightning web apps

1,247 lines (1,188 loc) 45.9 kB
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; } } export { Invoice, decodeInvoice, fromHexString }; //# sourceMappingURL=bolt11.js.map