@atomiqlabs/sdk-lib
Version:
Basic SDK functionality library for atomiq
281 lines (280 loc) • 11.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LnForGasSwap = exports.isLnForGasSwapInit = exports.LnForGasSwapState = void 0;
const bolt11_1 = require("@atomiqlabs/bolt11");
const SwapType_1 = require("../../enums/SwapType");
const PaymentAuthError_1 = require("../../../errors/PaymentAuthError");
const Utils_1 = require("../../../utils/Utils");
const ISwap_1 = require("../../ISwap");
const TrustedIntermediaryAPI_1 = require("../../../intermediaries/TrustedIntermediaryAPI");
const Tokens_1 = require("../../../Tokens");
const Fee_1 = require("../../fee/Fee");
var LnForGasSwapState;
(function (LnForGasSwapState) {
LnForGasSwapState[LnForGasSwapState["EXPIRED"] = -2] = "EXPIRED";
LnForGasSwapState[LnForGasSwapState["FAILED"] = -1] = "FAILED";
LnForGasSwapState[LnForGasSwapState["PR_CREATED"] = 0] = "PR_CREATED";
LnForGasSwapState[LnForGasSwapState["PR_PAID"] = 1] = "PR_PAID";
LnForGasSwapState[LnForGasSwapState["FINISHED"] = 2] = "FINISHED";
})(LnForGasSwapState = exports.LnForGasSwapState || (exports.LnForGasSwapState = {}));
function isLnForGasSwapInit(obj) {
return typeof (obj.pr) === "string" &&
typeof (obj.outputAmount) === "bigint" &&
typeof (obj.recipient) === "string" &&
typeof (obj.token) === "string" &&
(0, ISwap_1.isISwapInit)(obj);
}
exports.isLnForGasSwapInit = isLnForGasSwapInit;
class LnForGasSwap extends ISwap_1.ISwap {
constructor(wrapper, initOrObj) {
if (isLnForGasSwapInit(initOrObj))
initOrObj.url += "/lnforgas";
super(wrapper, initOrObj);
this.currentVersion = 2;
this.TYPE = SwapType_1.SwapType.TRUSTED_FROM_BTCLN;
if (isLnForGasSwapInit(initOrObj)) {
this.state = LnForGasSwapState.PR_CREATED;
}
else {
this.pr = initOrObj.pr;
this.outputAmount = initOrObj.outputAmount == null ? null : BigInt(initOrObj.outputAmount);
this.recipient = initOrObj.recipient;
this.token = initOrObj.token;
this.scTxId = initOrObj.scTxId;
}
this.tryRecomputeSwapPrice();
if (this.pr != null) {
const decoded = (0, bolt11_1.decode)(this.pr);
this.expiry = decoded.timeExpireDate * 1000;
}
this.logger = (0, Utils_1.getLogger)("LnForGas(" + this.getId() + "): ");
}
upgradeVersion() {
if (this.version == 1) {
if (this.state === 1)
this.state = LnForGasSwapState.FINISHED;
this.version = 2;
}
if (this.version == null) {
//Noop
this.version = 1;
}
}
/**
* In case swapFee in BTC is not supplied it recalculates it based on swap price
* @protected
*/
tryRecomputeSwapPrice() {
if (this.swapFeeBtc == null) {
this.swapFeeBtc = this.swapFee * this.getInput().rawAmount / this.getOutAmountWithoutFee();
}
super.tryRecomputeSwapPrice();
}
//////////////////////////////
//// Getters & utils
_getEscrowHash() {
return this.getId();
}
getOutputAddress() {
return this.recipient;
}
getInputTxId() {
return this.getId();
}
getOutputTxId() {
return this.scTxId;
}
getId() {
if (this.pr == null)
return null;
const decodedPR = (0, bolt11_1.decode)(this.pr);
return decodedPR.tagsObject.payment_hash;
}
/**
* Returns the lightning network BOLT11 invoice that needs to be paid as an input to the swap
*/
getAddress() {
return this.pr;
}
/**
* Returns a string that can be displayed as QR code representation of the lightning invoice (with lightning: prefix)
*/
getHyperlink() {
return "lightning:" + this.pr.toUpperCase();
}
requiresAction() {
return false;
}
isFinished() {
return this.state === LnForGasSwapState.FINISHED || this.state === LnForGasSwapState.FAILED || this.state === LnForGasSwapState.EXPIRED;
}
isQuoteExpired() {
return this.state === LnForGasSwapState.EXPIRED;
}
isQuoteSoftExpired() {
return this.expiry < Date.now();
}
isFailed() {
return this.state === LnForGasSwapState.FAILED;
}
isSuccessful() {
return this.state === LnForGasSwapState.FINISHED;
}
verifyQuoteValid() {
return Promise.resolve(this.expiry > Date.now());
}
//////////////////////////////
//// Amounts & fees
getOutAmountWithoutFee() {
return this.outputAmount + this.swapFee;
}
getOutput() {
return (0, Tokens_1.toTokenAmount)(this.outputAmount, this.wrapper.tokens[this.wrapper.chain.getNativeCurrencyAddress()], this.wrapper.prices);
}
getInput() {
const parsed = (0, bolt11_1.decode)(this.pr);
const amount = (BigInt(parsed.millisatoshis) + 999n) / 1000n;
return (0, Tokens_1.toTokenAmount)(amount, Tokens_1.BitcoinTokens.BTCLN, this.wrapper.prices);
}
getInputWithoutFee() {
const parsed = (0, bolt11_1.decode)(this.pr);
const amount = (BigInt(parsed.millisatoshis) + 999n) / 1000n;
return (0, Tokens_1.toTokenAmount)(amount - this.swapFeeBtc, Tokens_1.BitcoinTokens.BTCLN, this.wrapper.prices);
}
getSwapFee() {
const feeWithoutBaseFee = this.swapFeeBtc - this.pricingInfo.satsBaseFee;
const swapFeePPM = feeWithoutBaseFee * 1000000n / this.getInputWithoutFee().rawAmount;
return {
amountInSrcToken: (0, Tokens_1.toTokenAmount)(this.swapFeeBtc, Tokens_1.BitcoinTokens.BTCLN, this.wrapper.prices),
amountInDstToken: (0, Tokens_1.toTokenAmount)(this.swapFee, this.wrapper.tokens[this.wrapper.chain.getNativeCurrencyAddress()], this.wrapper.prices),
usdValue: (abortSignal, preFetchedUsdPrice) => this.wrapper.prices.getBtcUsdValue(this.swapFeeBtc, abortSignal, preFetchedUsdPrice),
composition: {
base: (0, Tokens_1.toTokenAmount)(this.pricingInfo.satsBaseFee, Tokens_1.BitcoinTokens.BTCLN, this.wrapper.prices),
percentage: (0, ISwap_1.ppmToPercentage)(swapFeePPM)
}
};
}
getFee() {
return this.getSwapFee();
}
getFeeBreakdown() {
return [{
type: Fee_1.FeeType.SWAP,
fee: this.getSwapFee()
}];
}
//////////////////////////////
//// Payment
async checkInvoicePaid(save = true) {
if (this.state === LnForGasSwapState.FAILED || this.state === LnForGasSwapState.EXPIRED)
return false;
if (this.state === LnForGasSwapState.FINISHED)
return true;
const decodedPR = (0, bolt11_1.decode)(this.pr);
const paymentHash = decodedPR.tagsObject.payment_hash;
const response = await TrustedIntermediaryAPI_1.TrustedIntermediaryAPI.getInvoiceStatus(this.url, paymentHash, this.wrapper.options.getRequestTimeout);
this.logger.debug("checkInvoicePaid(): LP response: ", response);
switch (response.code) {
case TrustedIntermediaryAPI_1.InvoiceStatusResponseCodes.PAID:
this.scTxId = response.data.txId;
const txStatus = await this.wrapper.chain.getTxIdStatus(this.scTxId);
if (txStatus === "success") {
this.state = LnForGasSwapState.FINISHED;
if (save)
await this._saveAndEmit();
return true;
}
return null;
case TrustedIntermediaryAPI_1.InvoiceStatusResponseCodes.EXPIRED:
if (this.state === LnForGasSwapState.PR_CREATED) {
this.state = LnForGasSwapState.EXPIRED;
}
else {
this.state = LnForGasSwapState.FAILED;
}
if (save)
await this._saveAndEmit();
return false;
case TrustedIntermediaryAPI_1.InvoiceStatusResponseCodes.TX_SENT:
this.scTxId = response.data.txId;
if (this.state === LnForGasSwapState.PR_CREATED) {
this.state = LnForGasSwapState.PR_PAID;
if (save)
await this._saveAndEmit();
}
return null;
case TrustedIntermediaryAPI_1.InvoiceStatusResponseCodes.PENDING:
if (this.state === LnForGasSwapState.PR_CREATED) {
this.state = LnForGasSwapState.PR_PAID;
if (save)
await this._saveAndEmit();
}
return null;
case TrustedIntermediaryAPI_1.InvoiceStatusResponseCodes.AWAIT_PAYMENT:
return null;
default:
this.state = LnForGasSwapState.FAILED;
if (save)
await this._saveAndEmit();
return false;
}
}
/**
* A blocking promise resolving when payment was received by the intermediary and client can continue
* rejecting in case of failure
*
* @param checkIntervalSeconds How often to poll the intermediary for answer (default 5 seconds)
* @param abortSignal Abort signal
* @throws {PaymentAuthError} If swap expired or failed
* @throws {Error} When in invalid state (not PR_CREATED)
*/
async waitForPayment(checkIntervalSeconds, abortSignal) {
if (this.state !== LnForGasSwapState.PR_CREATED)
throw new Error("Must be in PR_CREATED state!");
if (!this.initiated) {
this.initiated = true;
await this._saveAndEmit();
}
while (!abortSignal.aborted && (this.state === LnForGasSwapState.PR_CREATED || this.state === LnForGasSwapState.PR_PAID)) {
await this.checkInvoicePaid(true);
if (this.state === LnForGasSwapState.PR_CREATED || this.state === LnForGasSwapState.PR_PAID)
await (0, Utils_1.timeoutPromise)(checkIntervalSeconds * 1000, abortSignal);
}
if (this.isFailed())
throw new PaymentAuthError_1.PaymentAuthError("Swap failed");
return !this.isQuoteExpired();
}
//////////////////////////////
//// Storage
serialize() {
return {
...super.serialize(),
pr: this.pr,
outputAmount: this.outputAmount == null ? null : this.outputAmount.toString(10),
recipient: this.recipient,
token: this.token,
scTxId: this.scTxId
};
}
_getInitiator() {
return this.recipient;
}
//////////////////////////////
//// Swap ticks & sync
async _sync(save) {
if (this.state === LnForGasSwapState.PR_CREATED) {
//Check if it's maybe already paid
const res = await this.checkInvoicePaid(false);
if (res !== null) {
if (save)
await this._saveAndEmit();
return true;
}
}
return false;
}
_tick(save) {
return Promise.resolve(false);
}
}
exports.LnForGasSwap = LnForGasSwap;