@atomiqlabs/sdk
Version:
atomiq labs SDK for cross-chain swaps between smart chains and bitcoin
433 lines (432 loc) • 22.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FromBTCLNWrapper = void 0;
const FromBTCLNSwap_1 = require("./FromBTCLNSwap");
const bolt11_1 = require("@atomiqlabs/bolt11");
const base_1 = require("@atomiqlabs/base");
const Intermediary_1 = require("../../../../intermediaries/Intermediary");
const buffer_1 = require("buffer");
const UserError_1 = require("../../../../errors/UserError");
const IntermediaryError_1 = require("../../../../errors/IntermediaryError");
const SwapType_1 = require("../../../../enums/SwapType");
const Utils_1 = require("../../../../utils/Utils");
const RequestError_1 = require("../../../../errors/RequestError");
const IFromBTCLNWrapper_1 = require("../IFromBTCLNWrapper");
const RetryUtils_1 = require("../../../../utils/RetryUtils");
const sha2_1 = require("@noble/hashes/sha2");
/**
* Legacy escrow (HTLC) based swap for Bitcoin Lightning -> Smart chains, requires manual settlement
* of the swap on the destination network once the lightning network payment is received by the LP.
*
* @category Swaps/Legacy/Lightning → Smart chain
*/
class FromBTCLNWrapper extends IFromBTCLNWrapper_1.IFromBTCLNWrapper {
/**
* @param chainIdentifier
* @param unifiedStorage Storage interface for the current environment
* @param unifiedChainEvents On-chain event listener
* @param chain
* @param prices Swap pricing handler
* @param tokens
* @param versionedContracts
* @param lnApi
* @param lpApi
* @param options
* @param events Instance to use for emitting events
*/
constructor(chainIdentifier, unifiedStorage, unifiedChainEvents, chain, prices, tokens, versionedContracts, lnApi, lpApi, options, events) {
super(chainIdentifier, unifiedStorage, unifiedChainEvents, chain, prices, tokens, versionedContracts, lnApi, lpApi, {
...options,
safetyFactor: options?.safetyFactor ?? 2,
bitcoinBlocktime: options?.bitcoinBlocktime ?? 10 * 60,
unsafeSkipLnNodeCheck: options?.unsafeSkipLnNodeCheck ?? false
}, events);
this.TYPE = SwapType_1.SwapType.FROM_BTCLN;
/**
* @internal
*/
this.tickSwapState = [
FromBTCLNSwap_1.FromBTCLNSwapState.PR_CREATED,
FromBTCLNSwap_1.FromBTCLNSwapState.PR_PAID,
FromBTCLNSwap_1.FromBTCLNSwapState.CLAIM_COMMITED
];
/**
* @internal
*/
this._pendingSwapStates = [
FromBTCLNSwap_1.FromBTCLNSwapState.PR_CREATED,
FromBTCLNSwap_1.FromBTCLNSwapState.QUOTE_SOFT_EXPIRED,
FromBTCLNSwap_1.FromBTCLNSwapState.PR_PAID,
FromBTCLNSwap_1.FromBTCLNSwapState.CLAIM_COMMITED,
FromBTCLNSwap_1.FromBTCLNSwapState.EXPIRED
];
/**
* @internal
*/
this._claimableSwapStates = [FromBTCLNSwap_1.FromBTCLNSwapState.CLAIM_COMMITED];
/**
* @internal
*/
this._swapDeserializer = FromBTCLNSwap_1.FromBTCLNSwap;
}
/**
* @inheritDoc
* @internal
*/
processEventInitialize(swap, event) {
if (swap._state === FromBTCLNSwap_1.FromBTCLNSwapState.PR_PAID || swap._state === FromBTCLNSwap_1.FromBTCLNSwapState.QUOTE_SOFT_EXPIRED) {
swap._state = FromBTCLNSwap_1.FromBTCLNSwapState.CLAIM_COMMITED;
return Promise.resolve(true);
}
return Promise.resolve(false);
}
/**
* @inheritDoc
* @internal
*/
processEventClaim(swap, event) {
if (swap._state !== FromBTCLNSwap_1.FromBTCLNSwapState.FAILED && swap._state !== FromBTCLNSwap_1.FromBTCLNSwapState.CLAIM_CLAIMED) {
swap._state = FromBTCLNSwap_1.FromBTCLNSwapState.CLAIM_CLAIMED;
swap._setSwapSecret(event.result);
return Promise.resolve(true);
}
return Promise.resolve(false);
}
/**
* @inheritDoc
* @internal
*/
processEventRefund(swap, event) {
if (swap._state !== FromBTCLNSwap_1.FromBTCLNSwapState.CLAIM_CLAIMED && swap._state !== FromBTCLNSwap_1.FromBTCLNSwapState.FAILED) {
swap._state = FromBTCLNSwap_1.FromBTCLNSwapState.FAILED;
return Promise.resolve(true);
}
return Promise.resolve(false);
}
/**
* Verifies response returned from intermediary
*
* @param resp Response as returned by the intermediary
* @param amountData
* @param lp Intermediary
* @param options Options as passed to the swap creation function
* @param decodedPr Decoded bolt11 lightning network invoice
* @param paymentHash Expected payment hash of the bolt11 lightning network invoice
*
* @throws {IntermediaryError} in case the response is invalid
*
* @private
*/
verifyReturnedData(resp, amountData, lp, options, decodedPr, paymentHash) {
if (lp.getAddress(this.chainIdentifier) !== resp.intermediaryKey)
throw new IntermediaryError_1.IntermediaryError("Invalid intermediary address/pubkey");
if (options.descriptionHash != null && decodedPr.tagsObject.purpose_commit_hash !== options.descriptionHash.toString("hex"))
throw new IntermediaryError_1.IntermediaryError("Invalid pr returned - description hash");
if (options.description != null && decodedPr.tagsObject.description !== options.description)
throw new IntermediaryError_1.IntermediaryError("Invalid pr returned - description");
if (decodedPr.tagsObject.payment_hash == null ||
!buffer_1.Buffer.from(decodedPr.tagsObject.payment_hash, "hex").equals(paymentHash))
throw new IntermediaryError_1.IntermediaryError("Invalid pr returned - payment hash");
if (decodedPr.millisatoshis == null)
throw new IntermediaryError_1.IntermediaryError("Invalid pr returned - msat field");
if (!amountData.exactIn) {
if (resp.total != amountData.amount)
throw new IntermediaryError_1.IntermediaryError("Invalid amount returned");
}
else {
const amountIn = (BigInt(decodedPr.millisatoshis) + 999n) / 1000n;
if (amountIn !== amountData.amount)
throw new IntermediaryError_1.IntermediaryError("Invalid payment request returned, amount mismatch");
}
}
/**
* Returns a newly created legacy Lightning -> Smart chain swap using the HTLC based escrow swap protocol,
* where the user needs to manually settle swap on the destination smart chain. The user has to pay
* a bolt11 invoice on the input lightning network side.
*
* @param recipient Smart chain signer's address on the destination chain, that will have to manually
* settle the swap.
* @param amountData Amount, token and exact input/output data for to swap
* @param lps An array of intermediaries (LPs) to get the quotes from
* @param options Optional additional quote options
* @param additionalParams Optional additional parameters sent to the LP when creating the swap
* @param abortSignal Abort signal
* @param preFetches Optional pre-fetches for speeding up the quoting process (mainly used internally)
*/
create(recipient, amountData, lps, options, additionalParams, abortSignal, preFetches) {
if (!this.isInitialized)
throw new Error("Not initialized, call init() first!");
const _options = {
paymentHash: (0, Utils_1.parseHashValueExact32Bytes)(options?.paymentHash, "payment hash"),
description: options?.description,
descriptionHash: (0, Utils_1.parseHashValueExact32Bytes)(options?.descriptionHash, "description hash"),
unsafeSkipLnNodeCheck: options?.unsafeSkipLnNodeCheck ?? this._options.unsafeSkipLnNodeCheck
};
if (_options.description != null && buffer_1.Buffer.byteLength(_options.description, "utf8") > 500)
throw new UserError_1.UserError("Invalid description length");
const lpVersions = Intermediary_1.Intermediary.getContractVersionsForLps(this.chainIdentifier, lps);
let secret;
let paymentHash;
if (_options.paymentHash != null) {
paymentHash = _options.paymentHash;
}
else {
({ secret, paymentHash } = this.getSecretAndHash());
}
const _hash = (0, Utils_1.mapArrayToObject)(lpVersions, (contractVersion) => {
return this._contract(contractVersion).getHashForHtlc(paymentHash).toString("hex");
});
const nativeTokenAddress = this._chain.getNativeCurrencyAddress();
const _abortController = (0, Utils_1.extendAbortController)(abortSignal);
const _preFetches = preFetches ?? {
pricePrefetchPromise: this.preFetchPrice(amountData, _abortController.signal),
feeRatePromise: this.preFetchFeeRate(recipient, amountData, _hash, _abortController, lpVersions),
usdPricePrefetchPromise: this.preFetchUsdPrice(_abortController.signal),
};
return lps.map(lp => {
return {
intermediary: lp,
quote: (async () => {
if (lp.services[SwapType_1.SwapType.FROM_BTCLN] == null)
throw new Error("LP service for processing from btcln swaps not found!");
const version = lp.getContractVersion(this.chainIdentifier);
const abortController = (0, Utils_1.extendAbortController)(_abortController.signal);
const liquidityPromise = this.preFetchIntermediaryLiquidity(amountData, lp, abortController, version);
const { lnCapacityPromise, resp } = await (0, RetryUtils_1.tryWithRetries)(async (retryCount) => {
const { lnPublicKey, response } = this._lpApi.initFromBTCLN(this.chainIdentifier, lp.url, nativeTokenAddress, {
paymentHash,
amount: amountData.amount,
claimer: recipient,
token: amountData.token.toString(),
description: _options.description,
descriptionHash: _options.descriptionHash,
exactOut: !amountData.exactIn,
feeRate: (0, Utils_1.throwIfUndefined)(_preFetches.feeRatePromise[version], "Network fee rate pre-fetch failed!"),
additionalParams
}, this._options.postRequestTimeout, abortController.signal, retryCount > 0 ? false : undefined);
let lnCapacityPromise;
if (!_options.unsafeSkipLnNodeCheck) {
lnCapacityPromise = this.preFetchLnCapacity(lnPublicKey);
}
else
lnPublicKey.catch(() => { });
return {
lnCapacityPromise,
resp: await response
};
}, undefined, RequestError_1.RequestError, abortController.signal);
const decodedPr = (0, bolt11_1.decode)(resp.pr);
if (decodedPr.millisatoshis == null)
throw new IntermediaryError_1.IntermediaryError("Invalid returned swap invoice, no msat amount field");
if (decodedPr.timeExpireDate == null)
throw new IntermediaryError_1.IntermediaryError("Invalid returned swap invoice, no expiry date field");
const amountIn = (BigInt(decodedPr.millisatoshis) + 999n) / 1000n;
const swapFeeBtc = resp.swapFee * amountIn / (resp.total + resp.swapFee);
try {
this.verifyReturnedData(resp, amountData, lp, _options, decodedPr, paymentHash);
const [pricingInfo] = await Promise.all([
this.verifyReturnedPrice(lp.services[SwapType_1.SwapType.FROM_BTCLN], false, amountIn, resp.total, amountData.token, { swapFeeBtc }, _preFetches.pricePrefetchPromise, _preFetches.usdPricePrefetchPromise, abortController.signal),
this.verifyIntermediaryLiquidity(resp.total, (0, Utils_1.throwIfUndefined)(liquidityPromise, "LP liquidity pre-fetch failed!")),
lnCapacityPromise != null ? this.verifyLnNodeCapacity(lp, decodedPr, lnCapacityPromise, abortController.signal) : Promise.resolve()
]);
const quote = new FromBTCLNSwap_1.FromBTCLNSwap(this, {
pricingInfo,
url: lp.url,
expiry: decodedPr.timeExpireDate * 1000,
swapFee: resp.swapFee,
swapFeeBtc,
feeRate: (await _preFetches.feeRatePromise[version]),
initialSwapData: await this._contract(version).createSwapData(base_1.ChainSwapType.HTLC, lp.getAddress(this.chainIdentifier), recipient, amountData.token, resp.total, _hash[version], this.getRandomSequence(), BigInt(Math.floor(Date.now() / 1000)), false, true, resp.securityDeposit, 0n, nativeTokenAddress),
pr: resp.pr,
secret: secret?.toString("hex"),
exactIn: amountData.exactIn ?? true,
contractVersion: version
});
return quote;
}
catch (e) {
abortController.abort(e);
throw e;
}
})()
};
});
}
/**
* Returns a newly created legacy Lightning -> Smart chain swap using the HTLC based escrow swap protocol,
* where the user needs to manually settle swap on the destination smart chain. The swap is created
* with an LNURL-withdraw link which will be used to pay the generated bolt11 invoice automatically
* when {@link FromBTCLNSwap.waitForPayment} is called on the swap.
*
* @param recipient Smart chain signer's address on the destination chain, that will have to manually
* settle the swap.
* @param lnurl LNURL-withdraw link to pull the funds from
* @param amountData Amount, token and exact input/output data for to swap
* @param lps An array of intermediaries (LPs) to get the quotes from
* @param options Optional additional quote options
* @param additionalParams Optional additional parameters sent to the LP when creating the swap
* @param abortSignal Abort signal
*/
async createViaLNURL(recipient, lnurl, amountData, lps, options, additionalParams, abortSignal) {
if (!this.isInitialized)
throw new Error("Not initialized, call init() first!");
const _options = {
paymentHash: (0, Utils_1.parseHashValueExact32Bytes)(options?.paymentHash, "payment hash"),
description: options?.description,
descriptionHash: (0, Utils_1.parseHashValueExact32Bytes)(options?.descriptionHash, "description hash"),
unsafeSkipLnNodeCheck: options?.unsafeSkipLnNodeCheck ?? this._options.unsafeSkipLnNodeCheck
};
const lpVersions = Intermediary_1.Intermediary.getContractVersionsForLps(this.chainIdentifier, lps);
const abortController = (0, Utils_1.extendAbortController)(abortSignal);
const preFetches = {
pricePrefetchPromise: this.preFetchPrice(amountData, abortController.signal),
usdPricePrefetchPromise: this.preFetchUsdPrice(abortController.signal),
feeRatePromise: this.preFetchFeeRate(recipient, amountData, undefined, abortController, lpVersions)
};
try {
const exactOutAmountPromise = !amountData.exactIn ? preFetches.pricePrefetchPromise.then(price => this._prices.getToBtcSwapAmount(this.chainIdentifier, amountData.amount, amountData.token, abortController.signal, price)).catch(e => {
abortController.abort(e);
return undefined;
}) : undefined;
const withdrawRequest = await this.getLNURLWithdraw(lnurl, abortController.signal);
const min = BigInt(withdrawRequest.minWithdrawable) / 1000n;
const max = BigInt(withdrawRequest.maxWithdrawable) / 1000n;
if (amountData.exactIn) {
if (amountData.amount < min)
throw new UserError_1.UserError("Amount less than LNURL-withdraw minimum");
if (amountData.amount > max)
throw new UserError_1.UserError("Amount more than LNURL-withdraw maximum");
}
else {
const amount = (await exactOutAmountPromise);
abortController.signal.throwIfAborted();
if ((amount * 95n / 100n) < min)
throw new UserError_1.UserError("Amount less than LNURL-withdraw minimum");
if ((amount * 105n / 100n) > max)
throw new UserError_1.UserError("Amount more than LNURL-withdraw maximum");
}
return this.create(recipient, amountData, lps, _options, additionalParams, abortSignal, preFetches).map(data => {
return {
quote: data.quote.then(quote => {
quote._setLNURLData(withdrawRequest.url, withdrawRequest.k1, withdrawRequest.callback);
const amountIn = quote.getInput().rawAmount;
if (amountIn < min)
throw new UserError_1.UserError("Amount less than LNURL-withdraw minimum");
if (amountIn > max)
throw new UserError_1.UserError("Amount more than LNURL-withdraw maximum");
return quote;
}),
intermediary: data.intermediary
};
});
}
catch (e) {
abortController.abort(e);
throw e;
}
}
/**
* @inheritDoc
* @internal
*/
async _checkPastSwaps(pastSwaps) {
const changedSwapSet = new Set();
const swapExpiredStatus = {};
const checkStatusSwaps = {};
await Promise.all(pastSwaps.map(async (pastSwap) => {
if (pastSwap._shouldCheckIntermediary()) {
try {
const result = await pastSwap._checkIntermediaryPaymentReceived(false);
if (result != null) {
changedSwapSet.add(pastSwap);
}
}
catch (e) {
this.logger.error(`_checkPastSwaps(): Failed to contact LP regarding swap ${pastSwap.getId()}, error: `, e);
}
}
if (pastSwap._shouldFetchExpiryStatus()) {
//Check expiry
swapExpiredStatus[pastSwap.getId()] = await pastSwap._verifyQuoteDefinitelyExpired();
}
if (pastSwap._shouldFetchOnchainState()) {
//Add to swaps for which status should be checked
if (pastSwap._data != null)
(checkStatusSwaps[pastSwap._contractVersion ?? "v1"] ??= []).push(pastSwap);
}
}));
for (let version in checkStatusSwaps) {
if (this._versionedContracts[version] == null) {
this.logger.warn(`_checkPastSwaps(): No contract was found for ${this.chainIdentifier} version ${version}! Skipping these swaps!`);
continue;
}
const _checkStatusSwap = checkStatusSwaps[version];
const swapStatuses = await this._contract(version).getCommitStatuses(_checkStatusSwap.map(val => ({ signer: val._getInitiator(), swapData: val._data })));
for (let pastSwap of _checkStatusSwap) {
const shouldSave = await pastSwap._sync(false, swapExpiredStatus[pastSwap.getId()], swapStatuses[pastSwap.getEscrowHash()], true);
if (shouldSave) {
changedSwapSet.add(pastSwap);
}
}
}
const changedSwaps = [];
const removeSwaps = [];
changedSwapSet.forEach(val => {
if (val.isQuoteExpired()) {
removeSwaps.push(val);
}
else {
changedSwaps.push(val);
}
});
return {
changedSwaps,
removeSwaps
};
}
/**
* @inheritDoc
* @internal
*/
async recoverFromSwapDataAndState(init, state, contractVersion, lp) {
const data = init.data;
let paymentHash = data.getHTLCHashHint();
let secret;
if (state.type === base_1.SwapCommitStateType.PAID) {
secret = await state.getClaimResult();
paymentHash = buffer_1.Buffer.from((0, sha2_1.sha256)(buffer_1.Buffer.from(secret, "hex"))).toString("hex");
}
const swapInit = {
pricingInfo: {
isValid: true,
satsBaseFee: 0n,
swapPriceUSatPerToken: 100000000000000n,
realPriceUSatPerToken: 100000000000000n,
differencePPM: 0n,
feePPM: 0n,
},
url: lp?.url,
expiry: 0,
swapFee: 0n,
swapFeeBtc: 0n,
feeRate: "",
signatureData: undefined,
initialSwapData: data,
data,
pr: paymentHash ?? undefined,
secret,
exactIn: false,
contractVersion
};
const swap = new FromBTCLNSwap_1.FromBTCLNSwap(this, swapInit);
swap._commitTxId = await init.getInitTxId();
const blockData = await init.getTxBlock();
swap.createdAt = blockData.blockTime * 1000;
swap._setInitiated();
swap._state = FromBTCLNSwap_1.FromBTCLNSwapState.CLAIM_COMMITED;
await swap._sync(false, false, state);
await swap._save();
return swap;
}
}
exports.FromBTCLNWrapper = FromBTCLNWrapper;