@atomiqlabs/sdk-lib
Version:
Basic SDK functionality library for atomiq
659 lines (658 loc) • 33 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FromBTCLNSwap = exports.isFromBTCLNSwapInit = exports.FromBTCLNSwapState = void 0;
const bolt11_1 = require("@atomiqlabs/bolt11");
const IFromBTCSwap_1 = require("../IFromBTCSwap");
const SwapType_1 = require("../../SwapType");
const base_1 = require("@atomiqlabs/base");
const ISwap_1 = require("../../ISwap");
const buffer_1 = require("buffer");
const LNURL_1 = require("../../../utils/LNURL");
const UserError_1 = require("../../../errors/UserError");
const IntermediaryAPI_1 = require("../../../intermediaries/IntermediaryAPI");
const IntermediaryError_1 = require("../../../errors/IntermediaryError");
const PaymentAuthError_1 = require("../../../errors/PaymentAuthError");
const Utils_1 = require("../../../utils/Utils");
const Tokens_1 = require("../../Tokens");
var FromBTCLNSwapState;
(function (FromBTCLNSwapState) {
FromBTCLNSwapState[FromBTCLNSwapState["FAILED"] = -4] = "FAILED";
FromBTCLNSwapState[FromBTCLNSwapState["QUOTE_EXPIRED"] = -3] = "QUOTE_EXPIRED";
FromBTCLNSwapState[FromBTCLNSwapState["QUOTE_SOFT_EXPIRED"] = -2] = "QUOTE_SOFT_EXPIRED";
FromBTCLNSwapState[FromBTCLNSwapState["EXPIRED"] = -1] = "EXPIRED";
FromBTCLNSwapState[FromBTCLNSwapState["PR_CREATED"] = 0] = "PR_CREATED";
FromBTCLNSwapState[FromBTCLNSwapState["PR_PAID"] = 1] = "PR_PAID";
FromBTCLNSwapState[FromBTCLNSwapState["CLAIM_COMMITED"] = 2] = "CLAIM_COMMITED";
FromBTCLNSwapState[FromBTCLNSwapState["CLAIM_CLAIMED"] = 3] = "CLAIM_CLAIMED";
})(FromBTCLNSwapState = exports.FromBTCLNSwapState || (exports.FromBTCLNSwapState = {}));
function isFromBTCLNSwapInit(obj) {
return typeof obj.pr === "string" &&
typeof obj.secret === "string" &&
(obj.lnurl == null || typeof (obj.lnurl) === "string") &&
(obj.lnurlK1 == null || typeof (obj.lnurlK1) === "string") &&
(obj.lnurlCallback == null || typeof (obj.lnurlCallback) === "string") &&
(0, ISwap_1.isISwapInit)(obj);
}
exports.isFromBTCLNSwapInit = isFromBTCLNSwapInit;
class FromBTCLNSwap extends IFromBTCSwap_1.IFromBTCSwap {
getSwapData() {
return this.data ?? this.initialSwapData;
}
constructor(wrapper, initOrObject) {
if (isFromBTCLNSwapInit(initOrObject))
initOrObject.url += "/frombtcln";
super(wrapper, initOrObject);
this.inputToken = Tokens_1.BitcoinTokens.BTCLN;
this.TYPE = SwapType_1.SwapType.FROM_BTCLN;
this.lnurlFailSignal = new AbortController();
this.prPosted = false;
if (isFromBTCLNSwapInit(initOrObject)) {
this.state = FromBTCLNSwapState.PR_CREATED;
}
else {
this.pr = initOrObject.pr;
this.secret = initOrObject.secret;
this.initialSwapData = initOrObject.initialSwapData == null ? null : base_1.SwapData.deserialize(initOrObject.initialSwapData);
this.lnurl = initOrObject.lnurl;
this.lnurlK1 = initOrObject.lnurlK1;
this.lnurlCallback = initOrObject.lnurlCallback;
this.prPosted = initOrObject.prPosted;
if (this.state === FromBTCLNSwapState.PR_CREATED && this.data != null) {
this.initialSwapData = this.data;
delete this.data;
}
}
this.tryCalculateSwapFee();
this.logger = (0, Utils_1.getLogger)("FromBTCLN(" + this.getIdentifierHashString() + "): ");
}
upgradeVersion() {
if (this.version == null) {
switch (this.state) {
case -2:
this.state = FromBTCLNSwapState.QUOTE_EXPIRED;
break;
case -1:
this.state = FromBTCLNSwapState.FAILED;
break;
case 0:
this.state = FromBTCLNSwapState.PR_CREATED;
break;
case 1:
this.state = FromBTCLNSwapState.PR_PAID;
break;
case 2:
this.state = FromBTCLNSwapState.CLAIM_COMMITED;
break;
case 3:
this.state = FromBTCLNSwapState.CLAIM_CLAIMED;
break;
}
this.version = 1;
}
}
//////////////////////////////
//// Getters & utils
getInputTxId() {
return this.getPaymentHash().toString("hex");
}
getIdentifierHash() {
const paymentHashBuffer = this.getPaymentHash();
if (this.randomNonce == null)
return paymentHashBuffer;
return buffer_1.Buffer.concat([paymentHashBuffer, buffer_1.Buffer.from(this.randomNonce, "hex")]);
}
getPaymentHash() {
if (this.pr == null)
return null;
const decodedPR = (0, bolt11_1.decode)(this.pr);
return buffer_1.Buffer.from(decodedPR.tagsObject.payment_hash, "hex");
}
getAddress() {
return this.pr;
}
/**
* Returns the lightning network BOLT11 invoice that needs to be paid as an input to the swap
*/
getLightningInvoice() {
return this.pr;
}
getQrData() {
return "lightning:" + this.getLightningInvoice().toUpperCase();
}
/**
* Returns timeout time (in UNIX milliseconds) when the LN invoice will expire
*/
getTimeoutTime() {
if (this.pr == null)
return null;
const decoded = (0, bolt11_1.decode)(this.pr);
return (decoded.timeExpireDate * 1000);
}
/**
* Returns timeout time (in UNIX milliseconds) when the on-chain address will expire and no funds should be sent
* to that address anymore
*/
getHtlcTimeoutTime() {
return Number(this.wrapper.getHtlcTimeout(this.data)) * 1000;
}
isFinished() {
return this.state === FromBTCLNSwapState.CLAIM_CLAIMED || this.state === FromBTCLNSwapState.QUOTE_EXPIRED || this.state === FromBTCLNSwapState.FAILED;
}
isClaimable() {
return this.state === FromBTCLNSwapState.PR_PAID || this.state === FromBTCLNSwapState.CLAIM_COMMITED;
}
isSuccessful() {
return this.state === FromBTCLNSwapState.CLAIM_CLAIMED;
}
isFailed() {
return this.state === FromBTCLNSwapState.FAILED || this.state === FromBTCLNSwapState.EXPIRED;
}
isQuoteExpired() {
return this.state === FromBTCLNSwapState.QUOTE_EXPIRED;
}
isQuoteSoftExpired() {
return this.state === FromBTCLNSwapState.QUOTE_EXPIRED || this.state === FromBTCLNSwapState.QUOTE_SOFT_EXPIRED;
}
isQuoteValid() {
if (this.state === FromBTCLNSwapState.PR_CREATED ||
(this.state === FromBTCLNSwapState.QUOTE_SOFT_EXPIRED && this.signatureData == null)) {
return Promise.resolve(this.getTimeoutTime() > Date.now());
}
return super.isQuoteValid();
}
canCommit() {
return this.state === FromBTCLNSwapState.PR_PAID;
}
canClaim() {
return this.state === FromBTCLNSwapState.CLAIM_COMMITED;
}
//////////////////////////////
//// Amounts & fees
getInput() {
const parsed = (0, bolt11_1.decode)(this.pr);
const amount = (BigInt(parsed.millisatoshis) + 999n) / 1000n;
return (0, Tokens_1.toTokenAmount)(amount, this.inputToken, this.wrapper.prices);
}
/**
* Estimated transaction fee for commit & claim txs combined
*/
async getCommitAndClaimFee() {
const swapContract = this.wrapper.contract;
const feeRate = this.feeRate ?? await swapContract.getInitFeeRate(this.getSwapData().getOfferer(), this.getSwapData().getClaimer(), this.getSwapData().getToken(), this.getSwapData().getClaimHash());
const commitFee = await (swapContract.getRawCommitFee != null ?
swapContract.getRawCommitFee(this.getSwapData(), feeRate) :
swapContract.getCommitFee(this.getSwapData(), feeRate));
const claimFee = await (swapContract.getRawClaimFee != null ?
swapContract.getRawClaimFee(this.getInitiator(), this.getSwapData(), feeRate) :
swapContract.getClaimFee(this.getInitiator(), this.getSwapData(), feeRate));
return commitFee + claimFee;
}
async getSmartChainNetworkFee() {
return (0, Tokens_1.toTokenAmount)(await this.getCommitAndClaimFee(), this.wrapper.getNativeToken(), this.wrapper.prices);
}
async hasEnoughForTxFees() {
const [balance, feeRate] = await Promise.all([
this.wrapper.contract.getBalance(this.getInitiator(), this.wrapper.contract.getNativeCurrencyAddress(), false),
this.feeRate != null ? Promise.resolve(this.feeRate) : this.wrapper.contract.getInitFeeRate(this.getSwapData().getOfferer(), this.getSwapData().getClaimer(), this.getSwapData().getToken(), this.getSwapData().getClaimHash())
]);
const commitFee = await this.wrapper.contract.getCommitFee(this.getSwapData(), feeRate);
const claimFee = await this.wrapper.contract.getClaimFee(this.getInitiator(), this.getSwapData(), feeRate);
const totalFee = commitFee + claimFee + this.getSwapData().getTotalDeposit();
return {
enoughBalance: balance >= totalFee,
balance: (0, Tokens_1.toTokenAmount)(balance, this.wrapper.getNativeToken(), this.wrapper.prices),
required: (0, Tokens_1.toTokenAmount)(totalFee, this.wrapper.getNativeToken(), this.wrapper.prices)
};
}
//////////////////////////////
//// Payment
/**
* Waits till an LN payment is received by the intermediary and client can continue commiting & claiming the HTLC
*
* @param abortSignal Abort signal to stop waiting for payment
* @param checkIntervalSeconds How often to poll the intermediary for answer
*/
async waitForPayment(abortSignal, checkIntervalSeconds = 5) {
if (this.state !== FromBTCLNSwapState.PR_CREATED &&
(this.state !== FromBTCLNSwapState.QUOTE_SOFT_EXPIRED || this.signatureData != null))
throw new Error("Must be in PR_CREATED state!");
const abortController = new AbortController();
if (abortSignal != null)
abortSignal.addEventListener("abort", () => abortController.abort(abortSignal.reason));
let save = false;
if (this.lnurl != null && !this.prPosted) {
LNURL_1.LNURL.postInvoiceToLNURLWithdraw({ k1: this.lnurlK1, callback: this.lnurlCallback }, this.pr).catch(e => {
this.lnurlFailSignal.abort(e);
});
this.prPosted = true;
save ||= true;
}
if (!this.initiated) {
this.initiated = true;
save ||= true;
}
if (save)
await this._saveAndEmit();
let lnurlFailListener = () => abortController.abort(this.lnurlFailSignal.signal.reason);
this.lnurlFailSignal.signal.addEventListener("abort", lnurlFailListener);
this.lnurlFailSignal.signal.throwIfAborted();
let resp = { code: IntermediaryAPI_1.PaymentAuthorizationResponseCodes.PENDING, msg: "" };
while (!abortController.signal.aborted && resp.code === IntermediaryAPI_1.PaymentAuthorizationResponseCodes.PENDING) {
resp = await IntermediaryAPI_1.IntermediaryAPI.getPaymentAuthorization(this.url, this.getPaymentHash().toString("hex"));
if (resp.code === IntermediaryAPI_1.PaymentAuthorizationResponseCodes.PENDING)
await (0, Utils_1.timeoutPromise)(checkIntervalSeconds * 1000, abortController.signal);
}
this.lnurlFailSignal.signal.removeEventListener("abort", lnurlFailListener);
abortController.signal.throwIfAborted();
if (resp.code === IntermediaryAPI_1.PaymentAuthorizationResponseCodes.AUTH_DATA) {
const sigData = resp.data;
const swapData = new this.wrapper.swapDataDeserializer(resp.data.data);
await this.checkIntermediaryReturnedAuthData(this.getInitiator(), swapData, sigData);
this.expiry = await (0, Utils_1.tryWithRetries)(() => this.wrapper.contract.getInitAuthorizationExpiry(swapData, sigData));
if (this.state === FromBTCLNSwapState.PR_CREATED || this.state === FromBTCLNSwapState.QUOTE_SOFT_EXPIRED) {
delete this.initialSwapData;
this.data = swapData;
this.signatureData = {
prefix: sigData.prefix,
timeout: sigData.timeout,
signature: sigData.signature
};
await this._saveAndEmit(FromBTCLNSwapState.PR_PAID);
}
return;
}
if (this.state === FromBTCLNSwapState.PR_CREATED || this.state === FromBTCLNSwapState.QUOTE_SOFT_EXPIRED) {
if (resp.code === IntermediaryAPI_1.PaymentAuthorizationResponseCodes.EXPIRED) {
await this._saveAndEmit(FromBTCLNSwapState.QUOTE_EXPIRED);
}
throw new PaymentAuthError_1.PaymentAuthError(resp.msg, resp.code, resp.data);
}
}
/**
* Checks whether the LP received the LN payment and we can continue by committing & claiming the HTLC on-chain
*
* @param save If the new swap state should be saved
*/
async checkIntermediaryPaymentReceived(save = true) {
if (this.state === FromBTCLNSwapState.PR_PAID ||
this.state === FromBTCLNSwapState.CLAIM_COMMITED ||
this.state === FromBTCLNSwapState.CLAIM_CLAIMED ||
this.state === FromBTCLNSwapState.FAILED)
return true;
if (this.state === FromBTCLNSwapState.QUOTE_EXPIRED || (this.state === FromBTCLNSwapState.QUOTE_SOFT_EXPIRED && this.signatureData != null))
return false;
const resp = await IntermediaryAPI_1.IntermediaryAPI.getPaymentAuthorization(this.url, this.getPaymentHash().toString("hex"));
switch (resp.code) {
case IntermediaryAPI_1.PaymentAuthorizationResponseCodes.AUTH_DATA:
const data = new this.wrapper.swapDataDeserializer(resp.data.data);
try {
await this.checkIntermediaryReturnedAuthData(this.getInitiator(), data, resp.data);
this.expiry = await (0, Utils_1.tryWithRetries)(() => this.wrapper.contract.getInitAuthorizationExpiry(data, resp.data));
this.state = FromBTCLNSwapState.PR_PAID;
delete this.initialSwapData;
this.data = data;
this.signatureData = {
prefix: resp.data.prefix,
timeout: resp.data.timeout,
signature: resp.data.signature
};
this.initiated = true;
if (save)
await this._saveAndEmit();
return true;
}
catch (e) { }
return null;
case IntermediaryAPI_1.PaymentAuthorizationResponseCodes.EXPIRED:
this.state = FromBTCLNSwapState.QUOTE_EXPIRED;
this.initiated = true;
if (save)
await this._saveAndEmit();
return false;
default:
return null;
}
}
/**
* Checks the data returned by the intermediary in the payment auth request
*
* @param signer Smart chain signer's address initiating the swap
* @param data Parsed swap data as returned by the intermediary
* @param signature Signature data as returned by the intermediary
* @protected
* @throws {IntermediaryError} If the returned are not valid
* @throws {SignatureVerificationError} If the returned signature is not valid
* @throws {Error} If the swap is already committed on-chain
*/
async checkIntermediaryReturnedAuthData(signer, data, signature) {
data.setClaimer(signer);
if (data.getOfferer() !== this.getSwapData().getOfferer())
throw new IntermediaryError_1.IntermediaryError("Invalid offerer used");
if (!data.isToken(this.getSwapData().getToken()))
throw new IntermediaryError_1.IntermediaryError("Invalid token used");
if (data.getSecurityDeposit() > this.getSwapData().getSecurityDeposit())
throw new IntermediaryError_1.IntermediaryError("Invalid security deposit!");
if (data.getAmount() < this.getSwapData().getAmount())
throw new IntermediaryError_1.IntermediaryError("Invalid amount received!");
if (data.getClaimHash() !== this.getSwapData().getClaimHash())
throw new IntermediaryError_1.IntermediaryError("Invalid payment hash used!");
if (!data.isDepositToken(this.getSwapData().getDepositToken()))
throw new IntermediaryError_1.IntermediaryError("Invalid deposit token used!");
await Promise.all([
(0, Utils_1.tryWithRetries)(() => this.wrapper.contract.isValidInitAuthorization(data, signature, this.feeRate), null, base_1.SignatureVerificationError),
(0, Utils_1.tryWithRetries)(() => this.wrapper.contract.getCommitStatus(data.getClaimer(), data)).then(status => {
if (status !== base_1.SwapCommitStatus.NOT_COMMITED)
throw new Error("Swap already committed on-chain!");
})
]);
}
//////////////////////////////
//// Commit
/**
* Commits the swap on-chain, locking the tokens from the intermediary in an HTLC
*
* @param signer Signer to sign the transactions with, must be the same as used in the initialization
* @param abortSignal Abort signal to stop waiting for the transaction confirmation and abort
* @param skipChecks Skip checks like making sure init signature is still valid and swap wasn't commited yet
* (this is handled when swap is created (quoted), if you commit right after quoting, you can use skipChecks=true)
* @throws {Error} If invalid signer is provided that doesn't match the swap data
*/
async commit(signer, abortSignal, skipChecks) {
this.checkSigner(signer);
const result = await this.wrapper.contract.sendAndConfirm(signer, await this.txsCommit(skipChecks), true, abortSignal);
this.commitTxId = result[0];
if (this.state === FromBTCLNSwapState.PR_PAID || this.state === FromBTCLNSwapState.QUOTE_SOFT_EXPIRED) {
await this._saveAndEmit(FromBTCLNSwapState.CLAIM_COMMITED);
}
return result[0];
}
async waitTillCommited(abortSignal) {
if (this.state === FromBTCLNSwapState.CLAIM_COMMITED || this.state === FromBTCLNSwapState.CLAIM_CLAIMED)
return Promise.resolve();
if (this.state !== FromBTCLNSwapState.PR_PAID && (this.state !== FromBTCLNSwapState.QUOTE_SOFT_EXPIRED && this.signatureData != null))
throw new Error("Invalid state");
const abortController = (0, Utils_1.extendAbortController)(abortSignal);
const result = await Promise.race([
this.watchdogWaitTillCommited(abortController.signal),
this.waitTillState(FromBTCLNSwapState.CLAIM_COMMITED, "gte", abortController.signal).then(() => 0)
]);
abortController.abort();
if (result === 0)
this.logger.debug("waitTillCommited(): Resolved from state changed");
if (result === true)
this.logger.debug("waitTillCommited(): Resolved from watchdog - commited");
if (result === false) {
this.logger.debug("waitTillCommited(): Resolved from watchdog - signature expired");
if (this.state === FromBTCLNSwapState.PR_PAID ||
this.state === FromBTCLNSwapState.QUOTE_SOFT_EXPIRED) {
await this._saveAndEmit(FromBTCLNSwapState.QUOTE_EXPIRED);
}
return;
}
if (this.state === FromBTCLNSwapState.PR_PAID ||
this.state === FromBTCLNSwapState.QUOTE_SOFT_EXPIRED) {
await this._saveAndEmit(FromBTCLNSwapState.CLAIM_COMMITED);
}
}
//////////////////////////////
//// Claim
/**
* Returns transactions required for claiming the HTLC and finishing the swap by revealing the HTLC secret
* (hash preimage)
*
* @param signer Optional signer address to use for claiming the swap, can also be different from the initializer
* @throws {Error} If in invalid state (must be CLAIM_COMMITED)
*/
txsClaim(signer) {
if (this.state !== FromBTCLNSwapState.CLAIM_COMMITED)
throw new Error("Must be in CLAIM_COMMITED state!");
return this.wrapper.contract.txsClaimWithSecret(signer ?? this.getInitiator(), this.data, this.secret, true, true);
}
/**
* Claims and finishes the swap
*
* @param signer Signer to sign the transactions with, can also be different to the initializer
* @param abortSignal Abort signal to stop waiting for transaction confirmation
*/
async claim(signer, abortSignal) {
const result = await this.wrapper.contract.sendAndConfirm(signer, await this.txsClaim(), true, abortSignal);
this.claimTxId = result[0];
if (FromBTCLNSwapState.CLAIM_COMMITED || FromBTCLNSwapState.EXPIRED || FromBTCLNSwapState.FAILED) {
await this._saveAndEmit(FromBTCLNSwapState.CLAIM_CLAIMED);
}
return result[0];
}
/**
* Waits till the swap is successfully claimed
*
* @param abortSignal AbortSignal
* @throws {Error} If swap is in invalid state (must be BTC_TX_CONFIRMED)
* @throws {Error} If the LP refunded sooner than we were able to claim
*/
async waitTillClaimed(abortSignal) {
if (this.state === FromBTCLNSwapState.CLAIM_CLAIMED)
return Promise.resolve();
if (this.state !== FromBTCLNSwapState.CLAIM_COMMITED)
throw new Error("Invalid state (not CLAIM_COMMITED)");
const abortController = new AbortController();
if (abortSignal != null)
abortSignal.addEventListener("abort", () => abortController.abort(abortSignal.reason));
const res = await Promise.race([
this.watchdogWaitTillResult(abortController.signal),
this.waitTillState(FromBTCLNSwapState.CLAIM_CLAIMED, "eq", abortController.signal).then(() => 0),
this.waitTillState(FromBTCLNSwapState.EXPIRED, "eq", abortController.signal).then(() => 1),
]);
abortController.abort();
if (res === 0) {
this.logger.debug("waitTillClaimed(): Resolved from state change (CLAIM_CLAIMED)");
return;
}
if (res === 1) {
this.logger.debug("waitTillClaimed(): Resolved from state change (EXPIRED)");
throw new Error("Swap expired during claiming");
}
this.logger.debug("waitTillClaimed(): Resolved from watchdog");
if (res === base_1.SwapCommitStatus.PAID) {
if (this.state !== FromBTCLNSwapState.CLAIM_CLAIMED)
await this._saveAndEmit(FromBTCLNSwapState.CLAIM_CLAIMED);
}
if (res === base_1.SwapCommitStatus.NOT_COMMITED || res === base_1.SwapCommitStatus.EXPIRED) {
if (this.state !== FromBTCLNSwapState.CLAIM_CLAIMED &&
this.state !== FromBTCLNSwapState.FAILED)
await this._saveAndEmit(FromBTCLNSwapState.FAILED);
}
}
//////////////////////////////
//// Commit & claim
canCommitAndClaimInOneShot() {
return this.wrapper.contract.initAndClaimWithSecret != null;
}
/**
* Commits and claims the swap, in a way that the transactions can be signed together by the underlying provider and
* then sent sequentially
*
* @param signer Signer to sign the transactions with, must be the same as used in the initialization
* @param abortSignal Abort signal to stop waiting for the transaction confirmation and abort
* @param skipChecks Skip checks like making sure init signature is still valid and swap wasn't commited yet
* (this is handled when swap is created (quoted), if you commit right after quoting, you can use skipChecks=true)
* @throws {Error} If in invalid state (must be PR_PAID or CLAIM_COMMITED)
* @throws {Error} If invalid signer is provided that doesn't match the swap data
*/
async commitAndClaim(signer, abortSignal, skipChecks) {
if (!this.canCommitAndClaimInOneShot())
throw new Error("Cannot commitAndClaim in single action, please run commit and claim separately!");
this.checkSigner(signer);
if (this.state === FromBTCLNSwapState.CLAIM_COMMITED)
return [null, await this.claim(signer)];
const result = await this.wrapper.contract.sendAndConfirm(signer, await this.txsCommitAndClaim(skipChecks), true, abortSignal);
this.commitTxId = result[0] || this.commitTxId;
this.claimTxId = result[result.length - 1] || this.claimTxId;
if (this.state !== FromBTCLNSwapState.CLAIM_CLAIMED) {
await this._saveAndEmit(FromBTCLNSwapState.CLAIM_CLAIMED);
}
return result;
}
/**==
* Returns transactions for both commit & claim operation together, such that they can be signed all at once by
* the wallet. CAUTION: transactions must be sent sequentially, such that the claim (2nd) transaction is only
* sent after the commit (1st) transaction confirms. Failure to do so can reveal the HTLC pre-image too soon,
* opening a possibility for the LP to steal funds.
*
* @param skipChecks Skip checks like making sure init signature is still valid and swap wasn't commited yet
* (this is handled when swap is created (quoted), if you commit right after quoting, you can use skipChecks=true)
*
* @throws {Error} If in invalid state (must be PR_PAID or CLAIM_COMMITED)
*/
async txsCommitAndClaim(skipChecks) {
if (this.state === FromBTCLNSwapState.CLAIM_COMMITED)
return await this.txsClaim();
if (this.state !== FromBTCLNSwapState.PR_PAID && (this.state !== FromBTCLNSwapState.QUOTE_SOFT_EXPIRED || this.signatureData == null))
throw new Error("Must be in PR_PAID state!");
const initTxs = await this.txsCommit(skipChecks);
const claimTxs = await this.wrapper.contract.txsClaimWithSecret(this.getInitiator(), this.data, this.secret, true, true, null, true);
return initTxs.concat(claimTxs);
}
//////////////////////////////
//// LNURL
/**
* Is this an LNURL-withdraw swap?
*/
isLNURL() {
return this.lnurl != null;
}
/**
* Gets the used LNURL or null if this is not an LNURL-withdraw swap
*/
getLNURL() {
return this.lnurl;
}
/**
* Pay the generated lightning network invoice with LNURL-withdraw
*/
async settleWithLNURLWithdraw(lnurl) {
if (this.lnurl != null)
throw new Error("Cannot settle LNURL-withdraw swap with different LNURL");
let lnurlParams;
if (typeof (lnurl) === "string") {
const parsedLNURL = await LNURL_1.LNURL.getLNURL(lnurl);
if (parsedLNURL == null || parsedLNURL.tag !== "withdrawRequest")
throw new UserError_1.UserError("Invalid LNURL-withdraw to settle the swap");
lnurlParams = parsedLNURL;
}
else {
lnurlParams = lnurl.params;
}
LNURL_1.LNURL.useLNURLWithdraw(lnurlParams, this.pr).catch(e => this.lnurlFailSignal.abort(e));
this.lnurl = lnurlParams.url;
this.lnurlCallback = lnurlParams.callback;
this.lnurlK1 = lnurlParams.k1;
this.prPosted = true;
await this._saveAndEmit();
}
//////////////////////////////
//// Storage
serialize() {
return {
...super.serialize(),
pr: this.pr,
secret: this.secret,
lnurl: this.lnurl,
lnurlK1: this.lnurlK1,
lnurlCallback: this.lnurlCallback,
prPosted: this.prPosted,
initialSwapData: this.initialSwapData == null ? null : this.initialSwapData.serialize()
};
}
//////////////////////////////
//// Swap ticks & sync
/**
* Checks the swap's state on-chain and compares it to its internal state, updates/changes it according to on-chain
* data
*
* @private
*/
async syncStateFromChain() {
if (this.state === FromBTCLNSwapState.PR_PAID || (this.state === FromBTCLNSwapState.QUOTE_SOFT_EXPIRED && this.signatureData != null)) {
//Check if it's already committed
const status = await (0, Utils_1.tryWithRetries)(() => this.wrapper.contract.getCommitStatus(this.getInitiator(), this.data));
switch (status) {
case base_1.SwapCommitStatus.COMMITED:
this.state = FromBTCLNSwapState.CLAIM_COMMITED;
return true;
case base_1.SwapCommitStatus.EXPIRED:
this.state = FromBTCLNSwapState.QUOTE_EXPIRED;
return true;
case base_1.SwapCommitStatus.PAID:
this.state = FromBTCLNSwapState.CLAIM_CLAIMED;
return true;
}
return false;
}
if (this.state === FromBTCLNSwapState.CLAIM_COMMITED || this.state === FromBTCLNSwapState.EXPIRED) {
//Check if it's already successfully paid
const commitStatus = await (0, Utils_1.tryWithRetries)(() => this.wrapper.contract.getCommitStatus(this.getInitiator(), this.data));
if (commitStatus === base_1.SwapCommitStatus.PAID) {
this.state = FromBTCLNSwapState.CLAIM_CLAIMED;
return true;
}
if (commitStatus === base_1.SwapCommitStatus.NOT_COMMITED || commitStatus === base_1.SwapCommitStatus.EXPIRED) {
this.state = FromBTCLNSwapState.FAILED;
return true;
}
return false;
}
}
async _sync(save) {
let changed = false;
if (this.state === FromBTCLNSwapState.PR_CREATED || (this.state === FromBTCLNSwapState.QUOTE_SOFT_EXPIRED && this.signatureData == null)) {
if (this.getTimeoutTime() < Date.now()) {
this.state = FromBTCLNSwapState.QUOTE_SOFT_EXPIRED;
changed ||= true;
}
const result = await this.checkIntermediaryPaymentReceived(false);
if (result !== null)
changed ||= true;
}
if (await this.syncStateFromChain())
changed = true;
if (this.state === FromBTCLNSwapState.PR_PAID || (this.state === FromBTCLNSwapState.QUOTE_SOFT_EXPIRED && this.signatureData != null)) {
if (!await this.isQuoteValid()) {
this.state = FromBTCLNSwapState.QUOTE_EXPIRED;
changed ||= true;
}
}
if (save && changed)
await this._saveAndEmit();
return changed;
}
async _tick(save) {
switch (this.state) {
case FromBTCLNSwapState.PR_CREATED:
if (this.getTimeoutTime() < Date.now()) {
this.state = FromBTCLNSwapState.QUOTE_SOFT_EXPIRED;
if (save)
await this._saveAndEmit();
return true;
}
break;
case FromBTCLNSwapState.PR_PAID:
if (this.expiry < Date.now()) {
this.state = FromBTCLNSwapState.QUOTE_SOFT_EXPIRED;
if (save)
await this._saveAndEmit();
return true;
}
break;
case FromBTCLNSwapState.CLAIM_COMMITED:
const expired = await this.wrapper.contract.isExpired(this.getInitiator(), this.data);
if (expired) {
this.state = FromBTCLNSwapState.EXPIRED;
if (save)
await this._saveAndEmit();
return true;
}
break;
}
}
}
exports.FromBTCLNSwap = FromBTCLNSwap;