@atomiqlabs/sdk-lib
Version:
Basic SDK functionality library for atomiq
358 lines (357 loc) • 20.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ToBTCLNWrapper = void 0;
const bolt11_1 = require("@atomiqlabs/bolt11");
const ToBTCLNSwap_1 = require("./ToBTCLNSwap");
const IToBTCWrapper_1 = require("../IToBTCWrapper");
const UserError_1 = require("../../../errors/UserError");
const base_1 = require("@atomiqlabs/base");
const IntermediaryError_1 = require("../../../errors/IntermediaryError");
const SwapType_1 = require("../../SwapType");
const Utils_1 = require("../../../utils/Utils");
const IntermediaryAPI_1 = require("../../../intermediaries/IntermediaryAPI");
const RequestError_1 = require("../../../errors/RequestError");
const LNURL_1 = require("../../../utils/LNURL");
const IToBTCSwap_1 = require("../IToBTCSwap");
class ToBTCLNWrapper extends IToBTCWrapper_1.IToBTCWrapper {
constructor(chainIdentifier, unifiedStorage, unifiedChainEvents, contract, prices, tokens, swapDataDeserializer, options, events) {
if (options == null)
options = {};
options.paymentTimeoutSeconds ??= 4 * 24 * 60 * 60;
options.lightningBaseFee ??= 10;
options.lightningFeePPM ??= 2000;
super(chainIdentifier, unifiedStorage, unifiedChainEvents, contract, prices, tokens, swapDataDeserializer, options, events);
this.TYPE = SwapType_1.SwapType.TO_BTCLN;
this.swapDeserializer = ToBTCLNSwap_1.ToBTCLNSwap;
}
async checkPaymentHashWasPaid(paymentHash) {
const swaps = await this.unifiedStorage.query([[{ key: "type", value: this.TYPE }, { key: "paymentHash", value: paymentHash }]], (obj) => new this.swapDeserializer(this, obj));
for (let value of swaps) {
if (value.state === IToBTCSwap_1.ToBTCSwapState.CLAIMED || value.state === IToBTCSwap_1.ToBTCSwapState.SOFT_CLAIMED)
throw new UserError_1.UserError("Lightning invoice was already paid!");
}
}
/**
* Calculates maximum lightning network routing fee based on amount
*
* @param amount BTC amount of the swap in satoshis
* @param overrideBaseFee Override wrapper's default base fee
* @param overrideFeePPM Override wrapper's default PPM
* @private
* @returns Maximum lightning routing fee in sats
*/
calculateFeeForAmount(amount, overrideBaseFee, overrideFeePPM) {
return BigInt(overrideBaseFee ?? this.options.lightningBaseFee)
+ (amount * BigInt(overrideFeePPM ?? this.options.lightningFeePPM) / 1000000n);
}
/**
* Verifies returned LP data
*
* @param resp Response as returned by the LP
* @param parsedPr Parsed bolt11 lightning invoice
* @param token Smart chain token to be used in the swap
* @param lp
* @param options Swap options as passed to the swap create function
* @param data Parsed swap data returned by the LP
* @param requiredTotal Required total to be paid on the input (for exactIn swaps)
* @private
* @throws {IntermediaryError} In case the response is not valid
*/
async verifyReturnedData(resp, parsedPr, token, lp, options, data, requiredTotal) {
if (resp.routingFeeSats > await options.maxFee)
throw new IntermediaryError_1.IntermediaryError("Invalid max fee sats returned");
if (requiredTotal != null && resp.total !== requiredTotal)
throw new IntermediaryError_1.IntermediaryError("Invalid data returned - total amount");
const claimHash = this.contract.getHashForHtlc(Buffer.from(parsedPr.tagsObject.payment_hash, "hex"));
if (data.getAmount() !== resp.total ||
!Buffer.from(data.getClaimHash(), "hex").equals(claimHash) ||
data.getExpiry() !== options.expiryTimestamp ||
data.getType() !== base_1.ChainSwapType.HTLC ||
!data.isPayIn() ||
!data.isToken(token) ||
data.getClaimer() !== lp.getAddress(this.chainIdentifier)) {
throw new IntermediaryError_1.IntermediaryError("Invalid data returned");
}
}
/**
* Returns the quote/swap from a given intermediary
*
* @param signer Smartchain signer initiating the swap
* @param amountData
* @param lp Intermediary
* @param pr bolt11 lightning network invoice
* @param parsedPr Parsed bolt11 lightning network invoice
* @param options Options as passed to the swap create function
* @param preFetches
* @param abort Abort signal or controller, if AbortController is passed it is used as-is, when AbortSignal is passed
* it is extended with extendAbortController and then used
* @param additionalParams Additional params that should be sent to the LP
* @private
*/
async getIntermediaryQuote(signer, amountData, lp, pr, parsedPr, options, preFetches, abort, additionalParams) {
const abortController = abort instanceof AbortController ? abort : (0, Utils_1.extendAbortController)(abort);
preFetches.reputationPromise ??= this.preFetchIntermediaryReputation(amountData, lp, abortController);
try {
const { signDataPromise, resp } = await (0, Utils_1.tryWithRetries)(async (retryCount) => {
const { signDataPrefetch, response } = IntermediaryAPI_1.IntermediaryAPI.initToBTCLN(this.chainIdentifier, lp.url, {
offerer: signer,
pr,
maxFee: await options.maxFee,
expiryTimestamp: options.expiryTimestamp,
token: amountData.token,
feeRate: preFetches.feeRatePromise,
additionalParams
}, this.options.postRequestTimeout, abortController.signal, retryCount > 0 ? false : null);
return {
signDataPromise: this.preFetchSignData(signDataPrefetch),
resp: await response
};
}, null, e => e instanceof RequestError_1.RequestError, abortController.signal);
const amountOut = (BigInt(parsedPr.millisatoshis) + 999n) / 1000n;
const totalFee = resp.swapFee + resp.maxFee;
const data = new this.swapDataDeserializer(resp.data);
data.setOfferer(signer);
await this.verifyReturnedData(resp, parsedPr, amountData.token, lp, options, data);
const [pricingInfo, signatureExpiry, reputation] = await Promise.all([
this.verifyReturnedPrice(lp.services[SwapType_1.SwapType.TO_BTCLN], true, amountOut, data.getAmount(), amountData.token, { swapFee: resp.swapFee, networkFee: resp.maxFee, totalFee }, preFetches.pricePreFetchPromise, abortController.signal),
this.verifyReturnedSignature(data, resp, preFetches.feeRatePromise, signDataPromise, abortController.signal),
preFetches.reputationPromise
]);
abortController.signal.throwIfAborted();
lp.reputation[amountData.token.toString()] = reputation;
const quote = new ToBTCLNSwap_1.ToBTCLNSwap(this, {
pricingInfo,
url: lp.url,
expiry: signatureExpiry,
swapFee: resp.swapFee,
feeRate: await preFetches.feeRatePromise,
signatureData: resp,
data,
networkFee: resp.maxFee,
networkFeeBtc: resp.routingFeeSats,
confidence: resp.confidence,
pr,
exactIn: false
});
await quote._save();
return quote;
}
catch (e) {
abortController.abort(e);
throw e;
}
}
/**
* Returns a newly created swap, paying for 'bolt11PayRequest' - a bitcoin LN invoice
*
* @param signer Smartchain signer's address initiating the swap
* @param bolt11PayRequest BOLT11 payment request (bitcoin lightning invoice) you wish to pay
* @param amountData Amount of token & amount to swap
* @param lps LPs (liquidity providers) to get the quotes from
* @param options Quote options
* @param additionalParams Additional parameters sent to the LP when creating the swap
* @param abortSignal Abort signal for aborting the process
* @param preFetches Existing pre-fetches for the swap (only used internally for LNURL swaps)
*/
async create(signer, bolt11PayRequest, amountData, lps, options, additionalParams, abortSignal, preFetches) {
options ??= {};
options.expirySeconds ??= this.options.paymentTimeoutSeconds;
options.expiryTimestamp ??= BigInt(Math.floor(Date.now() / 1000) + options.expirySeconds);
const parsedPr = (0, bolt11_1.decode)(bolt11PayRequest);
if (parsedPr.millisatoshis == null)
throw new UserError_1.UserError("Must be an invoice with amount");
const amountOut = (BigInt(parsedPr.millisatoshis) + 999n) / 1000n;
options.maxFee ??= this.calculateFeeForAmount(amountOut, options.maxRoutingBaseFee, options.maxRoutingPPM);
await this.checkPaymentHashWasPaid(parsedPr.tagsObject.payment_hash);
const claimHash = this.contract.getHashForHtlc(Buffer.from(parsedPr.tagsObject.payment_hash, "hex"));
const _abortController = (0, Utils_1.extendAbortController)(abortSignal);
if (preFetches == null)
preFetches = {
pricePreFetchPromise: this.preFetchPrice(amountData, _abortController.signal),
feeRatePromise: this.preFetchFeeRate(signer, amountData, claimHash.toString("hex"), _abortController)
};
return lps.map(lp => {
return {
intermediary: lp,
quote: this.getIntermediaryQuote(signer, amountData, lp, bolt11PayRequest, parsedPr, options, preFetches, _abortController.signal, additionalParams)
};
});
}
/**
* Parses and fetches lnurl pay params from the specified lnurl
*
* @param lnurl LNURL to be parsed and fetched
* @param abortSignal
* @private
* @throws {UserError} if the LNURL is invalid or if it's not a LNURL-pay
*/
async getLNURLPay(lnurl, abortSignal) {
if (typeof (lnurl) !== "string")
return lnurl;
const res = await LNURL_1.LNURL.getLNURL(lnurl, true, this.options.getRequestTimeout, abortSignal);
if (res == null)
throw new UserError_1.UserError("Invalid LNURL");
if (res.tag !== "payRequest")
throw new UserError_1.UserError("Not a LNURL-pay");
return res;
}
/**
* Returns the quote/swap from the given LP
*
* @param signer Smartchain signer's address initiating the swap
* @param amountData
* @param payRequest Parsed LNURL-pay params
* @param lp Intermediary
* @param dummyPr Dummy minimum value bolt11 lightning invoice returned from the LNURL-pay
* @param options Options as passed to the swap create function
* @param preFetches
* @param abortSignal
* @param additionalParams Additional params to be sent to the intermediary
* @private
*/
async getIntermediaryQuoteExactIn(signer, amountData, payRequest, lp, dummyPr, options, preFetches, abortSignal, additionalParams) {
const abortController = (0, Utils_1.extendAbortController)(abortSignal);
const reputationPromise = this.preFetchIntermediaryReputation(amountData, lp, abortController);
try {
const { signDataPromise, prepareResp } = await (0, Utils_1.tryWithRetries)(async (retryCount) => {
const { signDataPrefetch, response } = IntermediaryAPI_1.IntermediaryAPI.prepareToBTCLNExactIn(this.chainIdentifier, lp.url, {
token: amountData.token,
offerer: signer,
pr: dummyPr,
amount: amountData.amount,
maxFee: await options.maxFee,
expiryTimestamp: options.expiryTimestamp,
additionalParams
}, this.options.postRequestTimeout, abortController.signal, retryCount > 0 ? false : null);
return {
signDataPromise: this.preFetchSignData(signDataPrefetch),
prepareResp: await response
};
}, null, e => e instanceof RequestError_1.RequestError, abortController.signal);
if (prepareResp.amount <= 0n)
throw new IntermediaryError_1.IntermediaryError("Invalid amount returned (zero or negative)");
const min = BigInt(payRequest.minSendable) / 1000n;
const max = BigInt(payRequest.maxSendable) / 1000n;
if (prepareResp.amount < min)
throw new UserError_1.UserError("Amount less than minimum");
if (prepareResp.amount > max)
throw new UserError_1.UserError("Amount more than maximum");
const { invoice, parsedInvoice, successAction } = await LNURL_1.LNURL.useLNURLPay(payRequest, prepareResp.amount, options.comment, this.options.getRequestTimeout, abortController.signal);
const resp = await (0, Utils_1.tryWithRetries)((retryCount) => IntermediaryAPI_1.IntermediaryAPI.initToBTCLNExactIn(lp.url, {
pr: invoice,
reqId: prepareResp.reqId,
feeRate: preFetches.feeRatePromise,
additionalParams
}, this.options.postRequestTimeout, abortController.signal, retryCount > 0 ? false : null), null, RequestError_1.RequestError, abortController.signal);
const totalFee = resp.swapFee + resp.maxFee;
const data = new this.swapDataDeserializer(resp.data);
data.setOfferer(signer);
await this.verifyReturnedData(resp, parsedInvoice, amountData.token, lp, options, data, amountData.amount);
const [pricingInfo, signatureExpiry, reputation] = await Promise.all([
this.verifyReturnedPrice(lp.services[SwapType_1.SwapType.TO_BTCLN], true, prepareResp.amount, data.getAmount(), amountData.token, { swapFee: resp.swapFee, networkFee: resp.maxFee, totalFee }, preFetches.pricePreFetchPromise, abortSignal),
this.verifyReturnedSignature(data, resp, preFetches.feeRatePromise, signDataPromise, abortController.signal),
reputationPromise
]);
abortController.signal.throwIfAborted();
lp.reputation[amountData.token.toString()] = reputation;
const quote = new ToBTCLNSwap_1.ToBTCLNSwap(this, {
pricingInfo,
url: lp.url,
expiry: signatureExpiry,
swapFee: resp.swapFee,
feeRate: await preFetches.feeRatePromise,
signatureData: resp,
data,
networkFee: resp.maxFee,
networkFeeBtc: resp.routingFeeSats,
confidence: resp.confidence,
pr: invoice,
lnurl: payRequest.url,
successAction,
exactIn: true
});
await quote._save();
return quote;
}
catch (e) {
abortController.abort(e);
throw e;
}
}
/**
* Returns a newly created swap, paying for 'lnurl' - a lightning LNURL-pay
*
* @param signer Smartchain signer's address initiating the swap
* @param lnurl LMURL-pay you wish to pay
* @param amountData Amount of token & amount to swap
* @param lps LPs (liquidity providers/intermediaries) to get the quotes from
* @param options Quote options
* @param additionalParams Additional parameters sent to the intermediary when creating the swap
* @param abortSignal Abort signal for aborting the process
*/
async createViaLNURL(signer, lnurl, amountData, lps, options, additionalParams, abortSignal) {
if (!this.isInitialized)
throw new Error("Not initialized, call init() first!");
options ??= {};
options.expirySeconds ??= this.options.paymentTimeoutSeconds;
options.expiryTimestamp ??= BigInt(Math.floor(Date.now() / 1000) + options.expirySeconds);
const _abortController = (0, Utils_1.extendAbortController)(abortSignal);
const pricePreFetchPromise = this.preFetchPrice(amountData, _abortController.signal);
const feeRatePromise = this.preFetchFeeRate(signer, amountData, null, _abortController);
options.maxRoutingPPM ??= BigInt(this.options.lightningFeePPM);
options.maxRoutingBaseFee ??= BigInt(this.options.lightningBaseFee);
if (amountData.exactIn) {
options.maxFee ??= pricePreFetchPromise
.then(val => this.prices.getFromBtcSwapAmount(this.chainIdentifier, options.maxRoutingBaseFee, amountData.token, abortSignal, val))
.then(_maxBaseFee => this.calculateFeeForAmount(amountData.amount, _maxBaseFee, options.maxRoutingPPM));
}
else {
options.maxFee = this.calculateFeeForAmount(amountData.amount, options.maxRoutingBaseFee, options.maxRoutingPPM);
}
try {
let payRequest = await this.getLNURLPay(lnurl, _abortController.signal);
if (options.comment != null &&
(payRequest.commentAllowed == null || options.comment.length > payRequest.commentAllowed))
throw new UserError_1.UserError("Comment not allowed or too long");
if (amountData.exactIn) {
const { invoice: dummyInvoice } = await LNURL_1.LNURL.useLNURLPay(payRequest, BigInt(payRequest.minSendable) / 1000n, null, this.options.getRequestTimeout, _abortController.signal);
return lps.map(lp => {
return {
quote: this.getIntermediaryQuoteExactIn(signer, amountData, payRequest, lp, dummyInvoice, options, {
pricePreFetchPromise,
feeRatePromise
}, _abortController.signal, additionalParams),
intermediary: lp
};
});
}
else {
const min = BigInt(payRequest.minSendable) / 1000n;
const max = BigInt(payRequest.maxSendable) / 1000n;
if (amountData.amount < min)
throw new UserError_1.UserError("Amount less than minimum");
if (amountData.amount > max)
throw new UserError_1.UserError("Amount more than maximum");
const { invoice, parsedInvoice, successAction } = await LNURL_1.LNURL.useLNURLPay(payRequest, amountData.amount, options.comment, this.options.getRequestTimeout, _abortController.signal);
return (await this.create(signer, invoice, amountData, lps, options, additionalParams, _abortController.signal, {
feeRatePromise,
pricePreFetchPromise
})).map(data => {
return {
quote: data.quote.then(quote => {
quote.lnurl = payRequest.url;
quote.successAction = successAction;
return quote;
}),
intermediary: data.intermediary
};
});
}
}
catch (e) {
_abortController.abort(e);
throw e;
}
}
}
exports.ToBTCLNWrapper = ToBTCLNWrapper;