UNPKG

lnurl-pay

Version:

Client library for lnurl-pay and lightning address

465 lines (459 loc) 15.7 kB
//#region rolldown:runtime var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __commonJS = (cb, mod) => function() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { key = keys[i]; if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); //#endregion const bech32 = __toESM(require("bech32")); const axios = __toESM(require("axios")); const aes_js = __toESM(require("aes-js")); const base64_js = __toESM(require("base64-js")); const bolt11 = __toESM(require("bolt11")); const __aws_crypto_sha256_js = __toESM(require("@aws-crypto/sha256-js")); //#region node_modules/@oxc-project/runtime/src/helpers/asyncToGenerator.js var require_asyncToGenerator = /* @__PURE__ */ __commonJS({ "node_modules/@oxc-project/runtime/src/helpers/asyncToGenerator.js": ((exports, module) => { function asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.value; } catch (n$1) { return void e(n$1); } i.done ? t(u) : Promise.resolve(u).then(r, o); } function _asyncToGenerator$3(n) { return function() { var t = this, e = arguments; return new Promise(function(r, o) { var a = n.apply(t, e); function _next(n$1) { asyncGeneratorStep(a, r, o, _next, _throw, "next", n$1); } function _throw(n$1) { asyncGeneratorStep(a, r, o, _next, _throw, "throw", n$1); } _next(void 0); }); }; } module.exports = _asyncToGenerator$3, module.exports.__esModule = true, module.exports["default"] = module.exports; }) }); //#endregion //#region src/utils.ts var utils_exports = {}; __export(utils_exports, { checkedToSats: () => checkedToSats, decipherAES: () => decipherAES, decodeInvoice: () => decodeInvoice, decodeUrlOrAddress: () => decodeUrlOrAddress, getHashFromInvoice: () => getHashFromInvoice, getJson: () => getJson, isLightningAddress: () => isLightningAddress, isLnurl: () => isLnurl, isLnurlp: () => isLnurlp, isOnionUrl: () => isOnionUrl, isUrl: () => isUrl, isValidAmount: () => isValidAmount, isValidPreimage: () => isValidPreimage, parseLightningAddress: () => parseLightningAddress, parseLnUrl: () => parseLnUrl, parseLnurlp: () => parseLnurlp, sha256: () => sha256, toSats: () => toSats }); var import_asyncToGenerator$2 = /* @__PURE__ */ __toESM(require_asyncToGenerator()); const PROTOCOL_AND_DOMAIN = new RegExp("^(?:\\w+:)?\\/\\/(\\S+)$", ""); const LOCALHOST_DOMAIN = /^localhost[:?\d]*(?:[^:?\d]\S*)?$/; const NON_LOCALHOST_DOMAIN = /^[^\s.]+\.\S{2,}$/; const LNURL_REGEX = new RegExp("^(?:http.*[&?]lightning=|lightning:)?(lnurl[0-9]{1,}[02-9ac-hj-np-z]+)", ""); const LN_ADDRESS_REGEX = new RegExp("^((?:[^<>()[\\]\\\\.,;:\\s@\"]+(?:\\.[^<>()[\\]\\\\.,;:\\s@\"]+)*)|(?:\".+\"))@((?:\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}])|(?:(?:[a-zA-Z-0-9]+\\.)+[a-zA-Z]{2,}))$", ""); const ONION_REGEX = new RegExp("^(http:\\/\\/[^/:@]+\\.onion(?::\\d{1,5})?)(\\/.*)?$", ""); const LNURLP_REGEX = new RegExp("^lnurlp:\\/\\/([\\w-]+\\.)+[\\w-]+(:\\d{1,5})?(\\/[\\w-./?%&=]*)?$", ""); /** * Decode a bech32 encoded url (lnurl), lightning address or lnurlp url and return a url * @method decodeUrlOrAddress * @param lnUrlOrAddress string to decode * @return plain url or null if is an invalid url, lightning address or lnurlp */ const decodeUrlOrAddress = (lnUrlOrAddress) => { const bech32Url = parseLnUrl(lnUrlOrAddress); if (bech32Url) { const decoded = bech32.bech32.decode(bech32Url, 2e4); return Buffer.from(bech32.bech32.fromWords(decoded.words)).toString(); } const address = parseLightningAddress(lnUrlOrAddress); if (address) { const { username, domain } = address; const protocol = domain.match(/\.onion$/) ? "http" : "https"; return `${protocol}://${domain}/.well-known/lnurlp/${username}`; } return parseLnurlp(lnUrlOrAddress); }; /** * Parse an url and return a bech32 encoded url (lnurl) * @method parseLnUrl * @param url string to parse * @return bech32 encoded url (lnurl) or null if is an invalid url */ const parseLnUrl = (url) => { if (!url) return null; const result = LNURL_REGEX.exec(url.toLowerCase()); return result ? result[1] : null; }; /** * Verify if a string is a valid lnurl value * @method isLnurl * @param url string to validate * @return true if is a valid lnurl value */ const isLnurl = (url) => { if (!url) return false; return LNURL_REGEX.test(url.toLowerCase()); }; /** * Verify if a string is a lightning adress * @method isLightningAddress * @param address string to validate * @return true if is a lightning address */ const isLightningAddress = (address) => { if (!address) return false; return LN_ADDRESS_REGEX.test(address); }; /** * Parse an address and return username and domain * @method parseLightningAddress * @param address string to parse * @return LightningAddress { username, domain } */ const parseLightningAddress = (address) => { if (!address) return null; const result = LN_ADDRESS_REGEX.exec(address); return result ? { username: result[1], domain: result[2] } : null; }; /** * Verify if a string is a lnurlp url * @method isLnurlp * @param url string to validate * @return true if is a lnurlp url */ const isLnurlp = (url) => { if (!url) return false; return LNURLP_REGEX.test(url); }; /** * Parse a lnurlp url and return an url with the proper protocol * @method parseLnurlp * @param url string to parse * @return url (http or https) or null if is an invalid lnurlp */ const parseLnurlp = (url) => { if (!url) return null; const parsedUrl = url.toLowerCase(); if (!LNURLP_REGEX.test(parsedUrl)) return null; const protocol = parsedUrl.includes(".onion") ? "http://" : "https://"; return parsedUrl.replace("lnurlp://", protocol); }; /** * Verify if a string is an url * @method isUrl * @param url string to validate * @return true if is an url */ const isUrl = (url) => { if (!url) return false; if (typeof url !== "string") return false; var match = url.match(PROTOCOL_AND_DOMAIN); if (!match) return false; var everythingAfterProtocol = match[1]; if (!everythingAfterProtocol) return false; if (LOCALHOST_DOMAIN.test(everythingAfterProtocol) || NON_LOCALHOST_DOMAIN.test(everythingAfterProtocol)) return true; return false; }; /** * Verify if a string is an onion url * @method isOnionUrl * @param url string to validate * @return true if is an onion url */ const isOnionUrl = (url) => { return isUrl(url) && ONION_REGEX.test(url.toLowerCase()); }; /** * Parse a number to Satoshis * @method checkedToSats * @param value number to parse * @return Satoshis or null */ const checkedToSats = (value) => { if (value && value >= 0) return toSats(value); return null; }; /** * Cast a number to Satoshis type * @method toSats * @param value number to cast * @return Satoshis */ const toSats = (value) => { return value; }; const isValidAmount = ({ amount, min, max }) => { const isValid = amount > 0 && amount >= min && amount <= max; const isFixed = min === max; return isValid && isFixed ? amount === min : isValid; }; const getJson = function() { var _ref = (0, import_asyncToGenerator$2.default)(function* ({ url, params }) { return axios.default.get(url, { params }).then((response) => { if (response.data.status === "ERROR") throw new Error(response.data.reason + ""); return response.data; }); }); return function getJson$1(_x) { return _ref.apply(this, arguments); }; }(); const sha256 = (data, encoding = "hex") => { const sha256$1 = new __aws_crypto_sha256_js.Sha256(); sha256$1.update(Buffer.from(data, encoding)); return Buffer.from(sha256$1.digestSync()).toString("hex"); }; const decodeInvoice = (invoice) => { if (!invoice) return null; try { let network = void 0; if (invoice.startsWith("lntbs")) network = { bech32: "tbs", pubKeyHash: 111, scriptHash: 196, validWitnessVersions: [0, 1] }; return bolt11.decode(invoice, network); } catch (_unused) { return null; } }; const getHashFromInvoice = (invoice) => { if (!invoice) return null; try { const decoded = decodeInvoice(invoice); if (!decoded || !decoded.tags) return null; const hashTag = decoded.tags.find((value) => value.tagName === "payment_hash"); if (!hashTag || !hashTag.data) return null; return hashTag.data.toString(); } catch (_unused2) { return null; } }; const isValidPreimage = ({ invoice, preimage }) => { if (!invoice || !preimage) return false; const invoiceHash = getHashFromInvoice(invoice); if (!invoiceHash) return false; try { const preimageHash = sha256(preimage); return invoiceHash === preimageHash; } catch (_unused3) { return false; } }; const decipherAES = ({ successAction, preimage }) => { if (successAction.tag !== "aes" || !successAction.iv || !successAction.ciphertext || !preimage) return null; const key = aes_js.default.utils.hex.toBytes(preimage); const iv = base64_js.default.toByteArray(successAction.iv); const ciphertext = base64_js.default.toByteArray(successAction.ciphertext); const cbc = new aes_js.default.ModeOfOperation.cbc(key, iv); let plaintext = cbc.decrypt(ciphertext); const size = plaintext.length; const pad = plaintext[size - 1]; plaintext = plaintext.slice(0, size - pad); return aes_js.default.utils.utf8.fromBytes(plaintext); }; //#endregion //#region src/request-pay-service-params.ts var import_asyncToGenerator$1 = /* @__PURE__ */ __toESM(require_asyncToGenerator()); const TAG_PAY_REQUEST = "payRequest"; const requestPayServiceParams = function() { var _ref = (0, import_asyncToGenerator$1.default)(function* ({ lnUrlOrAddress, onionAllowed = false, fetchGet = getJson }) { const url = decodeUrlOrAddress(lnUrlOrAddress); if (!isUrl(url)) throw new Error("Invalid lnUrlOrAddress"); if (!onionAllowed && isOnionUrl(url)) throw new Error("Onion requests not allowed"); const json = yield fetchGet({ url }); const params = parseLnUrlPayServiceResponse(json); if (!params) throw new Error("Invalid pay service params"); return params; }); return function requestPayServiceParams$1(_x) { return _ref.apply(this, arguments); }; }(); /** * Parse the ln service response to LnUrlPayServiceResponse * @method parseLnUrlPayServiceResponse * @param data object to parse * @return LnUrlPayServiceResponse */ const parseLnUrlPayServiceResponse = (data) => { if (data.tag !== TAG_PAY_REQUEST) return null; const callback = (data.callback + "").trim(); if (!isUrl(callback)) return null; const min = checkedToSats(Math.ceil(Number(data.minSendable || 0) / 1e3)); const max = checkedToSats(Math.floor(Number(data.maxSendable) / 1e3)); if (!(min && max) || min > max) return null; let metadata; let metadataHash; try { metadata = JSON.parse(data.metadata + ""); metadataHash = sha256(data.metadata + "", "utf8"); } catch (_unused) { metadata = []; metadataHash = sha256("[]", "utf8"); } let image = ""; let description = ""; let identifier = ""; for (let i = 0; i < metadata.length; i++) { const [k, v] = metadata[i]; switch (k) { case "text/plain": description = v; break; case "text/identifier": identifier = v; break; case "image/png;base64": case "image/jpeg;base64": image = "data:" + k + "," + v; break; } } let domain; try { domain = new URL(callback).hostname; } catch (_unused2) {} return { callback, fixed: min === max, min, max, domain, metadata, metadataHash, identifier, description, image, commentAllowed: Number(data.commentAllowed) || 0, rawData: data }; }; //#endregion //#region src/request-invoice.ts var import_asyncToGenerator = /* @__PURE__ */ __toESM(require_asyncToGenerator()); const requestInvoiceWithServiceParams = function() { var _ref = (0, import_asyncToGenerator.default)(function* ({ params, tokens, comment, onionAllowed = false, validateInvoice = false, fetchGet = getJson }) { const { callback, commentAllowed, min, max } = params; if (!isValidAmount({ amount: tokens, min, max })) throw new Error("Invalid amount"); if (!isUrl(callback)) throw new Error("Callback must be a valid url"); if (!onionAllowed && isOnionUrl(callback)) throw new Error("Onion requests not allowed"); const invoiceParams = { amount: tokens * 1e3 }; if (comment && commentAllowed > 0 && comment.length > commentAllowed) throw new Error(`The comment length must be ${commentAllowed} characters or fewer`); if (comment) invoiceParams.comment = comment; const data = yield fetchGet({ url: callback, params: invoiceParams }); const invoice = data && data.pr && data.pr.toString(); if (!invoice) throw new Error("Invalid pay service invoice"); const decodedInvoice = decodeInvoice(invoice); const descriptionHash = decodedInvoice === null || decodedInvoice === void 0 ? void 0 : decodedInvoice.tags.find((t) => t.tagName === "purpose_commit_hash"); const hasValidDescriptionHash = descriptionHash ? params.metadataHash === descriptionHash.data : false; if (validateInvoice && !hasValidDescriptionHash) throw new Error(`Invoice description hash doesn't match metadata hash.`); const hasValidAmount = decodedInvoice ? decodedInvoice.satoshis === tokens : false; if (validateInvoice && !hasValidAmount) throw new Error(`Invalid invoice amount ${decodedInvoice === null || decodedInvoice === void 0 ? void 0 : decodedInvoice.satoshis}. Expected ${tokens}`); let successAction = void 0; if (data.successAction) { const decipher = (preimage) => decipherAES({ preimage, successAction: data.successAction }); successAction = Object.assign({ decipher }, data.successAction); } return { params, rawData: data, invoice, successAction, hasValidAmount, hasValidDescriptionHash, validatePreimage: (preimage) => isValidPreimage({ invoice, preimage }) }; }); return function requestInvoiceWithServiceParams$1(_x) { return _ref.apply(this, arguments); }; }(); const requestInvoice = function() { var _ref2 = (0, import_asyncToGenerator.default)(function* ({ lnUrlOrAddress, tokens, comment, onionAllowed = false, validateInvoice = false, fetchGet = getJson }) { const params = yield requestPayServiceParams({ lnUrlOrAddress, onionAllowed, fetchGet }); return requestInvoiceWithServiceParams({ params, tokens, comment, onionAllowed, validateInvoice, fetchGet }); }); return function requestInvoice$1(_x2) { return _ref2.apply(this, arguments); }; }(); //#endregion exports.requestInvoice = requestInvoice; exports.requestInvoiceWithServiceParams = requestInvoiceWithServiceParams; exports.requestPayServiceParams = requestPayServiceParams; Object.defineProperty(exports, 'utils', { enumerable: true, get: function () { return utils_exports; } }); //# sourceMappingURL=index.js.map