@atomiqlabs/sdk
Version:
atomiq labs SDK for cross-chain swaps between smart chains and bitcoin
753 lines (682 loc) • 36.2 kB
text/typescript
import {decode as bolt11Decode, PaymentRequestObject, TagsObject} from "@atomiqlabs/bolt11";
import {
ChainSwapType,
ChainType,
ClaimEvent,
InitializeEvent, LightningNetworkApi, LNNodeLiquidity, Messenger,
RefundEvent, SwapCommitState, SwapCommitStateType
} from "@atomiqlabs/base";
import {Intermediary} from "../../../../intermediaries/Intermediary";
import {Buffer} from "buffer";
import {UserError} from "../../../../errors/UserError";
import {IntermediaryError} from "../../../../errors/IntermediaryError";
import {SwapType} from "../../../../enums/SwapType";
import {
extendAbortController, mapArrayToObject, parseHashValueExact32Bytes,
randomBytes,
throwIfUndefined
} from "../../../../utils/Utils";
import {
FromBTCLNAutoResponseType,
IntermediaryAPI
} from "../../../../intermediaries/apis/IntermediaryAPI";
import {RequestError} from "../../../../errors/RequestError";
import {ISwapPrice} from "../../../../prices/abstract/ISwapPrice";
import {EventEmitter} from "events";
import {ISwapWrapperOptions, WrapperCtorTokens} from "../../../ISwapWrapper";
import {UnifiedSwapEventListener} from "../../../../events/UnifiedSwapEventListener";
import {UnifiedSwapStorage} from "../../../../storage/UnifiedSwapStorage";
import {ISwap} from "../../../ISwap";
import {FromBTCLNAutoSwap, FromBTCLNAutoSwapInit, FromBTCLNAutoSwapState} from "./FromBTCLNAutoSwap";
import {IFromBTCLNDefinition, IFromBTCLNWrapper} from "../IFromBTCLNWrapper";
import {IClaimableSwapWrapper} from "../../../IClaimableSwapWrapper";
import {AmountData} from "../../../../types/AmountData";
import {LNURLWithdrawParamsWithUrl} from "../../../../types/lnurl/LNURLWithdraw";
import {tryWithRetries} from "../../../../utils/RetryUtils";
import {AllOptional} from "../../../../utils/TypeUtils";
import {sha256} from "@noble/hashes/sha2";
import {fromHumanReadableString} from "../../../../utils/TokenUtils";
export type FromBTCLNAutoOptions = {
/**
* Instead of letting the SDK generate the preimage/paymentHash pair internally you can pass your computed
* paymentHash here, this will create the swap with the provided payment hash. Note that swaps created this way
* won't settle automatically (as the SDK is missing the preimage). Once the HTLC towards the user is created in
* the {@link FromBTCLNAutoSwapState.CLAIM_COMMITED} state, you should pass the secret preimage manually in the
* {@link FromBTCLNAutoSwap.waitTillClaimed}, {@link FromBTCLNAutoSwap.claim} or {@link FromBTCLNAutoSwap.txsClaim}
* functions.
*
* Accepts both, a {@link Buffer} and a hexadecimal `string`
*/
paymentHash?: Buffer | string,
/**
* Optional description to use for the swap lightning network invoice, keep the invoice length below 500 characters
*/
description?: string,
/**
* Optional description hash to use for the lightning network invoice, useful when returning the invoice as part of
* an LNURL-pay service endpoint.
*
* Accepts both, a {@link Buffer} and a hexadecimal `string`
*/
descriptionHash?: Buffer | string,
/**
* Optional additional native token to receive as an output of the swap (e.g. STRK on Starknet or cBTC on Citrea).
* When passed as a `bigint` it is specified in base units of the token and in `string` it is the human readable
* decimal format.
*/
gasAmount?: bigint | string,
/**
* A flag to skip checking whether the lightning network node of the LP has enough channel liquidity to facilitate
* the swap.
*/
unsafeSkipLnNodeCheck?: boolean,
/**
* A flag to attach 0 watchtower fee to the swap, this would make the settlement unattractive for the watchtowers
* and therefore automatic settlement for such swaps will not be possible, you will have to settle manually
* with {@link FromBTCLNSwap.claim} or {@link FromBTCLNSwap.txsClaim} functions.
*/
unsafeZeroWatchtowerFee?: boolean,
/**
* A safety factor to use when estimating the watchtower fee to attach to the swap (this has to cover the gas fee
* of watchtowers settling the swap). A higher multiple here would mean that a swap is more attractive for
* watchtowers to settle automatically.
*
* Uses a `1.25` multiple by default (i.e. the current network fee is multiplied by 1.25 and then used to estimate
* the settlement gas fee cost)
*/
feeSafetyFactor?: number
};
export type FromBTCLNAutoWrapperOptions = ISwapWrapperOptions & {
safetyFactor: number,
bitcoinBlocktime: number,
unsafeSkipLnNodeCheck: boolean
};
export type FromBTCLNAutoDefinition<T extends ChainType> = IFromBTCLNDefinition<T, FromBTCLNAutoWrapper<T>, FromBTCLNAutoSwap<T>>;
/**
* New escrow based (HTLC) swaps for Bitcoin Lightning -> Smart chain swaps not requiring manual settlement on
* the destination by the user, and instead letting the LP initiate the escrow. Permissionless watchtower network
* handles the claiming of HTLC, with the swap secret broadcasted over Nostr. Also adds a possibility for the user
* to receive a native token on the destination chain as part of the swap (a "gas drop" feature).
*
* @category Swaps/Lightning → Smart chain
*/
export class FromBTCLNAutoWrapper<
T extends ChainType
> extends IFromBTCLNWrapper<T, FromBTCLNAutoDefinition<T>, FromBTCLNAutoWrapperOptions> implements IClaimableSwapWrapper<FromBTCLNAutoSwap<T>> {
public readonly TYPE: SwapType.FROM_BTCLN_AUTO = SwapType.FROM_BTCLN_AUTO;
/**
* @internal
*/
protected readonly tickSwapState = [
FromBTCLNAutoSwapState.PR_CREATED,
FromBTCLNAutoSwapState.QUOTE_SOFT_EXPIRED,
FromBTCLNAutoSwapState.PR_PAID,
FromBTCLNAutoSwapState.CLAIM_COMMITED
];
/**
* @internal
*/
readonly _pendingSwapStates = [
FromBTCLNAutoSwapState.PR_CREATED,
FromBTCLNAutoSwapState.QUOTE_SOFT_EXPIRED,
FromBTCLNAutoSwapState.PR_PAID,
FromBTCLNAutoSwapState.CLAIM_COMMITED,
FromBTCLNAutoSwapState.EXPIRED
];
/**
* @internal
*/
readonly _claimableSwapStates = [FromBTCLNAutoSwapState.CLAIM_COMMITED];
/**
* @internal
*/
readonly _swapDeserializer = FromBTCLNAutoSwap;
/**
* @internal
*/
readonly _messenger: Messenger;
/**
* @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 messenger
* @param lpApi
* @param options
* @param events Instance to use for emitting events
*/
constructor(
chainIdentifier: string,
unifiedStorage: UnifiedSwapStorage<T>,
unifiedChainEvents: UnifiedSwapEventListener<T>,
chain: T["ChainInterface"],
prices: ISwapPrice,
tokens: WrapperCtorTokens,
versionedContracts: {
[version: string]: {
swapContract: T["Contract"],
swapDataConstructor: new (data: any) => T["Data"]
}
},
lnApi: LightningNetworkApi,
messenger: Messenger,
lpApi: IntermediaryAPI,
options?: AllOptional<FromBTCLNAutoWrapperOptions>,
events?: EventEmitter<{swapState: [ISwap]}>
) {
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._messenger = messenger;
}
/**
* @inheritDoc
* @internal
*/
protected async processEventInitialize(swap: FromBTCLNAutoSwap<T>, event: InitializeEvent<T["Data"]>): Promise<boolean> {
if(swap._state===FromBTCLNAutoSwapState.PR_PAID || swap._state===FromBTCLNAutoSwapState.PR_CREATED || swap._state===FromBTCLNAutoSwapState.QUOTE_SOFT_EXPIRED) {
if(swap._data==null) {
//Obtain data from the initialize event
const eventData = await event.swapData();
if(eventData==null) {
this.logger.error("processEventInitialize("+swap.getId()+"): Error when fetching swap data for swap, null returned!");
return false;
}
try {
await swap._saveRealSwapData(eventData, false);
this.logger.info("processEventInitialize("+swap.getId()+"): Successfully taken swap data from on-chain event!");
} catch (e) {
this.logger.error("processEventInitialize("+swap.getId()+"): Error when saving swap data for swap: ", e);
return false;
}
}
if(swap._getEscrowHash()!==event.escrowHash) {
this.logger.error("processEventInitialize("+swap.getId()+"): Error when processing event, escrow hashes don't match!");
return false;
}
swap._commitedAt ??= Date.now();
swap._state = FromBTCLNAutoSwapState.CLAIM_COMMITED;
if(swap.hasSecretPreimage()) swap._broadcastSecret().catch(e => {
this.logger.error("processEventInitialize("+swap.getId()+"): Error when broadcasting swap secret: ", e);
});
return true;
}
return false;
}
/**
* @inheritDoc
* @internal
*/
protected processEventClaim(swap: FromBTCLNAutoSwap<T>, event: ClaimEvent<T["Data"]>): Promise<boolean> {
if(swap._state!==FromBTCLNAutoSwapState.FAILED && swap._state!==FromBTCLNAutoSwapState.CLAIM_CLAIMED) {
swap._state = FromBTCLNAutoSwapState.CLAIM_CLAIMED;
swap._setSwapSecret(event.result);
return Promise.resolve(true);
}
return Promise.resolve(false);
}
/**
* @inheritDoc
* @internal
*/
protected processEventRefund(swap: FromBTCLNAutoSwap<T>, event: RefundEvent<T["Data"]>): Promise<boolean> {
if(swap._state!==FromBTCLNAutoSwapState.CLAIM_CLAIMED && swap._state!==FromBTCLNAutoSwapState.FAILED) {
swap._state = FromBTCLNAutoSwapState.FAILED;
return Promise.resolve(true);
}
return Promise.resolve(false);
}
/**
* Pre-fetches claimer (watchtower) bounty data for the swap. Doesn't throw, instead returns null and aborts the
* provided abortController
*
* @param signer Smartchain signer address initiating the swap
* @param amountData
* @param options Options as passed to the swap creation function
* @param abortController
*
* @param contractVersions
* @private
*/
private preFetchClaimerBounty(
signer: string,
amountData: AmountData,
options: {feeSafetyFactor: number, unsafeZeroWatchtowerFee: boolean},
abortController: AbortController,
contractVersions: string[]
): {[chainVersion: string]: Promise<bigint | undefined>} {
return mapArrayToObject(contractVersions, async (contractVersion) => {
if(options.unsafeZeroWatchtowerFee) return 0n;
const dummyAmount = BigInt(Math.floor(Math.random()* 0x1000000));
const dummySwapData = await this._contract(contractVersion).createSwapData(
ChainSwapType.HTLC, this._chain.randomAddress(), signer, amountData.token,
dummyAmount, this._contract(contractVersion).getHashForHtlc(randomBytes(32)).toString("hex"),
this.getRandomSequence(), BigInt(Math.floor(Date.now()/1000)), false, true,
BigInt(Math.floor(Math.random() * 0x10000)), BigInt(Math.floor(Math.random() * 0x10000))
);
try {
const result = await this._contract(contractVersion).getClaimFee(this._chain.randomAddress(), dummySwapData);
return result * BigInt(Math.floor(options.feeSafetyFactor*1000000)) / 1_000_000n
} catch (e) {
abortController.abort(e);
}
});
}
/**
* 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
* @param claimerBounty Claimer bounty as request by the user
*
* @throws {IntermediaryError} in case the response is invalid
*
* @private
*/
private verifyReturnedData(
resp: FromBTCLNAutoResponseType,
amountData: AmountData,
lp: Intermediary,
options: {gasAmount: bigint, description?: string, descriptionHash?: Buffer},
decodedPr: PaymentRequestObject & {tagsObject: TagsObject},
paymentHash: Buffer,
claimerBounty: bigint
): void {
if(lp.getAddress(this.chainIdentifier)!==resp.intermediaryKey) throw new IntermediaryError("Invalid intermediary address/pubkey");
if(options.descriptionHash!=null && decodedPr.tagsObject.purpose_commit_hash!==options.descriptionHash.toString("hex"))
throw new IntermediaryError("Invalid pr returned - description hash");
if(options.description!=null && decodedPr.tagsObject.description!==options.description)
throw new IntermediaryError("Invalid pr returned - description");
if(
decodedPr.tagsObject.payment_hash==null ||
!Buffer.from(decodedPr.tagsObject.payment_hash, "hex").equals(paymentHash)
) throw new IntermediaryError("Invalid pr returned - payment hash");
if(decodedPr.millisatoshis==null) throw new IntermediaryError("Invalid pr returned - msat field");
const amountIn = (BigInt(decodedPr.millisatoshis) + 999n) / 1000n;
if(resp.btcAmountGas + resp.btcAmountSwap !== amountIn) throw new IntermediaryError("Invalid total btc returned");
if(resp.gasSwapFeeBtc + resp.swapFeeBtc !== resp.totalFeeBtc) throw new IntermediaryError("Invalid total btc fee returned");
if(resp.claimerBounty !== claimerBounty) throw new IntermediaryError("Invalid claimer bounty");
if(resp.totalGas !== options.gasAmount) throw new IntermediaryError("Invalid total gas amount");
if(!amountData.exactIn) {
if(resp.total != amountData.amount) throw new IntermediaryError("Invalid amount returned");
} else {
if(amountIn !== amountData.amount) throw new IntermediaryError("Invalid payment request returned, amount mismatch");
}
}
/**
* Returns a newly created Lightning -> Smart chain swap using the HTLC based escrow swap protocol,
* where watchtowers handle the automatic settlement of the swap on the destination chain. Also allows
* specifying additional "gas drop" native token that the receipient receives on the destination chain
* in the `options` argument. The user has to pay a bolt11 invoice on the input lightning network side.
*
* @param recipient Recipient's address on the destination chain
* @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: string,
amountData: AmountData,
lps: Intermediary[],
options?: FromBTCLNAutoOptions,
additionalParams?: Record<string, any>,
abortSignal?: AbortSignal,
preFetches?: {
pricePrefetchPromise: Promise<bigint | undefined>,
usdPricePrefetchPromise: Promise<number | undefined>,
claimerBountyPrefetch: {[contractVersion: string]: Promise<bigint | undefined>},
gasTokenPricePrefetchPromise?: Promise<bigint | undefined>,
}
): {
quote: Promise<FromBTCLNAutoSwap<T>>,
intermediary: Intermediary
}[] {
if(!this.isInitialized) throw new Error("Not initialized, call init() first!");
const _options = {
paymentHash: parseHashValueExact32Bytes(options?.paymentHash, "payment hash"),
unsafeSkipLnNodeCheck: options?.unsafeSkipLnNodeCheck ?? this._options.unsafeSkipLnNodeCheck,
gasAmount: this.parseGasAmount(options?.gasAmount),
feeSafetyFactor: options?.feeSafetyFactor ?? 1.25, //No need to add much of a margin, since the claim should happen rather soon
unsafeZeroWatchtowerFee: options?.unsafeZeroWatchtowerFee ?? false,
description: options?.description,
descriptionHash: parseHashValueExact32Bytes(options?.descriptionHash, "description hash")
};
if(
_options.gasAmount!==0n &&
(
this._chain.shouldGetNativeTokenDrop!=null
? !this._chain.shouldGetNativeTokenDrop(amountData.token)
: amountData.token===this._chain.getNativeCurrencyAddress()
)
) throw new UserError("Cannot specify `gasAmount` for swaps to a native token!");
if(_options.description!=null && Buffer.byteLength(_options.description, "utf8") > 500)
throw new UserError("Invalid description length");
const lpVersions = Intermediary.getContractVersionsForLps(this.chainIdentifier, lps);
let secret: Buffer | undefined;
let paymentHash: Buffer;
if(_options?.paymentHash!=null) {
paymentHash = _options.paymentHash;
} else {
({secret, paymentHash} = this.getSecretAndHash());
}
const _hash = mapArrayToObject(lpVersions, (contractVersion: string) => {
return this._contract(contractVersion).getHashForHtlc(paymentHash).toString("hex");
});
const nativeTokenAddress = this._chain.getNativeCurrencyAddress();
const _abortController = extendAbortController(abortSignal);
const _preFetches = preFetches ?? {
pricePrefetchPromise: this.preFetchPrice(amountData, _abortController.signal),
usdPricePrefetchPromise: this.preFetchUsdPrice(_abortController.signal),
claimerBountyPrefetch: this.preFetchClaimerBounty(recipient, amountData, _options, _abortController, lpVersions),
gasTokenPricePrefetchPromise: _options.gasAmount!==0n || !_options.unsafeZeroWatchtowerFee ?
this.preFetchPrice({token: nativeTokenAddress}, _abortController.signal) :
undefined
};
return lps.map(lp => {
return {
intermediary: lp,
quote: (async () => {
if(lp.services[SwapType.FROM_BTCLN_AUTO]==null) throw new Error("LP service for processing from btcln auto swaps not found!");
const version = lp.getContractVersion(this.chainIdentifier);
const abortController = extendAbortController(_abortController.signal);
const liquidityPromise: Promise<bigint | undefined> = this.preFetchIntermediaryLiquidity(amountData, lp, abortController, version);
const {lnCapacityPromise, resp} = await tryWithRetries(async(retryCount: number) => {
const {lnPublicKey, response} = this._lpApi.initFromBTCLNAuto(
this.chainIdentifier, lp.url,
{
paymentHash,
amount: amountData.amount,
claimer: recipient,
token: amountData.token.toString(),
description: _options.description,
descriptionHash: _options.descriptionHash,
exactOut: !amountData.exactIn,
additionalParams,
gasToken: this._chain.getNativeCurrencyAddress(),
gasAmount: _options.gasAmount,
claimerBounty: throwIfUndefined(_preFetches.claimerBountyPrefetch[version], "Watchtower fee pre-fetch failed!")
},
this._options.postRequestTimeout, abortController.signal, retryCount>0 ? false : undefined
);
let lnCapacityPromise: Promise<LNNodeLiquidity | null> | undefined;
if(!_options.unsafeSkipLnNodeCheck) {
lnCapacityPromise = this.preFetchLnCapacity(lnPublicKey);
} else lnPublicKey.catch(() => {});
return {
lnCapacityPromise,
resp: await response
};
}, undefined, RequestError, abortController.signal);
const decodedPr = bolt11Decode(resp.pr);
if(decodedPr.millisatoshis==null) throw new IntermediaryError("Invalid returned swap invoice, no msat amount field");
if(decodedPr.timeExpireDate==null) throw new IntermediaryError("Invalid returned swap invoice, no expiry date field");
const amountIn = (BigInt(decodedPr.millisatoshis) + 999n) / 1000n;
const claimerBounty = (await _preFetches.claimerBountyPrefetch[version])!;
try {
this.verifyReturnedData(resp, amountData, lp, _options, decodedPr, paymentHash, claimerBounty);
const [pricingInfo, gasPricingInfo] = await Promise.all([
this.verifyReturnedPrice(
lp.services[SwapType.FROM_BTCLN_AUTO],
false, resp.btcAmountSwap,
resp.total,
amountData.token, {swapFeeBtc: resp.swapFeeBtc}, _preFetches.pricePrefetchPromise, _preFetches.usdPricePrefetchPromise, abortController.signal
),
_options.gasAmount===0n ? Promise.resolve(undefined) : this.verifyReturnedPrice(
{...lp.services[SwapType.FROM_BTCLN_AUTO], swapBaseFee: 0}, //Base fee should be charged only on the amount, not on gas
false, resp.btcAmountGas,
resp.totalGas + resp.claimerBounty,
nativeTokenAddress, {swapFeeBtc: resp.gasSwapFeeBtc}, _preFetches.gasTokenPricePrefetchPromise, _preFetches.usdPricePrefetchPromise, abortController.signal
),
this.verifyIntermediaryLiquidity(resp.total, throwIfUndefined(liquidityPromise, "LP liquidity pre-fetch failed!")),
_options.unsafeSkipLnNodeCheck ? Promise.resolve() : this.verifyLnNodeCapacity(lp, decodedPr, lnCapacityPromise, abortController.signal)
]);
const swapInit: FromBTCLNAutoSwapInit<T["Data"]> = {
pricingInfo,
url: lp.url,
expiry: decodedPr.timeExpireDate*1000,
swapFee: resp.swapFee,
gasSwapFee: resp.gasSwapFee,
swapFeeBtc: resp.swapFeeBtc,
gasSwapFeeBtc: resp.gasSwapFeeBtc,
btcAmountGas: resp.btcAmountGas,
btcAmountSwap: resp.btcAmountSwap,
gasPricingInfo,
initialSwapData: await this._contract(version).createSwapData(
ChainSwapType.HTLC, lp.getAddress(this.chainIdentifier), recipient, amountData.token,
resp.total, _hash[version],
this.getRandomSequence(), BigInt(Math.floor(Date.now()/1000)), false, true,
_options.gasAmount + resp.claimerBounty, resp.claimerBounty, nativeTokenAddress
),
pr: resp.pr,
secret: secret?.toString("hex"),
exactIn: amountData.exactIn ?? true,
contractVersion: version
};
const quote = new FromBTCLNAutoSwap<T>(this, swapInit);
return quote;
} catch (e) {
abortController.abort(e);
throw e;
}
})()
}
});
}
/**
* Returns a newly created Lightning -> Smart chain swap using the HTLC based escrow swap protocol,
* where watchtowers handle the automatic settlement of the swap on the destination chain. Also allows
* specifying additional "gas drop" native token that the receipient receives on the destination chain
* in the `options` argument. 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 Recipient's address on the destination chain
* @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: string,
lnurl: string | LNURLWithdrawParamsWithUrl,
amountData: AmountData,
lps: Intermediary[],
options?: FromBTCLNAutoOptions,
additionalParams?: Record<string, any>,
abortSignal?: AbortSignal
): Promise<{
quote: Promise<FromBTCLNAutoSwap<T>>,
intermediary: Intermediary
}[]> {
if(!this.isInitialized) throw new Error("Not initialized, call init() first!");
const _options = {
paymentHash: parseHashValueExact32Bytes(options?.paymentHash, "payment hash"),
unsafeSkipLnNodeCheck: options?.unsafeSkipLnNodeCheck ?? this._options.unsafeSkipLnNodeCheck,
gasAmount: this.parseGasAmount(options?.gasAmount),
feeSafetyFactor: options?.feeSafetyFactor ?? 1.25, //No need to add much of a margin, since the claim should happen rather soon
unsafeZeroWatchtowerFee: options?.unsafeZeroWatchtowerFee ?? false,
description: options?.description,
descriptionHash: parseHashValueExact32Bytes(options?.descriptionHash, "description hash")
};
const lpVersions = Intermediary.getContractVersionsForLps(this.chainIdentifier, lps);
const abortController = extendAbortController(abortSignal);
const preFetches = {
pricePrefetchPromise: this.preFetchPrice(amountData, abortController.signal),
usdPricePrefetchPromise: this.preFetchUsdPrice(abortController.signal),
gasTokenPricePrefetchPromise: _options.gasAmount!==0n || !_options.unsafeZeroWatchtowerFee ?
this.preFetchPrice({token: this._chain.getNativeCurrencyAddress()}, abortController.signal) :
undefined,
claimerBountyPrefetch: this.preFetchClaimerBounty(recipient, amountData, _options, abortController, lpVersions)
};
try {
const exactOutAmountPromise: Promise<bigint | undefined> | undefined = !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("Amount less than LNURL-withdraw minimum");
if(amountData.amount > max) throw new UserError("Amount more than LNURL-withdraw maximum");
} else {
const amount = (await exactOutAmountPromise)!;
abortController.signal.throwIfAborted();
if((amount * 95n / 100n) < min) throw new UserError("Amount less than LNURL-withdraw minimum");
if((amount * 105n / 100n) > max) throw new 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("Amount less than LNURL-withdraw minimum");
if(amountIn > max) throw new UserError("Amount more than LNURL-withdraw maximum");
return quote;
}),
intermediary: data.intermediary
}
});
} catch (e) {
abortController.abort(e);
throw e;
}
}
/**
* @inheritDoc
* @internal
*/
protected async _checkPastSwaps(pastSwaps: FromBTCLNAutoSwap<T>[]): Promise<{
changedSwaps: FromBTCLNAutoSwap<T>[];
removeSwaps: FromBTCLNAutoSwap<T>[]
}> {
const changedSwapSet: Set<FromBTCLNAutoSwap<T>> = new Set();
const swapExpiredStatus: {[id: string]: boolean} = {};
const checkStatusSwaps: {[contractVersion: string]: (FromBTCLNAutoSwap<T> & {_data: T["Data"]})[]} = {};
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 as FromBTCLNAutoSwap<T> & {_data: T["Data"]});
}
}));
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: FromBTCLNAutoSwap<T>[] = [];
const removeSwaps: FromBTCLNAutoSwap<T>[] = [];
changedSwapSet.forEach(val => {
if(val.isQuoteExpired()) {
removeSwaps.push(val);
} else {
changedSwaps.push(val);
}
});
return {
changedSwaps,
removeSwaps
};
}
/**
* @inheritDoc
*/
async recoverFromSwapDataAndState(
init: {data: T["Data"], getInitTxId: () => Promise<string>, getTxBlock: () => Promise<{blockTime: number, blockHeight: number}>},
state: SwapCommitState,
contractVersion: string,
lp?: Intermediary
): Promise<FromBTCLNAutoSwap<T> | null> {
const data = init.data;
let paymentHash = data.getHTLCHashHint();
let secret: string | undefined;
if(state.type===SwapCommitStateType.PAID) {
secret = await state.getClaimResult();
paymentHash = Buffer.from(sha256(Buffer.from(secret, "hex"))).toString("hex");
}
const swapInit: FromBTCLNAutoSwapInit<T["Data"]> = {
pricingInfo: {
isValid: true,
satsBaseFee: 0n,
swapPriceUSatPerToken: 100_000_000_000_000n,
realPriceUSatPerToken: 100_000_000_000_000n,
differencePPM: 0n,
feePPM: 0n,
},
url: lp?.url,
expiry: 0,
swapFee: 0n,
swapFeeBtc: 0n,
gasSwapFee: 0n,
gasSwapFeeBtc: 0n,
initialSwapData: data,
data,
pr: paymentHash ?? undefined,
secret,
exactIn: false,
contractVersion
}
const swap = new FromBTCLNAutoSwap(this, swapInit);
swap._commitTxId = await init.getInitTxId();
const blockData = await init.getTxBlock();
swap.createdAt = blockData.blockTime * 1000;
swap._commitedAt = blockData.blockTime * 1000;
swap._setInitiated();
swap._state = FromBTCLNAutoSwapState.CLAIM_COMMITED;
await swap._sync(false, false, state);
await swap._save();
return swap;
}
}