@atomiqlabs/sdk-lib
Version:
Basic SDK functionality library for atomiq
1,098 lines (1,020 loc) • 73.7 kB
text/typescript
import {ISwapPrice} from "../../prices/abstract/ISwapPrice";
import {
BitcoinNetwork,
BtcRelay,
ChainData,
ChainType,
RelaySynchronizer
} from "@atomiqlabs/base";
import {ToBTCLNOptions, ToBTCLNWrapper} from "../escrow_swaps/tobtc/ln/ToBTCLNWrapper";
import {ToBTCOptions, ToBTCWrapper} from "../escrow_swaps/tobtc/onchain/ToBTCWrapper";
import {FromBTCLNOptions, FromBTCLNWrapper} from "../escrow_swaps/frombtc/ln/FromBTCLNWrapper";
import {FromBTCOptions, FromBTCWrapper} from "../escrow_swaps/frombtc/onchain/FromBTCWrapper";
import {IntermediaryDiscovery, MultichainSwapBounds, SwapBounds} from "../../intermediaries/IntermediaryDiscovery";
import {decode as bolt11Decode} from "@atomiqlabs/bolt11";
import {ISwap} from "../ISwap";
import {IntermediaryError} from "../../errors/IntermediaryError";
import {SwapType} from "../enums/SwapType";
import {FromBTCLNSwap} from "../escrow_swaps/frombtc/ln/FromBTCLNSwap";
import {FromBTCSwap} from "../escrow_swaps/frombtc/onchain/FromBTCSwap";
import {ToBTCLNSwap} from "../escrow_swaps/tobtc/ln/ToBTCLNSwap";
import {ToBTCSwap} from "../escrow_swaps/tobtc/onchain/ToBTCSwap";
import {MempoolApi} from "../../btc/mempool/MempoolApi";
import {MempoolBitcoinRpc} from "../../btc/mempool/MempoolBitcoinRpc";
import {MempoolBtcRelaySynchronizer} from "../../btc/mempool/synchronizer/MempoolBtcRelaySynchronizer";
import {LnForGasWrapper} from "../trusted/ln/LnForGasWrapper";
import {LnForGasSwap} from "../trusted/ln/LnForGasSwap";
import {EventEmitter} from "events";
import {MempoolBitcoinBlock} from "../../btc/mempool/MempoolBitcoinBlock";
import {Intermediary} from "../../intermediaries/Intermediary";
import {isLNURLPay, isLNURLWithdraw, LNURLPay, LNURLWithdraw} from "../../utils/LNURL";
import {AmountData, ISwapWrapper, WrapperCtorTokens} from "../ISwapWrapper";
import {bigIntCompare, bigIntMax, bigIntMin, getLogger, objectMap, randomBytes} from "../../utils/Utils";
import {OutOfBoundsError} from "../../errors/RequestError";
import {SwapperWithChain} from "./SwapperWithChain";
import {
BitcoinTokens,
BtcToken,
isBtcToken,
isSCToken,
SCToken,
Token,
TokenAmount,
toTokenAmount
} from "../../Tokens";
import {OnchainForGasSwap} from "../trusted/onchain/OnchainForGasSwap";
import {OnchainForGasWrapper} from "../trusted/onchain/OnchainForGasWrapper";
import {BTC_NETWORK, NETWORK, TEST_NETWORK} from "@scure/btc-signer/utils";
import {IUnifiedStorage, QueryParams} from "../../storage/IUnifiedStorage";
import {IndexedDBUnifiedStorage} from "../../browser-storage/IndexedDBUnifiedStorage";
import {
UnifiedSwapStorage,
UnifiedSwapStorageCompositeIndexes,
UnifiedSwapStorageIndexes
} from "../../storage/UnifiedSwapStorage";
import {UnifiedSwapEventListener} from "../../events/UnifiedSwapEventListener";
import {IToBTCSwap} from "../escrow_swaps/tobtc/IToBTCSwap";
import {SpvFromBTCOptions, SpvFromBTCWrapper} from "../spv_swaps/SpvFromBTCWrapper";
import {SpvFromBTCSwap} from "../spv_swaps/SpvFromBTCSwap";
import {SwapperUtils} from "./utils/SwapperUtils";
export type SwapperOptions = {
intermediaryUrl?: string | string[],
registryUrl?: string,
bitcoinNetwork?: BitcoinNetwork,
getRequestTimeout?: number,
postRequestTimeout?: number,
defaultAdditionalParameters?: {[key: string]: any},
storagePrefix?: string
defaultTrustedIntermediaryUrl?: string,
swapStorage?: <T extends ChainType>(chainId: T["ChainId"]) => IUnifiedStorage<UnifiedSwapStorageIndexes, UnifiedSwapStorageCompositeIndexes>,
noTimers?: boolean,
noEvents?: boolean,
noSwapCache?: boolean,
dontCheckPastSwaps?: boolean,
dontFetchLPs?: boolean
};
export type MultiChain = {
[chainIdentifier in string]: ChainType;
};
export type ChainSpecificData<T extends ChainType> = {
wrappers: {
[SwapType.TO_BTCLN]: ToBTCLNWrapper<T>,
[SwapType.TO_BTC]: ToBTCWrapper<T>,
[SwapType.FROM_BTCLN]: FromBTCLNWrapper<T>,
[SwapType.FROM_BTC]: FromBTCWrapper<T>,
[SwapType.TRUSTED_FROM_BTCLN]: LnForGasWrapper<T>,
[SwapType.TRUSTED_FROM_BTC]: OnchainForGasWrapper<T>,
[SwapType.SPV_VAULT_FROM_BTC]: SpvFromBTCWrapper<T>
}
chainEvents: T["Events"],
swapContract: T["Contract"],
spvVaultContract: T["SpvVaultContract"],
chainInterface: T["ChainInterface"],
btcRelay: BtcRelay<any, T["TX"], MempoolBitcoinBlock, T["Signer"]>,
synchronizer: RelaySynchronizer<any, T["TX"], MempoolBitcoinBlock>,
unifiedChainEvents: UnifiedSwapEventListener<T>,
unifiedSwapStorage: UnifiedSwapStorage<T>,
reviver: (val: any) => ISwap<T>
};
export type MultiChainData<T extends MultiChain> = {
[chainIdentifier in keyof T]: ChainSpecificData<T[chainIdentifier]>
};
export type CtorMultiChainData<T extends MultiChain> = {
[chainIdentifier in keyof T]: ChainData<T[chainIdentifier]>
};
export type ChainIds<T extends MultiChain> = keyof T & string;
type NotNever<T> = [T] extends [never] ? false : true;
export type SupportsSwapType<
C extends ChainType,
Type extends SwapType
> = Type extends SwapType.SPV_VAULT_FROM_BTC ?
NotNever<C["SpvVaultContract"]> :
Type extends (SwapType.TRUSTED_FROM_BTCLN | SwapType.TRUSTED_FROM_BTC) ? true :
NotNever<C["Contract"]>;
export class Swapper<T extends MultiChain> extends EventEmitter<{
lpsRemoved: [Intermediary[]],
lpsAdded: [Intermediary[]],
swapState: [ISwap],
swapLimitsChanged: []
}> {
protected readonly logger = getLogger(this.constructor.name+": ");
protected readonly swapStateListener: (swap: ISwap) => void;
private defaultTrustedIntermediary: Intermediary;
readonly chains: MultiChainData<T>;
readonly prices: ISwapPrice<T>;
readonly intermediaryDiscovery: IntermediaryDiscovery;
readonly options: SwapperOptions;
readonly mempoolApi: MempoolApi;
readonly bitcoinRpc: MempoolBitcoinRpc;
readonly bitcoinNetwork: BTC_NETWORK;
private readonly _bitcoinNetwork: BitcoinNetwork;
readonly tokens: {
[chainId: string]: {
[tokenAddress: string]: SCToken
}
};
readonly Utils: SwapperUtils<T>;
constructor(
bitcoinRpc: MempoolBitcoinRpc,
chainsData: CtorMultiChainData<T>,
pricing: ISwapPrice<T>,
tokens: WrapperCtorTokens<T>,
options?: SwapperOptions
) {
super();
const storagePrefix = options?.storagePrefix ?? "atomiq-";
options.bitcoinNetwork = options.bitcoinNetwork==null ? BitcoinNetwork.TESTNET : options.bitcoinNetwork;
options.swapStorage ??= (name: string) => new IndexedDBUnifiedStorage(name);
this._bitcoinNetwork = options.bitcoinNetwork;
this.bitcoinNetwork = options.bitcoinNetwork===BitcoinNetwork.MAINNET ? NETWORK :
(options.bitcoinNetwork===BitcoinNetwork.TESTNET || options.bitcoinNetwork===BitcoinNetwork.TESTNET4) ? TEST_NETWORK : null;
this.Utils = new SwapperUtils(this);
this.prices = pricing;
this.bitcoinRpc = bitcoinRpc;
this.mempoolApi = bitcoinRpc.api;
this.options = options;
this.tokens = {};
for(let tokenData of tokens) {
for(let chainId in tokenData.chains) {
const chainData = tokenData.chains[chainId];
this.tokens[chainId] ??= {};
this.tokens[chainId][chainData.address] = {
chain: "SC",
chainId,
ticker: tokenData.ticker,
name: tokenData.name,
decimals: chainData.decimals,
displayDecimals: chainData.displayDecimals,
address: chainData.address
}
}
}
this.swapStateListener = (swap: ISwap) => {
this.emit("swapState", swap);
};
this.chains = objectMap<CtorMultiChainData<T>, MultiChainData<T>>(chainsData, <InputKey extends keyof CtorMultiChainData<T>>(chainData: CtorMultiChainData<T>[InputKey], key: string) => {
const {
swapContract, chainEvents, btcRelay,
chainInterface, spvVaultContract, spvVaultWithdrawalDataConstructor
} = chainData;
const synchronizer = new MempoolBtcRelaySynchronizer(btcRelay, bitcoinRpc);
const storageHandler = options.swapStorage(storagePrefix + chainData.chainId);
const unifiedSwapStorage = new UnifiedSwapStorage<T[InputKey]>(storageHandler, this.options.noSwapCache);
const unifiedChainEvents = new UnifiedSwapEventListener<T[InputKey]>(unifiedSwapStorage, chainEvents);
const wrappers: any = {};
wrappers[SwapType.TO_BTCLN] = new ToBTCLNWrapper<T[InputKey]>(
key,
unifiedSwapStorage,
unifiedChainEvents,
chainInterface,
swapContract,
pricing,
tokens,
chainData.swapDataConstructor,
{
getRequestTimeout: options.getRequestTimeout,
postRequestTimeout: options.postRequestTimeout,
}
);
wrappers[SwapType.TO_BTC] = new ToBTCWrapper<T[InputKey]>(
key,
unifiedSwapStorage,
unifiedChainEvents,
chainInterface,
swapContract,
pricing,
tokens,
chainData.swapDataConstructor,
this.bitcoinRpc,
{
getRequestTimeout: options.getRequestTimeout,
postRequestTimeout: options.postRequestTimeout,
bitcoinNetwork: this.bitcoinNetwork
}
);
wrappers[SwapType.FROM_BTCLN] = new FromBTCLNWrapper<T[InputKey]>(
key,
unifiedSwapStorage,
unifiedChainEvents,
chainInterface,
swapContract,
pricing,
tokens,
chainData.swapDataConstructor,
bitcoinRpc,
{
getRequestTimeout: options.getRequestTimeout,
postRequestTimeout: options.postRequestTimeout
}
);
wrappers[SwapType.FROM_BTC] = new FromBTCWrapper<T[InputKey]>(
key,
unifiedSwapStorage,
unifiedChainEvents,
chainInterface,
swapContract,
pricing,
tokens,
chainData.swapDataConstructor,
btcRelay,
synchronizer,
this.bitcoinRpc,
{
getRequestTimeout: options.getRequestTimeout,
postRequestTimeout: options.postRequestTimeout,
bitcoinNetwork: this.bitcoinNetwork
}
);
wrappers[SwapType.TRUSTED_FROM_BTCLN] = new LnForGasWrapper<T[InputKey]>(
key,
unifiedSwapStorage,
unifiedChainEvents,
chainInterface,
pricing,
tokens,
{
getRequestTimeout: options.getRequestTimeout,
postRequestTimeout: options.postRequestTimeout
}
);
wrappers[SwapType.TRUSTED_FROM_BTC] = new OnchainForGasWrapper<T[InputKey]>(
key,
unifiedSwapStorage,
unifiedChainEvents,
chainInterface,
pricing,
tokens,
bitcoinRpc,
{
getRequestTimeout: options.getRequestTimeout,
postRequestTimeout: options.postRequestTimeout,
bitcoinNetwork: this.bitcoinNetwork
}
);
if(spvVaultContract!=null) {
wrappers[SwapType.SPV_VAULT_FROM_BTC] = new SpvFromBTCWrapper<T[InputKey]>(
key,
unifiedSwapStorage,
unifiedChainEvents,
chainInterface,
spvVaultContract,
pricing,
tokens,
spvVaultWithdrawalDataConstructor,
btcRelay,
synchronizer,
bitcoinRpc,
{
getRequestTimeout: options.getRequestTimeout,
postRequestTimeout: options.postRequestTimeout,
bitcoinNetwork: this.bitcoinNetwork
}
);
}
Object.keys(wrappers).forEach(key => wrappers[key].events.on("swapState", this.swapStateListener));
const reviver = (val: any) => {
const wrapper = wrappers[val.type];
if(wrapper==null) return null;
return new wrapper.swapDeserializer(wrapper, val);
};
return {
chainEvents,
spvVaultContract,
swapContract,
chainInterface,
btcRelay,
synchronizer,
wrappers,
unifiedChainEvents,
unifiedSwapStorage,
reviver
}
});
const contracts = objectMap(chainsData, (data) => data.swapContract);
if(options.intermediaryUrl!=null) {
this.intermediaryDiscovery = new IntermediaryDiscovery(contracts, options.registryUrl, Array.isArray(options.intermediaryUrl) ? options.intermediaryUrl : [options.intermediaryUrl], options.getRequestTimeout);
} else {
this.intermediaryDiscovery = new IntermediaryDiscovery(contracts, options.registryUrl, null, options.getRequestTimeout);
}
this.intermediaryDiscovery.on("removed", (intermediaries: Intermediary[]) => {
this.emit("lpsRemoved", intermediaries);
});
this.intermediaryDiscovery.on("added", (intermediaries: Intermediary[]) => {
this.emit("lpsAdded", intermediaries);
});
}
/**
* Initializes the swap storage and loads existing swaps, needs to be called before any other action
*/
async init(): Promise<void> {
for(let chainIdentifier in this.chains) {
const {
swapContract,
unifiedChainEvents,
unifiedSwapStorage,
wrappers,
reviver
} = this.chains[chainIdentifier];
await swapContract.start();
this.logger.debug("init(): Intialized swap contract: "+chainIdentifier);
await unifiedSwapStorage.init();
if(unifiedSwapStorage.storage instanceof IndexedDBUnifiedStorage) {
//Try to migrate the data here
const storagePrefix = chainIdentifier==="SOLANA" ?
"SOLv4-"+this._bitcoinNetwork+"-Swaps-" :
"atomiqsdk-"+this._bitcoinNetwork+chainIdentifier+"-Swaps-";
await unifiedSwapStorage.storage.tryMigrate(
[
[storagePrefix+"FromBTC", SwapType.FROM_BTC],
[storagePrefix+"FromBTCLN", SwapType.FROM_BTCLN],
[storagePrefix+"ToBTC", SwapType.TO_BTC],
[storagePrefix+"ToBTCLN", SwapType.TO_BTCLN]
],
(obj: any) => {
const swap = reviver(obj);
if(swap.randomNonce==null) {
const oldIdentifierHash = swap.getId();
swap.randomNonce = randomBytes(16).toString("hex");
const newIdentifierHash = swap.getId();
this.logger.info("init(): Found older swap version without randomNonce, replacing, old hash: "+oldIdentifierHash+
" new hash: "+newIdentifierHash);
}
return swap;
}
)
}
if(!this.options.noEvents) await unifiedChainEvents.start();
this.logger.debug("init(): Intialized events: "+chainIdentifier);
for(let key in wrappers) {
// this.logger.debug("init(): Initializing "+SwapType[key]+": "+chainIdentifier);
await wrappers[key].init(this.options.noTimers, this.options.dontCheckPastSwaps);
}
}
this.logger.debug("init(): Initializing intermediary discovery");
if(!this.options.dontFetchLPs) await this.intermediaryDiscovery.init();
if(this.options.defaultTrustedIntermediaryUrl!=null) {
this.defaultTrustedIntermediary = await this.intermediaryDiscovery.getIntermediary(this.options.defaultTrustedIntermediaryUrl);
}
}
/**
* Stops listening for onchain events and closes this Swapper instance
*/
async stop() {
for(let chainIdentifier in this.chains) {
const {
wrappers,
unifiedChainEvents
} = this.chains[chainIdentifier];
for(let key in wrappers) {
(wrappers[key] as ISwapWrapper<any, any>).events.removeListener("swapState", this.swapStateListener);
await wrappers[key].stop();
}
await unifiedChainEvents.stop();
}
}
/**
* Creates swap & handles intermediary, quote selection
*
* @param chainIdentifier
* @param create Callback to create the
* @param amountData Amount data as passed to the function
* @param swapType Swap type of the execution
* @param maxWaitTimeMS Maximum waiting time after the first intermediary returns the quote
* @private
* @throws {Error} when no intermediary was found
* @throws {Error} if the chain with the provided identifier cannot be found
*/
private async createSwap<ChainIdentifier extends ChainIds<T>, S extends ISwap<T[ChainIdentifier]>>(
chainIdentifier: ChainIdentifier,
create: (candidates: Intermediary[], abortSignal: AbortSignal, chain: ChainSpecificData<T[ChainIdentifier]>) => Promise<{
quote: Promise<S>,
intermediary: Intermediary
}[]>,
amountData: AmountData,
swapType: SwapType,
maxWaitTimeMS: number = 2000
): Promise<S> {
if(this.chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier);
let candidates: Intermediary[];
const inBtc: boolean = swapType===SwapType.TO_BTCLN || swapType===SwapType.TO_BTC ? !amountData.exactIn : amountData.exactIn;
if(!inBtc) {
//Get candidates not based on the amount
candidates = this.intermediaryDiscovery.getSwapCandidates(chainIdentifier, swapType, amountData.token);
} else {
candidates = this.intermediaryDiscovery.getSwapCandidates(chainIdentifier, swapType, amountData.token, amountData.amount);
}
let swapLimitsChanged = false;
if(candidates.length===0) {
this.logger.warn("createSwap(): No valid intermediary found, reloading intermediary database...");
await this.intermediaryDiscovery.reloadIntermediaries();
swapLimitsChanged = true;
if(!inBtc) {
//Get candidates not based on the amount
candidates = this.intermediaryDiscovery.getSwapCandidates(chainIdentifier, swapType, amountData.token);
} else {
candidates = this.intermediaryDiscovery.getSwapCandidates(chainIdentifier, swapType, amountData.token, amountData.amount);
if(candidates.length===0) {
const min = this.intermediaryDiscovery.getSwapMinimum(chainIdentifier, swapType, amountData.token);
const max = this.intermediaryDiscovery.getSwapMaximum(chainIdentifier, swapType, amountData.token);
if(min!=null && max!=null) {
if(amountData.amount < BigInt(min)) throw new OutOfBoundsError("Amount too low!", 200, BigInt(min), BigInt(max));
if(amountData.amount > BigInt(max)) throw new OutOfBoundsError("Amount too high!", 200, BigInt(min), BigInt(max));
}
}
}
if(candidates.length===0) throw new Error("No intermediary found!");
}
const abortController = new AbortController();
this.logger.debug("createSwap() Swap candidates: ", candidates.map(lp => lp.url).join());
const quotePromises: {quote: Promise<S>, intermediary: Intermediary}[] = await create(candidates, abortController.signal, this.chains[chainIdentifier]);
const promiseAll = new Promise<{
quote: S,
intermediary: Intermediary
}[]>((resolve, reject) => {
let min: bigint;
let max: bigint;
let error: Error;
let numResolved = 0;
let quotes: {
quote: S,
intermediary: Intermediary
}[] = [];
let timeout: NodeJS.Timeout;
quotePromises.forEach(data => {
data.quote.then(quote => {
if(numResolved===0) {
timeout = setTimeout(() => {
abortController.abort(new Error("Timed out waiting for quote!"));
resolve(quotes);
}, maxWaitTimeMS);
}
numResolved++;
quotes.push({
quote,
intermediary: data.intermediary
});
if(numResolved===quotePromises.length) {
clearTimeout(timeout);
resolve(quotes);
return;
}
}).catch(e => {
numResolved++;
if(e instanceof IntermediaryError) {
//Blacklist that node
this.intermediaryDiscovery.removeIntermediary(data.intermediary);
swapLimitsChanged = true;
} else if(e instanceof OutOfBoundsError) {
if(min==null || max==null) {
min = e.min;
max = e.max;
} else {
min = bigIntMin(min, e.min);
max = bigIntMax(max, e.max);
}
data.intermediary.swapBounds[swapType] ??= {};
data.intermediary.swapBounds[swapType][chainIdentifier] ??= {};
const tokenBoundsData = (data.intermediary.swapBounds[swapType][chainIdentifier][amountData.token] ??= {input: null, output: null});
if(amountData.exactIn) {
tokenBoundsData.input = {min: e.min, max: e.max};
} else {
tokenBoundsData.output = {min: e.min, max: e.max};
}
swapLimitsChanged = true;
}
this.logger.warn("createSwap(): Intermediary "+data.intermediary.url+" error: ", e);
error = e;
if(numResolved===quotePromises.length) {
if(timeout!=null) clearTimeout(timeout);
if(quotes.length>0) {
resolve(quotes);
return;
}
if(min!=null && max!=null) {
reject(new OutOfBoundsError("Out of bounds", 400, min, max));
return;
}
reject(error);
}
});
});
});
try {
const quotes = await promiseAll;
//TODO: Intermediary's reputation is not taken into account!
quotes.sort((a, b) => {
if(amountData.exactIn) {
//Compare outputs
return bigIntCompare(b.quote.getOutput().rawAmount, a.quote.getOutput().rawAmount);
} else {
//Compare inputs
return bigIntCompare(a.quote.getOutput().rawAmount, b.quote.getOutput().rawAmount);
}
});
this.logger.debug("createSwap(): Sorted quotes, best price to worst: ", quotes);
if(swapLimitsChanged) this.emit("swapLimitsChanged");
return quotes[0].quote;
} catch (e) {
if(swapLimitsChanged) this.emit("swapLimitsChanged");
throw e;
}
}
/**
* Creates To BTC swap
*
* @param chainIdentifier
* @param signer
* @param tokenAddress Token address to pay with
* @param address Recipient's bitcoin address
* @param amount Amount to send in satoshis (bitcoin's smallest denomination)
* @param exactIn Whether to use exact in instead of exact out
* @param additionalParams Additional parameters sent to the LP when creating the swap
* @param options
*/
createToBTCSwap<ChainIdentifier extends ChainIds<T>>(
chainIdentifier: ChainIdentifier,
signer: string,
tokenAddress: string,
address: string,
amount: bigint,
exactIn?: boolean,
additionalParams: Record<string, any> = this.options.defaultAdditionalParameters,
options?: ToBTCOptions
): Promise<ToBTCSwap<T[ChainIdentifier]>> {
if(address.startsWith("bitcoin:")) {
address = address.substring(8).split("?")[0];
}
options ??= {};
options.confirmationTarget ??= 3;
options.confirmations ??= 2;
const amountData = {
amount,
token: tokenAddress,
exactIn
};
return this.createSwap(
chainIdentifier as ChainIdentifier,
(candidates: Intermediary[], abortSignal, chain) => Promise.resolve(chain.wrappers[SwapType.TO_BTC].create(
signer,
address,
amountData,
candidates,
options,
additionalParams,
abortSignal
)),
amountData,
SwapType.TO_BTC
);
}
/**
* Creates To BTCLN swap
*
* @param chainIdentifier
* @param signer
* @param tokenAddress Token address to pay with
* @param paymentRequest BOLT11 lightning network invoice to be paid (needs to have a fixed amount)
* @param additionalParams Additional parameters sent to the LP when creating the swap
* @param options
*/
async createToBTCLNSwap<ChainIdentifier extends ChainIds<T>>(
chainIdentifier: ChainIdentifier,
signer: string,
tokenAddress: string,
paymentRequest: string,
additionalParams: Record<string, any> = this.options.defaultAdditionalParameters,
options?: ToBTCLNOptions
): Promise<ToBTCLNSwap<T[ChainIdentifier]>> {
options ??= {};
if(paymentRequest.startsWith("lightning:")) paymentRequest = paymentRequest.substring(10);
const parsedPR = bolt11Decode(paymentRequest);
const amountData = {
amount: (BigInt(parsedPR.millisatoshis) + 999n) / 1000n,
token: tokenAddress,
exactIn: false
};
options.expirySeconds ??= 5*24*3600;
return this.createSwap(
chainIdentifier as ChainIdentifier,
(candidates: Intermediary[], abortSignal: AbortSignal, chain) => chain.wrappers[SwapType.TO_BTCLN].create(
signer,
paymentRequest,
amountData,
candidates,
options,
additionalParams,
abortSignal
),
amountData,
SwapType.TO_BTCLN
);
}
/**
* Creates To BTCLN swap via LNURL-pay
*
* @param chainIdentifier
* @param signer
* @param tokenAddress Token address to pay with
* @param lnurlPay LNURL-pay link to use for the payment
* @param amount Amount to be paid in sats
* @param exactIn Whether to do an exact in swap instead of exact out
* @param additionalParams Additional parameters sent to the LP when creating the swap
* @param options
*/
async createToBTCLNSwapViaLNURL<ChainIdentifier extends ChainIds<T>>(
chainIdentifier: ChainIdentifier,
signer: string,
tokenAddress: string,
lnurlPay: string | LNURLPay,
amount: bigint,
exactIn?: boolean,
additionalParams: Record<string, any> = this.options.defaultAdditionalParameters,
options?: ToBTCLNOptions & {comment?: string}
): Promise<ToBTCLNSwap<T[ChainIdentifier]>> {
options ??= {};
const amountData = {
amount,
token: tokenAddress,
exactIn
};
options.expirySeconds ??= 5*24*3600;
return this.createSwap(
chainIdentifier as ChainIdentifier,
(candidates: Intermediary[], abortSignal: AbortSignal, chain) => chain.wrappers[SwapType.TO_BTCLN].createViaLNURL(
signer,
typeof(lnurlPay)==="string" ? (lnurlPay.startsWith("lightning:") ? lnurlPay.substring(10): lnurlPay) : lnurlPay.params,
amountData,
candidates,
options,
additionalParams,
abortSignal
),
amountData,
SwapType.TO_BTCLN
);
}
/**
* Creates From BTC swap
*
* @param chainIdentifier
* @param signer
* @param tokenAddress Token address to receive
* @param amount Amount to receive, in satoshis (bitcoin's smallest denomination)
* @param exactOut Whether to use a exact out instead of exact in
* @param additionalParams Additional parameters sent to the LP when creating the swap
* @param options
*/
async createFromBTCSwapNew<ChainIdentifier extends ChainIds<T>>(
chainIdentifier: ChainIdentifier,
signer: string,
tokenAddress: string,
amount: bigint,
exactOut?: boolean,
additionalParams: Record<string, any> = this.options.defaultAdditionalParameters,
options?: SpvFromBTCOptions
): Promise<SpvFromBTCSwap<T[ChainIdentifier]>> {
const amountData = {
amount,
token: tokenAddress,
exactIn: !exactOut
};
return this.createSwap(
chainIdentifier as ChainIdentifier,
(candidates: Intermediary[], abortSignal: AbortSignal, chain) => Promise.resolve(chain.wrappers[SwapType.SPV_VAULT_FROM_BTC].create(
signer,
amountData,
candidates,
options,
additionalParams,
abortSignal
)),
amountData,
SwapType.SPV_VAULT_FROM_BTC
);
}
/**
* Creates From BTC swap
*
* @param chainIdentifier
* @param signer
* @param tokenAddress Token address to receive
* @param amount Amount to receive, in satoshis (bitcoin's smallest denomination)
* @param exactOut Whether to use a exact out instead of exact in
* @param additionalParams Additional parameters sent to the LP when creating the swap
* @param options
*/
async createFromBTCSwap<ChainIdentifier extends ChainIds<T>>(
chainIdentifier: ChainIdentifier,
signer: string,
tokenAddress: string,
amount: bigint,
exactOut?: boolean,
additionalParams: Record<string, any> = this.options.defaultAdditionalParameters,
options?: FromBTCOptions
): Promise<FromBTCSwap<T[ChainIdentifier]>> {
const amountData = {
amount,
token: tokenAddress,
exactIn: !exactOut
};
return this.createSwap(
chainIdentifier as ChainIdentifier,
(candidates: Intermediary[], abortSignal: AbortSignal, chain) => Promise.resolve(chain.wrappers[SwapType.FROM_BTC].create(
signer,
amountData,
candidates,
options,
additionalParams,
abortSignal
)),
amountData,
SwapType.FROM_BTC
);
}
/**
* Creates From BTCLN swap
*
* @param chainIdentifier
* @param signer
* @param tokenAddress Token address to receive
* @param amount Amount to receive, in satoshis (bitcoin's smallest denomination)
* @param exactOut Whether to use exact out instead of exact in
* @param additionalParams Additional parameters sent to the LP when creating the swap
* @param options
*/
async createFromBTCLNSwap<ChainIdentifier extends ChainIds<T>>(
chainIdentifier: ChainIdentifier,
signer: string,
tokenAddress: string,
amount: bigint,
exactOut?: boolean,
additionalParams: Record<string, any> = this.options.defaultAdditionalParameters,
options?: FromBTCLNOptions
): Promise<FromBTCLNSwap<T[ChainIdentifier]>> {
const amountData = {
amount,
token: tokenAddress,
exactIn: !exactOut
};
return this.createSwap(
chainIdentifier as ChainIdentifier,
(candidates: Intermediary[], abortSignal: AbortSignal, chain) => Promise.resolve(chain.wrappers[SwapType.FROM_BTCLN].create(
signer,
amountData,
candidates,
options,
additionalParams,
abortSignal
)),
amountData,
SwapType.FROM_BTCLN
);
}
/**
* Creates From BTCLN swap, withdrawing from LNURL-withdraw
*
* @param chainIdentifier
* @param signer
* @param tokenAddress Token address to receive
* @param lnurl LNURL-withdraw to pull the funds from
* @param amount Amount to receive, in satoshis (bitcoin's smallest denomination)
* @param exactOut Whether to use exact out instead of exact in
* @param additionalParams Additional parameters sent to the LP when creating the swap
*/
async createFromBTCLNSwapViaLNURL<ChainIdentifier extends ChainIds<T>>(
chainIdentifier: ChainIdentifier,
signer: string,
tokenAddress: string,
lnurl: string | LNURLWithdraw,
amount: bigint,
exactOut?: boolean,
additionalParams: Record<string, any> = this.options.defaultAdditionalParameters
): Promise<FromBTCLNSwap<T[ChainIdentifier]>> {
const amountData = {
amount,
token: tokenAddress,
exactIn: !exactOut
};
return this.createSwap(
chainIdentifier as ChainIdentifier,
(candidates: Intermediary[], abortSignal: AbortSignal, chain) => chain.wrappers[SwapType.FROM_BTCLN].createViaLNURL(
signer,
typeof(lnurl)==="string" ? (lnurl.startsWith("lightning:") ? lnurl.substring(10): lnurl) : lnurl.params,
amountData,
candidates,
additionalParams,
abortSignal
),
amountData,
SwapType.FROM_BTCLN
);
}
/**
* Creates trusted LN for Gas swap
*
* @param chainId
* @param signer
* @param amount Amount of native token to receive, in base units
* @param trustedIntermediaryOrUrl URL or Intermediary object of the trusted intermediary to use, otherwise uses default
* @throws {Error} If no trusted intermediary specified
*/
createTrustedLNForGasSwap<C extends ChainIds<T>>(chainId: C, signer: string, amount: bigint, trustedIntermediaryOrUrl?: Intermediary | string): Promise<LnForGasSwap<T[C]>> {
if(this.chains[chainId]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainId);
const useUrl = trustedIntermediaryOrUrl ?? this.defaultTrustedIntermediary ?? this.options.defaultTrustedIntermediaryUrl;
if(useUrl==null) throw new Error("No trusted intermediary specified!");
return this.chains[chainId as C].wrappers[SwapType.TRUSTED_FROM_BTCLN].create(signer, amount, useUrl);
}
/**
* Creates trusted BTC on-chain for Gas swap
*
* @param chainId
* @param signer
* @param amount Amount of native token to receive, in base units
* @param refundAddress Bitcoin refund address, in case the swap fails
* @param trustedIntermediaryOrUrl URL or Intermediary object of the trusted intermediary to use, otherwise uses default
* @throws {Error} If no trusted intermediary specified
*/
createTrustedOnchainForGasSwap<C extends ChainIds<T>>(
chainId: C, signer: string,
amount: bigint, refundAddress?: string,
trustedIntermediaryOrUrl?: Intermediary | string
): Promise<OnchainForGasSwap<T[C]>> {
if(this.chains[chainId]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainId);
const useUrl = trustedIntermediaryOrUrl ?? this.defaultTrustedIntermediary ?? this.options.defaultTrustedIntermediaryUrl;
if(useUrl==null) throw new Error("No trusted intermediary specified!");
return this.chains[chainId as C].wrappers[SwapType.TRUSTED_FROM_BTC].create(signer, amount, useUrl, refundAddress);
}
create<C extends ChainIds<T>>(signer: string, srcToken: BtcToken<true>, dstToken: SCToken<C>, amount: bigint, exactIn: boolean, lnurlWithdraw?: string | LNURLWithdraw): Promise<FromBTCLNSwap<T[C]>>;
create<C extends ChainIds<T>>(signer: string, srcToken: BtcToken<false>, dstToken: SCToken<C>, amount: bigint, exactIn: boolean): Promise<(SupportsSwapType<T[C], SwapType.SPV_VAULT_FROM_BTC> extends true ? SpvFromBTCSwap<T[C]> : FromBTCSwap<T[C]>)>;
create<C extends ChainIds<T>>(signer: string, srcToken: SCToken<C>, dstToken: BtcToken<false>, amount: bigint, exactIn: boolean, address: string): Promise<ToBTCSwap<T[C]>>;
create<C extends ChainIds<T>>(signer: string, srcToken: SCToken<C>, dstToken: BtcToken<true>, amount: bigint, exactIn: boolean, lnurlPay: string | LNURLPay): Promise<ToBTCLNSwap<T[C]>>;
create<C extends ChainIds<T>>(signer: string, srcToken: SCToken<C>, dstToken: BtcToken<true>, amount: bigint, exactIn: false, lightningInvoice: string): Promise<ToBTCLNSwap<T[C]>>;
create<C extends ChainIds<T>>(signer: string, srcToken: Token<C>, dstToken: Token<C>, amount: bigint, exactIn: boolean, addressLnurlLightningInvoice?: string | LNURLWithdraw | LNURLPay): Promise<ISwap<T[C]>>;
/**
* Creates a swap from srcToken to dstToken, of a specific token amount, either specifying input amount (exactIn=true)
* or output amount (exactIn=false), NOTE: For regular -> BTC-LN (lightning) swaps the passed amount is ignored and
* invoice's pre-set amount is used instead.
* @deprecated Use swap() instead
*
* @param signer Smartchain (Solana, Starknet, etc.) address of the user
* @param srcToken Source token of the swap, user pays this token
* @param dstToken Destination token of the swap, user receives this token
* @param amount Amount of the swap
* @param exactIn Whether the amount specified is an input amount (exactIn=true) or an output amount (exactIn=false)
* @param addressLnurlLightningInvoice Bitcoin on-chain address, lightning invoice, LNURL-pay to pay or
* LNURL-withdrawal to withdraw money from
*/
create<C extends ChainIds<T>>(signer: string, srcToken: Token<C>, dstToken: Token<C>, amount: bigint, exactIn: boolean, addressLnurlLightningInvoice?: string | LNURLWithdraw | LNURLPay): Promise<ISwap<T[C]>> {
if(srcToken.chain==="BTC") {
return this.swap(srcToken, dstToken, amount, exactIn, addressLnurlLightningInvoice as any, signer);
} else {
return this.swap(srcToken, dstToken, amount, exactIn, signer, addressLnurlLightningInvoice as any);
}
}
swap<C extends ChainIds<T>>(srcToken: BtcToken<true>, dstToken: SCToken<C>, amount: bigint, exactIn: boolean, src: undefined | string | LNURLWithdraw, dstSmartchainWallet: string, options?: FromBTCLNOptions): Promise<FromBTCLNSwap<T[C]>>;
swap<C extends ChainIds<T>>(srcToken: BtcToken<false>, dstToken: SCToken<C>, amount: bigint, exactIn: boolean, src: undefined | string, dstSmartchainWallet: string, options?: (SupportsSwapType<T[C], SwapType.SPV_VAULT_FROM_BTC> extends true ? SpvFromBTCOptions : FromBTCOptions)): Promise<(SupportsSwapType<T[C], SwapType.SPV_VAULT_FROM_BTC> extends true ? SpvFromBTCSwap<T[C]> : FromBTCSwap<T[C]>)>;
swap<C extends ChainIds<T>>(srcToken: SCToken<C>, dstToken: BtcToken<false>, amount: bigint, exactIn: boolean, src: string, dstAddress: string, options?: ToBTCOptions): Promise<ToBTCSwap<T[C]>>;
swap<C extends ChainIds<T>>(srcToken: SCToken<C>, dstToken: BtcToken<true>, amount: bigint, exactIn: boolean, src: string, dstLnurlPay: string | LNURLPay, options?: ToBTCLNOptions & {comment?: string}): Promise<ToBTCLNSwap<T[C]>>;
swap<C extends ChainIds<T>>(srcToken: SCToken<C>, dstToken: BtcToken<true>, amount: bigint, exactIn: false, src: string, dstLightningInvoice: string, options?: ToBTCLNOptions): Promise<ToBTCLNSwap<T[C]>>;
swap<C extends ChainIds<T>>(srcToken: Token<C>, dstToken: Token<C>, amount: bigint, exactIn: boolean, src: undefined | string | LNURLWithdraw, dst: string | LNURLPay, options?: FromBTCLNOptions | SpvFromBTCOptions | FromBTCOptions | ToBTCOptions | (ToBTCLNOptions & {comment?: string})): Promise<ISwap<T[C]>>;
/**
* Creates a swap from srcToken to dstToken, of a specific token amount, either specifying input amount (exactIn=true)
* or output amount (exactIn=false), NOTE: For regular SmartChain -> BTC-LN (lightning) swaps the passed amount is ignored and
* invoice's pre-set amount is used instead, use LNURL-pay for dynamic amounts
*
* @param srcToken Source token of the swap, user pays this token
* @param dstToken Destination token of the swap, user receives this token
* @param amount Amount of the swap
* @param exactIn Whether the amount specified is an input amount (exactIn=true) or an output amount (exactIn=false)
* @param src Source wallet/lnurl-withdraw of the swap
* @param dst Destination smart chain address, bitcoin on-chain address, lightning invoice, LNURL-pay
* @param options Options for the swap
*/
swap<C extends ChainIds<T>>(
srcToken: Token<C>,
dstToken: Token<C>,
amount: bigint,
exactIn: boolean,
src: undefined | string | LNURLWithdraw,
dst: string | LNURLPay,
options?: FromBTCLNOptions | SpvFromBTCOptions | FromBTCOptions | ToBTCOptions | (ToBTCLNOptions & {comment?: string})
): Promise<ISwap<T[C]>> {
if(srcToken.chain==="BTC") {
if(dstToken.chain==="SC") {
if(typeof(dst)!=="string") throw new Error("Destination for BTC/BTC-LN -> smart chain swaps must be a smart chain address!");
if(srcToken.lightning) {
//FROM_BTCLN
if(src!=null) {
if(typeof(src)!=="string" && !isLNURLWithdraw(src)) throw new Error("LNURL must be a string or LNURLWithdraw object!");
return this.createFromBTCLNSwapViaLNURL(dstToken.chainId, dst, dstToken.address, src, amount, !exactIn);
} else {
return this.createFromBTCLNSwap(dstToken.chainId, dst, dstToken.address, amount, !exactIn, undefined, options as any);
}
} else {
//FROM_BTC
if(this.supportsSwapType(dstToken.chainId, SwapType.SPV_VAULT_FROM_BTC)) {
return this.createFromBTCSwapNew(dstToken.chainId, dst, dstToken.address, amount, !exactIn, undefined, options as any);
} else {
return this.createFromBTCSwap(dstToken.chainId, dst, dstToken.address, amount, !exactIn, undefined, options as any);
}
}
}
} else {
if(dstToken.chain==="BTC") {
if(typeof(src)!=="string") throw new Error("Source address for BTC/BTC-LN -> smart chain swaps must be a smart chain address!");
if(dstToken.lightning) {
//TO_BTCLN
if(typeof(dst)!=="string" && !isLNURLPay(dst)) throw new Error("Destination LNURL link/lightning invoice must be a string or LNURLPay object!");
if(isLNURLPay(dst) || this.Utils.isValidLNURL(dst)) {
return this.createToBTCLNSwapViaLNURL(srcToken.chainId, src, srcToken.address, dst, amount, exactIn, undefined, options as any);
} else if(this.Utils.isLightningInvoice(dst)) {
if(!this.Utils.isValidLightningInvoice(dst))
throw new Error("Invalid lightning invoice specified, lightning invoice MUST contain pre-set amount!");
if(exactIn)
throw new Error("Only exact out swaps are possible with lightning invoices, use LNURL links for exact in lightning swaps!");
return this.createToBTCLNSwap(srcToken.chainId, src, srcToken.address, dst, undefined, options as any);
} else {
throw new Error("Supplied parameter is not LNURL link nor lightning invoice (bolt11)!");
}
} else {
//TO_BTC
if(typeof(dst)!=="string") throw new Error("Destination bitcoin address must be a string!");
return this.createToBTCSwap(srcToken.chainId, src, srcToken.address, dst, amount, exactIn, undefined, options as any);
}
}
}
throw new Error("Unsupported swap type");
}
/**
* Returns all swaps
*/
getAllSwaps(): Promise<ISwap[]>;
/**
* Returns all swaps for the specific chain, and optionally also for a specific signer's address
*/
getAllSwaps<C extends ChainIds<T>>(chainId: C, signer?: string): Promise<ISwap<T[C]>[]>;
async getAllSwaps<C extends ChainIds<T>>(chainId?: C, signer?: string): Promise<ISwap[]> {
const queryParams: QueryParams[] = [];
if(signer!=null) queryParams.push({key: "intiator", value: signer});
if(chainId==null) {
const res: ISwap[][] = await Promise.all(Object.keys(this.chains).map((chainId) => {
const {unifiedSwapStorage, reviver} = this.chains[chainId];
return unifiedSwapStorage.query([queryParams], reviver);
}));
return res.flat();
} else {
const {unifiedSwapStorage, reviver} = this.chains[chainId];
return await unifiedSwapStorage.query([queryParams], reviver);
}
}
/**
* Returns all swaps where an action is required (either claim or refund)
*/
getActionableSwaps(): Promise<ISwap[]>;
/**
* Returns swaps where an action is required (either claim or refund) for the specific chain, and optionally also for a specific signer's address
*/
getActionableSwaps<C extends ChainIds<T>>(chainId: C, signer?: string): Promise<ISwap<T[C]>[]>;
async getActionableSwaps<C extends ChainIds<T>>(chainId?: C, signer?: string): Promise<ISwap[]> {
if(chainId==null) {
const res: ISwap[][] = await Promise.all(Object.keys(this.chains).map((chainId) => {
const {unifiedSwapStorage, reviver, wrappers} = this.chains[chainId];
const queryParams: Array<QueryParams[]> = [];
for(let key in wrappers) {
const wrapper = wrappers[key];
const swapTypeQueryParams: QueryParams[] = [{key: "type", value: wrapper.TYPE}];
if(signer!=null) swapTypeQueryParams.push({key: "intiator", value: signer});
swapTypeQueryParams.push({key: "state", value: wrapper.pendingSwapStates});
queryParams.push(swapTypeQueryParams);
}
return unifiedSwapStorage.query(queryParams, reviver);
}));
return res.flat().filter(swap => swap.requiresAction());
} else {
const {unifiedSwapStorage, reviver, wrappers} = this.chains[chainId];
const queryParams: Array<QueryParams[]> = [];
for(let key in wrappers) {
const wrapper = wrappers[key];
const swapTypeQueryParams: QueryParams[] = [{key: "type", value: wrapper.TYPE}];
if(signer!=null) swapTypeQueryParams.push({key: "intiator", value: signer});
swapTypeQueryParams