lnurl-pay
Version:
Client library for lnurl-pay and lightning address
465 lines (459 loc) • 15.7 kB
JavaScript
//#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