@atomiqlabs/sdk
Version:
atomiq labs SDK for cross-chain swaps between smart chains and bitcoin
626 lines (625 loc) • 23.3 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 Utils_1 = require("../../../utils/Utils");
const ISwap_1 = require("../../ISwap");
const IntermediaryAPI_1 = require("../../../intermediaries/apis/IntermediaryAPI");
const FeeType_1 = require("../../../enums/FeeType");
const PercentagePPM_1 = require("../../../types/fees/PercentagePPM");
const TokenAmount_1 = require("../../../types/TokenAmount");
const Token_1 = require("../../../types/Token");
const Logger_1 = require("../../../utils/Logger");
const TimeoutUtils_1 = require("../../../utils/TimeoutUtils");
/**
* State enum for trusted Lightning gas swaps
*
* @category Swaps/Trusted Gas Swaps
*/
var LnForGasSwapState;
(function (LnForGasSwapState) {
/**
* The swap quote expired before the user paid the Lightning invoice
*/
LnForGasSwapState[LnForGasSwapState["EXPIRED"] = -2] = "EXPIRED";
/**
* The swap has failed before the destination payout completed, and the held Lightning invoice was released
*/
LnForGasSwapState[LnForGasSwapState["FAILED"] = -1] = "FAILED";
/**
* Swap was created, pay the provided Lightning invoice which will remain held until destination payout succeeds
*/
LnForGasSwapState[LnForGasSwapState["PR_CREATED"] = 0] = "PR_CREATED";
/**
* The Lightning invoice was paid and is currently held until the user receives the destination funds
*/
LnForGasSwapState[LnForGasSwapState["PR_PAID"] = 1] = "PR_PAID";
/**
* The swap is finished after the destination payout succeeded and the held Lightning invoice was settled
*/
LnForGasSwapState[LnForGasSwapState["FINISHED"] = 2] = "FINISHED";
})(LnForGasSwapState = exports.LnForGasSwapState || (exports.LnForGasSwapState = {}));
const LnForGasSwapStateDescription = {
[LnForGasSwapState.EXPIRED]: "The swap quote expired before the user paid the Lightning invoice",
[LnForGasSwapState.FAILED]: "The swap failed before destination payout completed, and the held Lightning invoice was released back to the user",
[LnForGasSwapState.PR_CREATED]: "Swap was created, pay the provided Lightning invoice. The invoice will remain held until destination payout succeeds",
[LnForGasSwapState.PR_PAID]: "The Lightning invoice was paid and is currently held. It will only settle once the user receives the destination funds",
[LnForGasSwapState.FINISHED]: "The swap is finished after the destination payout succeeded and the held Lightning invoice was settled"
};
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;
/**
* Trusted swap for Bitcoin Lightning -> Smart chains, to be used for minor amounts to get gas tokens on
* the destination chain, which is only needed for Solana, which still uses legacy swaps
*
* @category Swaps/Trusted Gas Swaps
*/
class LnForGasSwap extends ISwap_1.ISwap {
constructor(wrapper, initOrObj) {
if (isLnForGasSwapInit(initOrObj) && initOrObj.url != null)
initOrObj.url += "/lnforgas";
super(wrapper, initOrObj);
this.TYPE = SwapType_1.SwapType.TRUSTED_FROM_BTCLN;
/**
* @internal
*/
this.swapStateDescription = LnForGasSwapStateDescription;
/**
* @internal
*/
this.swapStateName = (state) => LnForGasSwapState[state];
/**
* @internal
*/
this.currentVersion = 2;
if (isLnForGasSwapInit(initOrObj)) {
this.pr = initOrObj.pr;
this.outputAmount = initOrObj.outputAmount;
this.recipient = initOrObj.recipient;
this.token = initOrObj.token;
this._state = LnForGasSwapState.PR_CREATED;
}
else {
this.pr = initOrObj.pr;
this.outputAmount = (0, Utils_1.toBigInt)(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);
if (decoded.timeExpireDate != null)
this.expiry = decoded.timeExpireDate * 1000;
}
this.logger = (0, Logger_1.getLogger)("LnForGas(" + this.getId() + "): ");
}
/**
* @inheritDoc
* @internal
*/
upgradeVersion() {
if (this.version == 1) {
if (this._state === 1)
this._state = LnForGasSwapState.FINISHED;
this.version = 2;
}
if (this.version == null) {
//Noop
this.version = 1;
}
}
/**
* @inheritDoc
* @internal
*/
tryRecomputeSwapPrice() {
if (this.swapFeeBtc == null && this.swapFee != null) {
this.swapFeeBtc = this.swapFee * this.getInput().rawAmount / this.getOutAmountWithoutFee();
}
super.tryRecomputeSwapPrice();
}
//////////////////////////////
//// Getters & utils
/**
* @inheritDoc
* @internal
*/
_getEscrowHash() {
return this.getId();
}
/**
* @inheritDoc
*/
getOutputAddress() {
return this.recipient;
}
/**
* @inheritDoc
*/
getInputAddress() {
return this.pr;
}
/**
* @inheritDoc
*/
getInputTxId() {
return this.getId();
}
/**
* @inheritDoc
*/
getOutputTxId() {
return this.scTxId ?? null;
}
/**
* @inheritDoc
*/
getId() {
if (this.pr == null)
throw new Error("No payment request assigned to this swap!");
const decodedPR = (0, bolt11_1.decode)(this.pr);
if (decodedPR.tagsObject.payment_hash == null)
throw new Error("Lightning invoice has no payment hash!");
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();
}
/**
* @inheritDoc
*/
requiresAction() {
return false;
}
/**
* @inheritDoc
*/
isFinished() {
return this._state === LnForGasSwapState.FINISHED || this._state === LnForGasSwapState.FAILED || this._state === LnForGasSwapState.EXPIRED;
}
/**
* @inheritDoc
*/
isQuoteExpired() {
return this._state === LnForGasSwapState.EXPIRED;
}
/**
* @inheritDoc
*/
isQuoteSoftExpired() {
return this.expiry < Date.now();
}
/**
* @inheritDoc
*/
isFailed() {
return this._state === LnForGasSwapState.FAILED;
}
/**
* @inheritDoc
*/
isSuccessful() {
return this._state === LnForGasSwapState.FINISHED;
}
/**
* @inheritDoc
*/
isInProgress() {
return (this._state === LnForGasSwapState.PR_CREATED && this.initiated) || this._state === LnForGasSwapState.PR_PAID;
}
/**
* @inheritDoc
* @internal
*/
_verifyQuoteDefinitelyExpired() {
return Promise.resolve(this.expiry < Date.now());
}
/**
* @inheritDoc
* @internal
*/
_verifyQuoteValid() {
return Promise.resolve(this.expiry > Date.now());
}
//////////////////////////////
//// Amounts & fees
/**
* Returns an output amount in base units without a swap fee included, hence this value
* is larger than the actual output amount
*
* @internal
*/
getOutAmountWithoutFee() {
return this.outputAmount + (this.swapFee ?? 0n);
}
/**
* @inheritDoc
*/
getOutputToken() {
return this.wrapper._tokens[this.wrapper._chain.getNativeCurrencyAddress()];
}
/**
* @inheritDoc
*/
getOutput() {
return (0, TokenAmount_1.toTokenAmount)(this.outputAmount, this.wrapper._tokens[this.wrapper._chain.getNativeCurrencyAddress()], this.wrapper._prices, this.pricingInfo);
}
/**
* @inheritDoc
*/
getInputToken() {
return Token_1.BitcoinTokens.BTCLN;
}
/**
* @inheritDoc
*/
getInput() {
const parsed = (0, bolt11_1.decode)(this.pr);
const msats = parsed.millisatoshis;
if (msats == null)
throw new Error("Swap lightning invoice has no msat amount field!");
const amount = (BigInt(msats) + 999n) / 1000n;
return (0, TokenAmount_1.toTokenAmount)(amount, Token_1.BitcoinTokens.BTCLN, this.wrapper._prices, this.pricingInfo);
}
/**
* @inheritDoc
*/
getInputWithoutFee() {
const parsed = (0, bolt11_1.decode)(this.pr);
const msats = parsed.millisatoshis;
if (msats == null)
throw new Error("Swap lightning invoice has no msat amount field!");
const amount = (BigInt(msats) + 999n) / 1000n;
return (0, TokenAmount_1.toTokenAmount)(amount - (this.swapFeeBtc ?? 0n), Token_1.BitcoinTokens.BTCLN, this.wrapper._prices, this.pricingInfo);
}
/**
* Returns the swap fee charged by the intermediary (LP) on this swap
*
* @internal
*/
getSwapFee() {
if (this.pricingInfo == null)
throw new Error("No pricing info known, cannot estimate swap fee!");
const feeWithoutBaseFee = this.swapFeeBtc == null ? 0n : this.swapFeeBtc - this.pricingInfo.satsBaseFee;
const swapFeePPM = feeWithoutBaseFee * 1000000n / this.getInputWithoutFee().rawAmount;
const amountInSrcToken = (0, TokenAmount_1.toTokenAmount)(this.swapFeeBtc ?? 0n, Token_1.BitcoinTokens.BTCLN, this.wrapper._prices, this.pricingInfo);
return {
amountInSrcToken,
amountInDstToken: (0, TokenAmount_1.toTokenAmount)(this.swapFee ?? 0n, this.wrapper._tokens[this.wrapper._chain.getNativeCurrencyAddress()], this.wrapper._prices, this.pricingInfo),
currentUsdValue: amountInSrcToken.currentUsdValue,
usdValue: amountInSrcToken.usdValue,
pastUsdValue: amountInSrcToken.pastUsdValue,
composition: {
base: (0, TokenAmount_1.toTokenAmount)(this.pricingInfo.satsBaseFee, Token_1.BitcoinTokens.BTCLN, this.wrapper._prices, this.pricingInfo),
percentage: (0, PercentagePPM_1.ppmToPercentage)(swapFeePPM)
}
};
}
/**
* @inheritDoc
*/
getFee() {
return this.getSwapFee();
}
/**
* @inheritDoc
*/
getFeeBreakdown() {
return [{
type: FeeType_1.FeeType.SWAP,
fee: this.getSwapFee()
}];
}
//////////////////////////////
//// Payment
/**
* @remarks Not supported
*/
async execute() {
throw new Error("Not supported");
}
/**
* @internal
*/
async _getExecutionStatus() {
const state = this._state;
let lightningPaymentStatus = "inactive";
let destinationSettlementStatus = "inactive";
let buildCurrentAction = async () => undefined;
switch (state) {
case LnForGasSwapState.PR_CREATED: {
const quoteValid = await this._verifyQuoteValid();
lightningPaymentStatus = quoteValid ? "awaiting" : "soft_expired";
if (quoteValid) {
buildCurrentAction = this._buildLightningPaymentAction.bind(this);
}
break;
}
case LnForGasSwapState.EXPIRED:
lightningPaymentStatus = "expired";
break;
case LnForGasSwapState.PR_PAID:
lightningPaymentStatus = "received";
destinationSettlementStatus = "waiting_lp";
buildCurrentAction = this._buildWaitLpAction.bind(this);
break;
case LnForGasSwapState.FAILED:
lightningPaymentStatus = "expired";
destinationSettlementStatus = "expired";
break;
case LnForGasSwapState.FINISHED:
lightningPaymentStatus = "confirmed";
destinationSettlementStatus = "settled";
break;
}
return {
steps: [
{
type: "Payment",
side: "source",
chain: "LIGHTNING",
title: "Lightning payment",
description: "Pay the Lightning network invoice to initiate the swap",
status: lightningPaymentStatus
},
{
type: "Settlement",
side: "destination",
chain: this.chainIdentifier,
title: "Destination payout",
description: "Wait for the intermediary to send the gas tokens on the destination smart chain",
status: destinationSettlementStatus
}
],
buildCurrentAction,
state
};
}
/**
* @internal
* @inheritDoc
*/
_submitExecutionTransactions() {
throw new Error("Invalid swap state for transaction submission!");
}
/**
* @internal
*/
async _buildLightningPaymentAction() {
return {
type: "SendToAddress",
name: "Deposit on Lightning",
description: "Pay the lightning network invoice to initiate the swap",
chain: "LIGHTNING",
txs: [{
type: "BOLT11_PAYMENT_REQUEST",
address: this.pr,
hyperlink: this.getHyperlink(),
amount: this.getInput()
}],
waitForTransactions: async (maxWaitTimeSeconds, pollIntervalSeconds, abortSignal) => {
const abortController = (0, Utils_1.extendAbortController)(abortSignal, maxWaitTimeSeconds, "Timed out waiting for lightning payment");
let lightningTxId;
try {
const success = await this.waitForPayment(pollIntervalSeconds, abortController.signal, (txId) => {
lightningTxId = txId;
abortController.abort();
});
if (!success)
throw new Error("Quote expired while waiting for lightning payment");
}
catch (e) {
if (lightningTxId != null)
return lightningTxId;
throw e;
}
return this.getInputTxId();
}
};
}
/**
* @internal
*/
async _buildWaitLpAction() {
return {
type: "Wait",
name: "Awaiting LP payout",
description: "Wait for the intermediary to send the gas tokens on the destination smart chain",
pollTimeSeconds: 5,
expectedTimeSeconds: 10,
wait: async (maxWaitTimeSeconds, pollIntervalSeconds, abortSignal) => {
const abortController = (0, Utils_1.extendAbortController)(abortSignal, maxWaitTimeSeconds, "Timed out waiting for LP payout");
await this.waitForPayment(pollIntervalSeconds, abortController.signal);
}
};
}
/**
* @inheritDoc
*/
async getExecutionAction() {
const executionStatus = await this._getExecutionStatus();
return executionStatus.buildCurrentAction();
}
/**
* @inheritDoc
*/
async getExecutionStatus(options) {
const executionStatus = await this._getExecutionStatus();
return {
steps: executionStatus.steps,
currentAction: options?.skipBuildingAction ? undefined : await executionStatus.buildCurrentAction(),
stateInfo: this._getStateInfo(executionStatus.state)
};
}
/**
* @inheritDoc
*/
async getExecutionSteps() {
return (await this._getExecutionStatus()).steps;
}
/**
* Queries the intermediary (LP) node for the state of the swap
*
* @param save Whether the save the result or not
*
* @returns Whether the swap was successful as `boolean` or `null` if the swap is still pending
* @internal
*/
async checkInvoicePaid(save = true) {
if (this._state === LnForGasSwapState.FAILED || this._state === LnForGasSwapState.EXPIRED)
return false;
if (this._state === LnForGasSwapState.FINISHED)
return true;
if (this.url == null)
return false;
const decodedPR = (0, bolt11_1.decode)(this.pr);
const paymentHash = decodedPR.tagsObject.payment_hash;
if (paymentHash == null)
throw new Error("Invalid swap invoice, payment hash not found!");
const response = await this.wrapper._lpApi.getTrustedInvoiceStatus(this.url, paymentHash, this.wrapper._options.getRequestTimeout);
this.logger.debug("checkInvoicePaid(): LP response: ", response);
switch (response.code) {
case IntermediaryAPI_1.TrustedInvoiceStatusResponseCodes.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 IntermediaryAPI_1.TrustedInvoiceStatusResponseCodes.EXPIRED:
if (this._state === LnForGasSwapState.PR_CREATED) {
this._state = LnForGasSwapState.EXPIRED;
}
else {
this._state = LnForGasSwapState.FAILED;
}
if (save)
await this._saveAndEmit();
return false;
case IntermediaryAPI_1.TrustedInvoiceStatusResponseCodes.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 IntermediaryAPI_1.TrustedInvoiceStatusResponseCodes.PENDING:
if (this._state === LnForGasSwapState.PR_CREATED) {
this._state = LnForGasSwapState.PR_PAID;
if (save)
await this._saveAndEmit();
}
return null;
case IntermediaryAPI_1.TrustedInvoiceStatusResponseCodes.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. The swap must be in {@link LnForGasSwapState.PR_CREATED} or
* {@link LnForGasSwapState.PR_PAID} state!
*
* @param checkIntervalSeconds How often to poll the intermediary for answer (default 5 seconds)
* @param abortSignal Abort signal
* @param onPaymentReceived Callback as for when the LP reports having received the ln payment
* @throws {Error} When in invalid state (not PR_CREATED)
*/
async waitForPayment(checkIntervalSeconds, abortSignal, onPaymentReceived) {
if (this._state !== LnForGasSwapState.PR_CREATED && this._state !== LnForGasSwapState.PR_PAID)
throw new Error("Must be in PR_CREATED or PR_PAID 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_PAID) {
if (onPaymentReceived != null) {
onPaymentReceived(this.getInputTxId());
onPaymentReceived = undefined; // Set to null so it only triggers once
}
}
if (this._state === LnForGasSwapState.PR_CREATED || this._state === LnForGasSwapState.PR_PAID)
await (0, TimeoutUtils_1.timeoutPromise)((checkIntervalSeconds ?? 5) * 1000, abortSignal);
}
if (abortSignal != null)
abortSignal.throwIfAborted();
if (this.isFailed())
throw new Error("Swap failed");
return !this.isQuoteExpired();
}
//////////////////////////////
//// Storage
/**
* @inheritDoc
*/
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
};
}
/**
* @inheritDoc
* @internal
*/
_getInitiator() {
return this.recipient;
}
//////////////////////////////
//// Swap ticks & sync
/**
* @inheritDoc
* @internal
*/
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;
}
/**
* @inheritDoc
* @internal
*/
_tick(save) {
return Promise.resolve(false);
}
}
exports.LnForGasSwap = LnForGasSwap;