import {ISwapPrice} from "../prices/abstract/ISwapPrice";
import {
    BitcoinNetwork, BitcoinRpc, BitcoinRpcWithAddressIndex, BtcBlock,
    BtcRelay,
    ChainData,
    ChainSwapType,
    ChainType, LightningNetworkApi,
    Messenger,
    RelaySynchronizer, SpvWithdrawalClaimedState, SpvWithdrawalFrontedState, SwapCommitState, SwapContract, SwapData
} from "@atomiqlabs/base";
import {
    ToBTCLNOptions,
    ToBTCLNWrapper
} from "../swaps/escrow_swaps/tobtc/ln/ToBTCLNWrapper";
import {ToBTCOptions, ToBTCWrapper} from "../swaps/escrow_swaps/tobtc/onchain/ToBTCWrapper";
import {FromBTCLNOptions, FromBTCLNWrapper} from "../swaps/escrow_swaps/frombtc/ln/FromBTCLNWrapper";
import {FromBTCOptions, FromBTCWrapper} from "../swaps/escrow_swaps/frombtc/onchain/FromBTCWrapper";
import {IntermediaryDiscovery, MultichainSwapBounds, SwapBounds} from "../intermediaries/IntermediaryDiscovery";
import {decode as bolt11Decode} from "@atomiqlabs/bolt11";
import {ISwap} from "../swaps/ISwap";
import {IntermediaryError} from "../errors/IntermediaryError";
import {SwapType} from "../enums/SwapType";
import {FromBTCLNSwap} from "../swaps/escrow_swaps/frombtc/ln/FromBTCLNSwap";
import {FromBTCSwap} from "../swaps/escrow_swaps/frombtc/onchain/FromBTCSwap";
import {ToBTCLNSwap} from "../swaps/escrow_swaps/tobtc/ln/ToBTCLNSwap";
import {ToBTCSwap} from "../swaps/escrow_swaps/tobtc/onchain/ToBTCSwap";
import {LnForGasWrapper} from "../swaps/trusted/ln/LnForGasWrapper";
import {LnForGasSwap} from "../swaps/trusted/ln/LnForGasSwap";
import {EventEmitter} from "events";
import {Intermediary} from "../intermediaries/Intermediary";
import {ISwapWrapper, WrapperCtorTokens} from "../swaps/ISwapWrapper";
import {bigIntCompare, bigIntMax, bigIntMin, fromDecimal, objectMap, randomBytes} from "../utils/Utils";
import {OutOfBoundsError} from "../errors/RequestError";
import {SwapperWithChain} from "./SwapperWithChain";
import {OnchainForGasSwap} from "../swaps/trusted/onchain/OnchainForGasSwap";
import {OnchainForGasWrapper} from "../swaps/trusted/onchain/OnchainForGasWrapper";
import {BTC_NETWORK, NETWORK, TEST_NETWORK} from "@scure/btc-signer/utils";
import {IUnifiedStorage, QueryParams} from "../storage/IUnifiedStorage";
import {
    UnifiedSwapStorage,
    UnifiedSwapStorageCompositeIndexes,
    UnifiedSwapStorageIndexes
} from "../storage/UnifiedSwapStorage";
import {UnifiedSwapEventListener} from "../events/UnifiedSwapEventListener";
import {IToBTCSwap} from "../swaps/escrow_swaps/tobtc/IToBTCSwap";
import {SpvFromBTCOptions, SpvFromBTCWrapper} from "../swaps/spv_swaps/SpvFromBTCWrapper";
import {SpvFromBTCSwap} from "../swaps/spv_swaps/SpvFromBTCSwap";
import {SwapperUtils} from "./SwapperUtils";
import {FromBTCLNAutoOptions, FromBTCLNAutoWrapper} from "../swaps/escrow_swaps/frombtc/ln_auto/FromBTCLNAutoWrapper";
import {FromBTCLNAutoSwap} from "../swaps/escrow_swaps/frombtc/ln_auto/FromBTCLNAutoSwap";
import {UserError} from "../errors/UserError";
import {SwapAmountType} from "../enums/SwapAmountType";
import {IClaimableSwap} from "../swaps/IClaimableSwap";
import {correctClock} from "../utils/AutomaticClockDriftCorrection";
import {isSwapType, SwapProtocolInfo, SwapTypeMapping} from "../utils/SwapUtils";
import {IndexedDBUnifiedStorage} from "../storage-browser/IndexedDBUnifiedStorage";
import {TokenAmount, toTokenAmount} from "../types/TokenAmount";
import {BitcoinTokens, BtcToken, isBtcToken, isSCToken, SCToken, Token} from "../types/Token";
import {AmountData} from "../types/AmountData";
import {getLogger} from "../utils/Logger";
import {isLNURLWithdraw, LNURLWithdraw} from "../types/lnurl/LNURLWithdraw";
import {isLNURLPay, LNURLPay} from "../types/lnurl/LNURLPay";
import {tryWithRetries} from "../utils/RetryUtils";
import {NotNever} from "../utils/TypeUtils";
import {IEscrowSwap} from "../swaps/escrow_swaps/IEscrowSwap";
import {LightningInvoiceCreateService, isLightningInvoiceCreateService} from "../types/wallets/LightningInvoiceCreateService";
import {SwapSide} from "../enums/SwapSide";
import {IntermediaryAPI} from "../intermediaries/apis/IntermediaryAPI";
import {BitcoinWalletUtxo, BitcoinWalletUtxoBase, IBitcoinWallet} from "../bitcoin/wallet/IBitcoinWallet";
import {MinimalBitcoinWalletInterface} from "../types/wallets/MinimalBitcoinWalletInterface";
import {toBitcoinWallet} from "../utils/BitcoinWalletUtils";
import {getSignedKeyBasedAuthHandler} from "../intermediaries/auth/SignedKeyBasedAuth";

/**
 * Configuration options for the Swapper
 * @category Core
 */
export type SwapperOptions = {
    /**
     * Manual override for the intermediary (LP) URLs for the SDK to use, by default these are fetched automatically
     * from the registry
     */
    intermediaryUrl?: string | string[],
    /**
     * Registry URL for where to look for active intermediary (LP) endpoint URLs
     */
    registryUrl?: string,

    /**
     * Bitcoin network to use for the swaps,
     */
    bitcoinNetwork?: BitcoinNetwork,

    /**
     * Timeout (in milliseconds) for HTTP GET requests done by the SDK
     */
    getRequestTimeout?: number,
    /**
     * Timeout (in milliseconds) for HTTP POST requests done by the SDK
     */
    postRequestTimeout?: number,
    /**
     * Additional parameters to be sent to the intermediaries (LPs), when requesting quotes from them
     */
    defaultAdditionalParameters?: {[key: string]: any},
    /**
     * Optional name prefix to use when creating a swap storage, you can use this to create separate storage
     *  instances that don't overlap.
     */
    storagePrefix?: string,
    /**
     * Sets the default intermediary (LP) to use for the trusted gas swaps, if not set the SDK uses a default one
     */
    defaultTrustedIntermediaryUrl?: string,

    /**
     * A function callback to retrieve a specific named storage container for swap persistency. If not present, the
     *  default IndexedDB storage adapter is used. When you use the SDK in non-browser based environments you need to
     *  provide this callback such that the SDK is able to use a custom storage adapter.
     *
     * @param storageName Name of the container to retrieve
     */
    swapStorage?: (storageName: string) => IUnifiedStorage<UnifiedSwapStorageIndexes, UnifiedSwapStorageCompositeIndexes>,

    /**
     * By setting this flag, the swapper doesn't schedule automatic tick timers. To make sure the swap states are
     * properly updated (e.g. the expired swaps properly move to the expired state), you should call the
     *  {@link Swapper._syncSwaps} function periodically. This flag should be set when you run an environment that
     *  doesn't support long-running timers - e.g. serverless environments like Azure Function Apps or AWS Lambda
     */
    noTimers?: boolean,
    /**
     * By setting this flag, the swapper doesn't subscribe to on-chain events. To make sure the swap states are
     *  properly updated you should either call the {@link Swapper._syncSwaps} function periodically, or use the
     *  {@link Swapper._pollChainEvents} function to manually poll for on-chain events. This flag should be set
     *  when you run an environment that doesn't support long-running timers and websocket connections - e.g.
     *  serverless environments like Azure Function Apps or AWS Lambda
     */
    noEvents?: boolean,
    /**
     * By setting this flag, the swap objects will not be cached in the SDK and instead will always be loaded from
     *  the persistent storage. By default, the SDK uses a `WeakRef` mapping of swaps, to ensure that when the same
     *  swap is loaded concurrently, it returns the same object reference to both, making the changes on the object
     *  atomic. This flag should be set to `true` when running in an environment where multiple instances of the SDK
     *  access the same swap database - e.g. serverless environments like Azure Function Apps or AWS Lambda
     */
    noSwapCache?: boolean,
    /**
     * Skip checking past swaps when the swapper is initiated with {@link Swapper.init}, you can call the
     * {@link Swapper._syncSwaps} function later, to check the swaps. By default, the SDK checks the state
     * of all the known swaps during init.
     */
    dontCheckPastSwaps?: boolean,
    /**
     * Skip fetching the LPs when the swapper is initiated with {@link Swapper.init}, this means the list of available
     *  tokens and swap limits won't be available immediately. LPs will be fetched automatically later, when a swap
     *  is requested
     */
    dontFetchLPs?: boolean,
    /**
     * Defaults to `true`, this means every swap regardless of it being initiated (i.e. when `commit()`, `execute()` or
     *  `waitTillPayment` is called) is saved to the persistent storage. This is a reasonable default for when you
     *  want to only create a swap, and then later on retrieve it with the `swapper.getSwapById()` function.
     *
     * Setting this to `false` means the SDK only saves and persists swaps that are considered initiated, i.e. when
     *  `commit()`, `execute()` or `waitTillPayment()` is called (or their respective txs... prefixed variations). This
     *  might save calls to the persistent storage for swaps that are never initiated. This is useful in e.g.
     *  frontend implementations where the frontend holds the swap object reference until it is initiated anyway, not
     *  necessitating the saving of the swap data to the persistent storage until it is actually initiated.
     */
    saveUninitializedSwaps?: boolean,
    /**
     * Automatically checks system time on initialize, if the system time drifts too far from the actual time
     *  (as checked from multiple server sources) it adjusts the `Date.now()` function to return proper actual time.
     */
    automaticClockDriftCorrection?: boolean,
    /**
     * Used in centralized API deployments to allow higher rate limits from LPs
     */
    signedKeyBasedAuth?: {
        certificate: string,
        privateKey: string
    },

    /**
     * If you set the option to `true` the chains for which the RPC is unresponsive are skipped and not initialized
     *  letting the swapper continue with only the available chains with responsive RPCs
     */
    gracefullyHandleChainErrors?: boolean,
};

/**
 * Type representing multiple blockchain configurations
 * @category Core
 */
export type MultiChain = {
    [chainIdentifier in string]: ChainType;
};

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>,
        [SwapType.FROM_BTCLN_AUTO]: FromBTCLNAutoWrapper<T>
    }
    chainEvents: T["Events"],
    chainInterface: T["ChainInterface"],
    unifiedChainEvents: UnifiedSwapEventListener<T>,
    unifiedSwapStorage: UnifiedSwapStorage<T>,
    reviver: (val: any) => ISwap<T>,
    defaultVersion: string,

    versionedContracts: {
        [contractVersion: string]: {
            swapContract: T["Contract"],
            spvVaultContract: T["SpvVaultContract"],
            btcRelay: BtcRelay<any, T["TX"], BtcBlock, T["Signer"]>,
            synchronizer: RelaySynchronizer<any, T["TX"], BtcBlock>,
        }
    }
};

type MultiChainData<T extends MultiChain> = {
    [chainIdentifier in keyof T]: ChainSpecificData<T[chainIdentifier]>
};

type CtorMultiChainData<T extends MultiChain> = {
    [chainIdentifier in keyof T]: ChainData<T[chainIdentifier]>
};

type SwapperCtorTokens<T extends MultiChain = MultiChain> = {
    ticker: string,
    name: string,
    chains: {[chainId in ChainIds<T>]?: {
        address: string,
        decimals: number,
        displayDecimals?: number
    }}
}[];

/**
 * Type extracting chain identifiers from a MultiChain type
 * @category Core
 */
export type ChainIds<T extends MultiChain> = keyof T & string;

/**
 * Type helper to check if a chain supports a specific swap type
 * @category Core
 */
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 :
    Type extends SwapType.FROM_BTCLN_AUTO ? (C["Contract"]["supportsInitWithoutClaimer"] extends true ? true : false) :
        NotNever<C["Contract"]>;

/**
 * Core orchestrator for all atomiq swap operations
 *
 * @category Core
 */
export class Swapper<T extends MultiChain> extends EventEmitter<{
    lpsRemoved: [Intermediary[]],
    lpsAdded: [Intermediary[]],
    swapState: [ISwap],
    swapLimitsChanged: []
}> {
    private readonly logger = getLogger(this.constructor.name+": ");
    private readonly swapStateListener: (swap: ISwap) => void;

    private defaultTrustedIntermediary?: Intermediary;
    private readonly bitcoinNetwork: BitcoinNetwork;
    private readonly options: SwapperOptions;
    /**
     * Data propagation layer used for broadcasting messages to watchtowers
     */
    private readonly messenger: Messenger;

    /**
     * A dictionary of smart chains used by the SDK
     * @internal
     */
    readonly _chains: MultiChainData<T>;
    /**
     * Bitcoin RPC for fetching bitcoin chain data
     * @internal
     */
    readonly _bitcoinRpc: BitcoinRpcWithAddressIndex<any>;
    /**
     * Bitcoin network specification
     * @internal
     */
    readonly _btcNetwork: BTC_NETWORK;
    /**
     * Token data indexed by chain identifier and token addresses
     * @internal
     */
    readonly _tokens: {
        [chainId: string]: {
            [tokenAddress: string]: SCToken
        }
    };
    /**
     * Token data indexed by chain identifier and token tickers
     * @internal
     */
    readonly _tokensByTicker: {
        [chainId: string]: {
            [tokenTicker: string]: SCToken
        }
    };

    /**
     * Pricing API used by the SDK
     */
    readonly prices: ISwapPrice<T>;
    /**
     * API for contacting LPs
     */
    readonly lpApi: IntermediaryAPI;
    /**
     * Intermediary discovery instance
     */
    readonly intermediaryDiscovery: IntermediaryDiscovery;
    /**
     * Miscellaneous utility functions
     */
    readonly Utils: SwapperUtils<T>;

    /**
     * @internal
     */
    constructor(
        bitcoinRpc: BitcoinRpcWithAddressIndex<any>,
        lightningApi: LightningNetworkApi,
        bitcoinSynchronizer: (btcRelay: BtcRelay<any, any, any>) => RelaySynchronizer<any, any, any>,
        chainsData: CtorMultiChainData<T>,
        pricing: ISwapPrice<T>,
        tokens: SCToken[],
        messenger: Messenger,
        options?: SwapperOptions
    ) {
        super();
        const storagePrefix = options?.storagePrefix ?? "atomiq-";

        options ??= {};
        options.saveUninitializedSwaps ??= true;
        options.bitcoinNetwork = options.bitcoinNetwork==null ? BitcoinNetwork.TESTNET : options.bitcoinNetwork;
        const swapStorage = options.swapStorage ??= (name: string) => new IndexedDBUnifiedStorage(name);

        this.options = options;

        this.bitcoinNetwork = options.bitcoinNetwork;
        this._btcNetwork = options.bitcoinNetwork===BitcoinNetwork.MAINNET ? NETWORK :
            (options.bitcoinNetwork===BitcoinNetwork.TESTNET || options.bitcoinNetwork===BitcoinNetwork.TESTNET4) ? TEST_NETWORK : {
                bech32: 'bcrt',
                pubKeyHash: 111,
                scriptHash: 196,
                wif: 239
            };
        this.Utils = new SwapperUtils(this);

        this.prices = pricing;
        this._bitcoinRpc = bitcoinRpc;
        this.messenger = messenger;

        this._tokens = {};
        this._tokensByTicker = {};
        for(let tokenData of tokens) {
            const chainId = tokenData.chainId;
            this._tokens[chainId] ??= {};
            this._tokensByTicker[chainId] ??= {};
            this._tokens[chainId][tokenData.address] = this._tokensByTicker[chainId][tokenData.ticker] = tokenData;
        }

        const lpApi = new IntermediaryAPI(
            this.options.signedKeyBasedAuth!=null
                ? getSignedKeyBasedAuthHandler(this.options.signedKeyBasedAuth.certificate, this.options.signedKeyBasedAuth.privateKey)
                : undefined
        );
        this.lpApi = lpApi;

        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) => {
            let {
                chainInterface, chainEvents, chainId,
                btcRelay,
                swapContract, swapDataConstructor,
                spvVaultContract, spvVaultWithdrawalDataConstructor, spvVaultDataConstructor,
                defaultVersion, versions
            } = chainData;

            defaultVersion ??= "v1";

            if(versions==null) {
                versions = {
                    [defaultVersion]: {
                        btcRelay,
                        swapContract,
                        swapDataConstructor,
                        spvVaultContract,
                        spvVaultDataConstructor,
                        spvVaultWithdrawalDataConstructor
                    }
                }
            }

            const versionedContracts = objectMap(versions, (value, key) => {
                return {
                    swapContract: value.swapContract,
                    spvVaultContract: value.spvVaultContract,
                    btcRelay: value.btcRelay,
                    synchronizer: bitcoinSynchronizer(value.btcRelay)
                };
            });

            const storageHandler = swapStorage(storagePrefix + 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,
                pricing,
                this._tokens[chainId],
                versions,
                lpApi,
                {
                    getRequestTimeout: this.options.getRequestTimeout,
                    postRequestTimeout: this.options.postRequestTimeout,
                    saveUninitializedSwaps: this.options.saveUninitializedSwaps,
                }
            );
            wrappers[SwapType.TO_BTC] = new ToBTCWrapper<T[InputKey]>(
                key,
                unifiedSwapStorage,
                unifiedChainEvents,
                chainInterface,
                pricing,
                this._tokens[chainId],
                versions,
                this._bitcoinRpc,
                lpApi,
                {
                    getRequestTimeout: this.options.getRequestTimeout,
                    postRequestTimeout: this.options.postRequestTimeout,
                    saveUninitializedSwaps: this.options.saveUninitializedSwaps,
                    bitcoinNetwork: this._btcNetwork
                }
            );
            wrappers[SwapType.FROM_BTCLN] = new FromBTCLNWrapper<T[InputKey]>(
                key,
                unifiedSwapStorage,
                unifiedChainEvents,
                chainInterface,
                pricing,
                this._tokens[chainId],
                versions,
                lightningApi,
                lpApi,
                {
                    getRequestTimeout: this.options.getRequestTimeout,
                    postRequestTimeout: this.options.postRequestTimeout,
                    saveUninitializedSwaps: this.options.saveUninitializedSwaps,
                    unsafeSkipLnNodeCheck: this.bitcoinNetwork===BitcoinNetwork.TESTNET4 || this.bitcoinNetwork===BitcoinNetwork.REGTEST
                }
            );
            wrappers[SwapType.FROM_BTC] = new FromBTCWrapper<T[InputKey]>(
                key,
                unifiedSwapStorage,
                unifiedChainEvents,
                chainInterface,
                pricing,
                this._tokens[chainId],
                versions,
                versionedContracts,
                this._bitcoinRpc,
                lpApi,
                {
                    getRequestTimeout: this.options.getRequestTimeout,
                    postRequestTimeout: this.options.postRequestTimeout,
                    saveUninitializedSwaps: this.options.saveUninitializedSwaps,
                    bitcoinNetwork: this._btcNetwork
                }
            );
            wrappers[SwapType.TRUSTED_FROM_BTCLN] = new LnForGasWrapper<T[InputKey]>(
                key,
                unifiedSwapStorage,
                unifiedChainEvents,
                chainInterface,
                pricing,
                this._tokens[chainId],
                lpApi,
                {
                    getRequestTimeout: this.options.getRequestTimeout,
                    postRequestTimeout: this.options.postRequestTimeout,
                    saveUninitializedSwaps: this.options.saveUninitializedSwaps,
                }
            );
            wrappers[SwapType.TRUSTED_FROM_BTC] = new OnchainForGasWrapper<T[InputKey]>(
                key,
                unifiedSwapStorage,
                unifiedChainEvents,
                chainInterface,
                pricing,
                this._tokens[chainId],
                bitcoinRpc,
                lpApi,
                {
                    getRequestTimeout: this.options.getRequestTimeout,
                    postRequestTimeout: this.options.postRequestTimeout,
                    saveUninitializedSwaps: this.options.saveUninitializedSwaps,
                    bitcoinNetwork: this._btcNetwork
                }
            );

            // This is gated on the default version of the contracts
            if(spvVaultContract!=null) {
                wrappers[SwapType.SPV_VAULT_FROM_BTC] = new SpvFromBTCWrapper<T[InputKey]>(
                    key,
                    unifiedSwapStorage,
                    unifiedChainEvents,
                    chainInterface,
                    pricing,
                    this._tokens[chainId],
                    versions,
                    versionedContracts,
                    bitcoinRpc,
                    lpApi,
                    {
                        getRequestTimeout: this.options.getRequestTimeout,
                        postRequestTimeout: this.options.postRequestTimeout,
                        saveUninitializedSwaps: this.options.saveUninitializedSwaps,
                        bitcoinNetwork: this._btcNetwork
                    }
                );
            }

            // This is gated on the default version of the contracts
            if(swapContract.supportsInitWithoutClaimer) {
                wrappers[SwapType.FROM_BTCLN_AUTO] = new FromBTCLNAutoWrapper<T[InputKey]>(
                    key,
                    unifiedSwapStorage,
                    unifiedChainEvents,
                    chainInterface,
                    pricing,
                    this._tokens[chainId],
                    versions,
                    lightningApi,
                    this.messenger,
                    lpApi,
                    {
                        getRequestTimeout: this.options.getRequestTimeout,
                        postRequestTimeout: this.options.postRequestTimeout,
                        saveUninitializedSwaps: this.options.saveUninitializedSwaps,
                        unsafeSkipLnNodeCheck: this.bitcoinNetwork===BitcoinNetwork.TESTNET4 || this.bitcoinNetwork===BitcoinNetwork.REGTEST
                    }
                );
            }

            Object.keys(wrappers).forEach(key => wrappers[key].events.on("swapState", this.swapStateListener));

            const reviver = (val: any) => {
                const wrapper: ISwapWrapper<any, any> = wrappers[val.type];
                if(wrapper==null) return null;
                return new wrapper._swapDeserializer(wrapper, val);
            };

            return {
                chainEvents,
                chainInterface,

                wrappers,

                unifiedChainEvents,
                unifiedSwapStorage,

                defaultVersion,

                reviver,

                versionedContracts
            }
        });

        const contracts = objectMap(chainsData, (data) => data.versions ?? {[data.defaultVersion ?? "v1"]: {swapContract: data.swapContract, spvVaultContract: data.spvVaultContract}});
        if(options.intermediaryUrl!=null) {
            this.intermediaryDiscovery = new IntermediaryDiscovery(contracts, lpApi, options.registryUrl, Array.isArray(options.intermediaryUrl) ? options.intermediaryUrl : [options.intermediaryUrl], options.getRequestTimeout);
        } else {
            this.intermediaryDiscovery = new IntermediaryDiscovery(contracts, lpApi, options.registryUrl, undefined, options.getRequestTimeout);
        }

        this.intermediaryDiscovery.on("removed", (intermediaries: Intermediary[]) => {
            this.emit("lpsRemoved", intermediaries);
        });

        this.intermediaryDiscovery.on("added", (intermediaries: Intermediary[]) => {
            this.emit("lpsAdded", intermediaries);
        });
    }

    private async _init(): Promise<void> {
        this.logger.debug("init(): Initializing swapper");

        const abortController = new AbortController();

        const promises: Promise<void>[] = [];
        let automaticClockDriftCorrectionPromise: Promise<void> | undefined = undefined;
        if(this.options.automaticClockDriftCorrection) {
            promises.push(automaticClockDriftCorrectionPromise = tryWithRetries(correctClock, undefined, undefined, abortController.signal).catch((err) => {
                abortController.abort(err);
            }));
        }

        this.logger.debug("init(): Initializing intermediary discovery");
        if(!this.options.dontFetchLPs) promises.push(this.intermediaryDiscovery.init(abortController.signal).catch(err => {
            if(abortController.signal.aborted) return;
            this.logger.error("init(): Failed to fetch intermediaries/LPs: ", err);
        }));

        if(this.options.defaultTrustedIntermediaryUrl!=null) {
            promises.push(
                this.intermediaryDiscovery.getIntermediary(this.options.defaultTrustedIntermediaryUrl, abortController.signal)
                    .then(val => {
                        if(val==null) throw new Error("Cannot get trusted LP");
                        this.defaultTrustedIntermediary = val;
                    })
                    .catch(err => {
                        if(abortController.signal.aborted) return;
                        this.logger.error("init(): Failed to contact trusted LP url: ", err);
                    })
            );
        }

        if(automaticClockDriftCorrectionPromise!=null) {
            //We should await the promises here before checking the swaps
            await automaticClockDriftCorrectionPromise;
        }

        const chainPromises = [];
        for(let chainIdentifier in this._chains) {
            chainPromises.push((async() => {
                const {
                    chainInterface,
                    versionedContracts,
                    unifiedChainEvents,
                    unifiedSwapStorage,
                    wrappers,
                    reviver
                } = this._chains[chainIdentifier];

                try {
                    const _chainInterface: any = chainInterface;
                    if(_chainInterface.verifyNetwork!=null) {
                        await _chainInterface.verifyNetwork(this.bitcoinNetwork);
                    }

                    for(let contractVersion in versionedContracts) {
                        await versionedContracts[contractVersion].swapContract.start();
                        this.logger.debug("init(): Intialized swap contract: "+chainIdentifier+` version: ${contractVersion}`);
                    }

                    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;
                            }
                        )
                    }

                    await unifiedChainEvents.start(this.options.noEvents);
                    this.logger.debug("init(): Initialized events: "+chainIdentifier);
                } catch (e) {
                    if(!this.options.gracefullyHandleChainErrors) throw e;
                    this.logger.error(`init(): Failed to initialize ${chainIdentifier} (skipped): `, e);
                    delete this._chains[chainIdentifier];
                    return;
                }

                for(let key in wrappers) {
                    // this.logger.debug("init(): Initializing "+SwapType[key]+": "+chainIdentifier);
                    await wrappers[key as unknown as SwapType].init(this.options.noTimers, this.options.dontCheckPastSwaps);
                }
            })());
        }
        await Promise.all(chainPromises);

        await Promise.all(promises);

        this.logger.debug("init(): Initializing messenger");
        await this.messenger.init();
    }

    private initPromise?: Promise<void>;
    private initialized: boolean = false;

    /**
     * Initializes the swap storage and loads existing swaps, needs to be called before any other action
     */
    async init(): Promise<void> {
        if(this.initialized) return;
        if(this.initPromise!=null) {
            await this.initPromise;
            return;
        }

        try {
            const promise = this._init();
            this.initPromise = promise;
            await promise;
            delete this.initPromise;
            this.initialized = true;
        } catch (e) {
            delete this.initPromise;
            throw e;
        }
    }

    /**
     * Whether the SDK is initialized (after {@link init} is called)
     */
    isInitialized(): boolean {
        return this.initialized;
    }

    /**
     * Stops listening for onchain events and closes this Swapper instance
     */
    async stop() {
        if(this.initPromise) await this.initPromise;
        for(let chainIdentifier in this._chains) {
            const {
                wrappers,
                unifiedChainEvents
            } = this._chains[chainIdentifier];
            for(let key in wrappers) {
                const wrapper = wrappers[key as unknown as SwapType];
                wrapper.events.removeListener("swapState", this.swapStateListener);
                await wrapper.stop();
            }
            await unifiedChainEvents.stop();
            await this.messenger.stop();
        }
        this.initialized = false;
    }

    /**
     * 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: { amount?: bigint, token: string, exactIn: boolean },
        swapType: SwapType,
        maxWaitTimeMS: number = 2000
    ): Promise<S> {
        if(!this.initialized) throw new Error("Swapper not initialized, init first with swapper.init()!");
        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 || amountData.amount==null) {
            //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 to execute the swap with, reloading intermediary database...");
            await this.intermediaryDiscovery.reloadIntermediaries();
            swapLimitsChanged = true;

            if(!inBtc || amountData.amount==null) {
                //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("Swap amount too low! Try swapping a higher amount.", 200, BigInt(min), BigInt(max));
                        if(amountData.amount > BigInt(max)) throw new OutOfBoundsError("Swap amount too high! Try swapping a lower amount.", 200, BigInt(min), BigInt(max));
                    }
                }
            }

            if(candidates.length===0) throw new Error("No intermediary found for the requested pair and amount! You can try swapping different pair or higher/lower amount.");
        }

        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: {}, output: {}});
                        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) {
                            let msg = "Swap amount too high or too low! Try swapping a different amount.";
                            if(amountData.amount!=null) {
                                if(min > amountData.amount) msg = "Swap amount too low! Try swapping a higher amount.";
                                if(max < amountData.amount) msg = "Swap amount too high! Try swapping a lower amount.";
                            }
                            reject(new OutOfBoundsError(msg, 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.getInput().rawAmount!, b.quote.getInput().rawAmount!);
                }
            });

            this.logger.debug("createSwap(): Sorted quotes, best price to worst: ", quotes);

            if(swapLimitsChanged) this.emit("swapLimitsChanged");

            const quote = quotes[0].quote;
            await quote._save();
            return quote;
        } catch (e) {
            if(swapLimitsChanged) this.emit("swapLimitsChanged");
            throw e;
        }
    }

    /**
     * Creates Smart chain -> Bitcoin ({@link SwapType.TO_BTC}) swap
     *
     * @param chainIdentifier Chain identifier string of the source smart chain
     * @param signer Signer's address on the source chain
     * @param tokenAddress Token address to pay with
     * @param address Recipient's bitcoin address
     * @param amount Amount to send in token based units (if `exactIn=true`) or receive in satoshis (if `exactIn=false`)
     * @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 Additional options for the swap
     */
    createToBTCSwap<ChainIdentifier extends ChainIds<T>>(
        chainIdentifier: ChainIdentifier,
        signer: string,
        tokenAddress: string,
        address: string,
        amount: bigint,
        exactIn: boolean = false,
        additionalParams: Record<string, any> | undefined = this.options.defaultAdditionalParameters,
        options?: ToBTCOptions
    ): Promise<ToBTCSwap<T[ChainIdentifier]>> {
        if(this._chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier);
        if(address.startsWith("bitcoin:")) {
            address = address.substring(8).split("?")[0];
        }
        if(!this.Utils.isValidBitcoinAddress(address)) throw new Error("Invalid bitcoin address");
        if(!this._chains[chainIdentifier].chainInterface.isValidAddress(signer, true)) throw new Error("Invalid "+chainIdentifier+" address");
        signer = this._chains[chainIdentifier].chainInterface.normalizeAddress(signer);
        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 Smart chain -> Bitcoin Lightning ({@link SwapType.TO_BTCLN}) swap
     *
     * @param chainIdentifier Chain identifier string of the source smart chain
     * @param signer Signer's address on the source chain
     * @param tokenAddress Token address to pay with
     * @param paymentRequest BOLT11 lightning network invoice to be paid (needs to have a fixed amount), and the swap
     *  amount is taken from this fixed amount, hence only exact output swaps are supported
     * @param additionalParams Additional parameters sent to the LP when creating the swap
     * @param options Additional options for the swap
     */
    async createToBTCLNSwap<ChainIdentifier extends ChainIds<T>>(
        chainIdentifier: ChainIdentifier,
        signer: string,
        tokenAddress: string,
        paymentRequest: string,
        additionalParams: Record<string, any> | undefined = this.options.defaultAdditionalParameters,
        options?: ToBTCLNOptions
    ): Promise<ToBTCLNSwap<T[ChainIdentifier]>> {
        if(this._chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier);
        if(paymentRequest.startsWith("lightning:")) paymentRequest = paymentRequest.substring(10);
        if(!this.Utils.isValidLightningInvoice(paymentRequest)) throw new Error("Invalid lightning network invoice");
        if(!this._chains[chainIdentifier].chainInterface.isValidAddress(signer, true)) throw new Error("Invalid "+chainIdentifier+" address");
        signer = this._chains[chainIdentifier].chainInterface.normalizeAddress(signer);
        const parsedPR = bolt11Decode(paymentRequest);
        if(parsedPR.millisatoshis==null) throw new Error("Invalid lightning network invoice, no msat value field!");
        const amountData = {
            amount: (BigInt(parsedPR.millisatoshis) + 999n) / 1000n,
            token: tokenAddress,
            exactIn: false as const
        };
        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 Smart chain -> Bitcoin Lightning ({@link SwapType.TO_BTCLN}) swap via LNURL-pay link
     *
     * @param chainIdentifier Chain identifier string of the source smart chain
     * @param signer Signer's address on the source chain
     * @param tokenAddress Token address to pay with
     * @param lnurlPay LNURL-pay link to use for the payment
     * @param amount Amount to send in token based units (if `exactIn=true`) or receive in satoshis (if `exactIn=false`)
     * @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 Additional options for the swap
     */
    async createToBTCLNSwapViaLNURL<ChainIdentifier extends ChainIds<T>>(
        chainIdentifier: ChainIdentifier,
        signer: string,
        tokenAddress: string,
        lnurlPay: string | LNURLPay,
        amount: bigint,
        exactIn: boolean = false,
        additionalParams: Record<string, any> | undefined = this.options.defaultAdditionalParameters,
        options?: ToBTCLNOptions & {comment?: string}
    ): Promise<ToBTCLNSwap<T[ChainIdentifier]>> {
        if(this._chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier);
        if(typeof(lnurlPay)==="string" && !this.Utils.isValidLNURL(lnurlPay)) throw new Error("Invalid LNURL-pay link");
        if(!this._chains[chainIdentifier].chainInterface.isValidAddress(signer, true)) throw new Error("Invalid "+chainIdentifier+" address");
        signer = this._chains[chainIdentifier].chainInterface.normalizeAddress(signer);
        const amountData = {
            amount,
            token: tokenAddress,
            exactIn
        };
        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 Smart chain -> Bitcoin Lightning ({@link SwapType.TO_BTCLN}) swap via {@link LightningInvoiceCreateService}
     *
     * @param chainIdentifier Chain identifier string of the source smart chain
     * @param signer Signer's address on the source chain
     * @param tokenAddress Token address to pay with
     * @param service Invoice create service object which facilitates the creation of fixed amount LN invoices
     * @param amount Amount to send in token based units (if `exactIn=true`) or receive in satoshis (if `exactIn=false`)
     * @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 Additional options for the swap
     */
    async createToBTCLNSwapViaInvoiceCreateService<ChainIdentifier extends ChainIds<T>>(
        chainIdentifier: ChainIdentifier,
        signer: string,
        tokenAddress: string,
        service: LightningInvoiceCreateService,
        amount: bigint,
        exactIn: boolean = false,
        additionalParams: Record<string, any> | undefined = this.options.defaultAdditionalParameters,
        options?: ToBTCLNOptions
    ): Promise<ToBTCLNSwap<T[ChainIdentifier]>> {
        if(this._chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier);
        if(!this._chains[chainIdentifier].chainInterface.isValidAddress(signer, true)) throw new Error("Invalid "+chainIdentifier+" address");
        signer = this._chains[chainIdentifier].chainInterface.normalizeAddress(signer);
        const amountData = {
            amount,
            token: tokenAddress,
            exactIn
        };
        return this.createSwap(
            chainIdentifier as ChainIdentifier,
            (candidates: Intermediary[], abortSignal: AbortSignal, chain) => chain.wrappers[SwapType.TO_BTCLN].createViaInvoiceCreateService(
                signer,
                Promise.resolve(service),
                amountData,
                candidates,
                options,
                additionalParams,
                abortSignal
            ),
            amountData,
            SwapType.TO_BTCLN
        );
    }

    /**
     * Creates Bitcoin -> Smart chain ({@link SwapType.SPV_VAULT_FROM_BTC}) swap
     *
     * @param chainIdentifier Chain identifier string of the destination smart chain
     * @param recipient Recipient address on the destination chain
     * @param tokenAddress Token address to receive
     * @param amount Amount to send in satoshis (if `exactOut=false`) or receive in token based units (if `exactOut=true`)
     * @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 Additional options for the swap
     */
    async createFromBTCSwapNew<ChainIdentifier extends ChainIds<T>>(
        chainIdentifier: ChainIdentifier,
        recipient: string,
        tokenAddress: string,
        amount: bigint | null,
        exactOut: boolean = false,
        additionalParams: Record<string, any> | undefined = this.options.defaultAdditionalParameters,
        options?: SpvFromBTCOptions
    ): Promise<SpvFromBTCSwap<T[ChainIdentifier]>> {
        if(this._chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier);
        if(this._chains[chainIdentifier].wrappers[SwapType.SPV_VAULT_FROM_BTC]==null) throw new Error("Chain "+chainIdentifier+" doesn't support new BTC swap protocol (spv vault swaps)!");
        if(!this._chains[chainIdentifier].chainInterface.isValidAddress(recipient, true)) throw new Error("Invalid "+chainIdentifier+" address");
        recipient = this._chains[chainIdentifier].chainInterface.normalizeAddress(recipient);
        const amountData = {
            amount: amount ?? undefined,
            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(
                recipient,
                amountData,
                candidates,
                options,
                additionalParams,
                abortSignal
            )),
            amountData,
            SwapType.SPV_VAULT_FROM_BTC
        );
    }

    /**
     * Creates LEGACY Bitcoin -> Smart chain ({@link SwapType.FROM_BTC}) swap
     *
     * @param chainIdentifier Chain identifier string of the destination smart chain
     * @param recipient Recipient address on the destination chain
     * @param tokenAddress Token address to receive
     * @param amount Amount to send in satoshis (if `exactOut=false`) or receive in token based units (if `exactOut=true`)
     * @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 Additional options for the swap
     */
    async createFromBTCSwap<ChainIdentifier extends ChainIds<T>>(
        chainIdentifier: ChainIdentifier,
        recipient: string,
        tokenAddress: string,
        amount: bigint,
        exactOut: boolean = false,
        additionalParams: Record<string, any> | undefined = this.options.defaultAdditionalParameters,
        options?: FromBTCOptions
    ): Promise<FromBTCSwap<T[ChainIdentifier]>> {
        if(this._chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier);
        if(!this._chains[chainIdentifier].chainInterface.isValidAddress(recipient, true)) throw new Error("Invalid "+chainIdentifier+" address");
        recipient = this._chains[chainIdentifier].chainInterface.normalizeAddress(recipient);
        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(
                recipient,
                amountData,
                candidates,
                options,
                additionalParams,
                abortSignal
            )),
            amountData,
            SwapType.FROM_BTC
        );
    }

    /**
     * Creates LEGACY Bitcoin Lightning -> Smart chain ({@link SwapType.FROM_BTCLN}) swap
     *
     * @param chainIdentifier Chain identifier string of the destination smart chain
     * @param recipient Recipient address on the destination chain
     * @param tokenAddress Token address to receive
     * @param amount Amount to send in satoshis (if `exactOut=false`) or receive in token based units (if `exactOut=true`)
     * @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 Additional options for the swap
     */
    async createFromBTCLNSwap<ChainIdentifier extends ChainIds<T>>(
        chainIdentifier: ChainIdentifier,
        recipient: string,
        tokenAddress: string,
        amount: bigint,
        exactOut: boolean = false,
        additionalParams: Record<string, any> | undefined = this.options.defaultAdditionalParameters,
        options?: FromBTCLNOptions
    ): Promise<FromBTCLNSwap<T[ChainIdentifier]>> {
        if(this._chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier);
        if(!this._chains[chainIdentifier].chainInterface.isValidAddress(recipient, true)) throw new Error("Invalid "+chainIdentifier+" address");
        recipient = this._chains[chainIdentifier].chainInterface.normalizeAddress(recipient);
        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(
                recipient,
                amountData,
                candidates,
                options,
                additionalParams,
                abortSignal
            )),
            amountData,
            SwapType.FROM_BTCLN
        );
    }

    /**
     * Creates LEGACY Bitcoin Lightning -> Smart chain ({@link SwapType.FROM_BTCLN}) swap, withdrawing from
     *  an LNURL-withdraw link
     *
     * @param chainIdentifier Chain identifier string of the destination smart chain
     * @param recipient Recipient address on the destination chain
     * @param tokenAddress Token address to receive
     * @param lnurl LNURL-withdraw link to pull the funds from
     * @param amount Amount to send in satoshis (if `exactOut=false`) or receive in token based units (if `exactOut=true`)
     * @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 Additional options for the swap
     */
    async createFromBTCLNSwapViaLNURL<ChainIdentifier extends ChainIds<T>>(
        chainIdentifier: ChainIdentifier,
        recipient: string,
        tokenAddress: string,
        lnurl: string | LNURLWithdraw,
        amount: bigint,
        exactOut: boolean = false,
        additionalParams: Record<string, any> | undefined = this.options.defaultAdditionalParameters,
        options?: FromBTCLNOptions
    ): Promise<FromBTCLNSwap<T[ChainIdentifier]>> {
        if(this._chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier);
        if(typeof(lnurl)==="string" && !this.Utils.isValidLNURL(lnurl)) throw new Error("Invalid LNURL-withdraw link");
        if(!this._chains[chainIdentifier].chainInterface.isValidAddress(recipient, true)) throw new Error("Invalid "+chainIdentifier+" address");
        recipient = this._chains[chainIdentifier].chainInterface.normalizeAddress(recipient);
        const amountData = {
            amount,
            token: tokenAddress,
            exactIn: !exactOut
        };
        return this.createSwap(
            chainIdentifier as ChainIdentifier,
            (candidates: Intermediary[], abortSignal: AbortSignal, chain) => chain.wrappers[SwapType.FROM_BTCLN].createViaLNURL(
                recipient,
                typeof(lnurl)==="string" ? (lnurl.startsWith("lightning:") ? lnurl.substring(10): lnurl) : lnurl.params,
                amountData,
                candidates,
                options,
                additionalParams,
                abortSignal
            ),
            amountData,
            SwapType.FROM_BTCLN
        );
    }

    /**
     * Creates Bitcoin Lightning -> Smart chain ({@link SwapType.FROM_BTCLN_AUTO}) swap
     *
     * @param chainIdentifier Chain identifier string of the destination smart chain
     * @param recipient Recipient address on the destination chain
     * @param tokenAddress Token address to receive
     * @param amount Amount to send in satoshis (if `exactOut=false`) or receive in token based units (if `exactOut=true`)
     * @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 Additional options for the swap
     */
    async createFromBTCLNSwapNew<ChainIdentifier extends ChainIds<T>>(
        chainIdentifier: ChainIdentifier,
        recipient: string,
        tokenAddress: string,
        amount: bigint,
        exactOut: boolean = false,
        additionalParams: Record<string, any> | undefined = this.options.defaultAdditionalParameters,
        options?: FromBTCLNAutoOptions
    ): Promise<FromBTCLNAutoSwap<T[ChainIdentifier]>> {
        if(this._chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier);
        if(this._chains[chainIdentifier].wrappers[SwapType.FROM_BTCLN_AUTO]==null) throw new Error("Chain "+chainIdentifier+" doesn't support new lightning swap protocol (from btcln auto)!");
        if(!this._chains[chainIdentifier].chainInterface.isValidAddress(recipient, true)) throw new Error("Invalid "+chainIdentifier+" address");
        recipient = this._chains[chainIdentifier].chainInterface.normalizeAddress(recipient);
        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_AUTO].create(
                recipient,
                amountData,
                candidates,
                options,
                additionalParams,
                abortSignal
            )),
            amountData,
            SwapType.FROM_BTCLN_AUTO
        );
    }

    /**
     * Creates Bitcoin Lightning -> Smart chain ({@link SwapType.FROM_BTCLN_AUTO}) swap, withdrawing from
     *  an LNURL-withdraw link
     *
     * @param chainIdentifier Chain identifier string of the destination smart chain
     * @param recipient Recipient address on the destination chain
     * @param tokenAddress Token address to receive
     * @param lnurl LNURL-withdraw link to pull the funds from
     * @param amount Amount to send in satoshis (if `exactOut=false`) or receive in token based units (if `exactOut=true`)
     * @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 Additional options for the swap
     */
    async createFromBTCLNSwapNewViaLNURL<ChainIdentifier extends ChainIds<T>>(
        chainIdentifier: ChainIdentifier,
        recipient: string,
        tokenAddress: string,
        lnurl: string | LNURLWithdraw,
        amount: bigint,
        exactOut: boolean = false,
        additionalParams: Record<string, any> | undefined = this.options.defaultAdditionalParameters,
        options?: FromBTCLNAutoOptions
    ): Promise<FromBTCLNAutoSwap<T[ChainIdentifier]>> {
        if(this._chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier);
        if(this._chains[chainIdentifier].wrappers[SwapType.FROM_BTCLN_AUTO]==null) throw new Error("Chain "+chainIdentifier+" doesn't support new lightning swap protocol (from btcln auto)!");
        if(typeof(lnurl)==="string" && !this.Utils.isValidLNURL(lnurl)) throw new Error("Invalid LNURL-withdraw link");
        if(!this._chains[chainIdentifier].chainInterface.isValidAddress(recipient, true)) throw new Error("Invalid "+chainIdentifier+" address");
        recipient = this._chains[chainIdentifier].chainInterface.normalizeAddress(recipient);
        const amountData = {
            amount,
            token: tokenAddress,
            exactIn: !exactOut
        };
        return this.createSwap(
            chainIdentifier as ChainIdentifier,
            (candidates: Intermediary[], abortSignal: AbortSignal, chain) => chain.wrappers[SwapType.FROM_BTCLN_AUTO].createViaLNURL(
                recipient,
                typeof(lnurl)==="string" ? (lnurl.startsWith("lightning:") ? lnurl.substring(10): lnurl) : lnurl.params,
                amountData,
                candidates,
                options,
                additionalParams,
                abortSignal
            ),
            amountData,
            SwapType.FROM_BTCLN_AUTO
        );
    }

    /**
     * Creates a trusted Bitcoin Lightning -> Smart chain ({@link SwapType.TRUSTED_FROM_BTCLN}) gas swap
     *
     * @param chainIdentifier Chain identifier string of the destination smart chain
     * @param recipient Recipient address on the destination chain
     * @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
     */
    async createTrustedLNForGasSwap<C extends ChainIds<T>>(chainIdentifier: C, recipient: string, amount: bigint, trustedIntermediaryOrUrl?: Intermediary | string): Promise<LnForGasSwap<T[C]>> {
        if(this._chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier);
        if(!this._chains[chainIdentifier].chainInterface.isValidAddress(recipient, true)) throw new Error("Invalid "+chainIdentifier+" address");
        recipient = this._chains[chainIdentifier].chainInterface.normalizeAddress(recipient);
        const useUrl = trustedIntermediaryOrUrl ?? this.defaultTrustedIntermediary ?? this.options.defaultTrustedIntermediaryUrl;
        if(useUrl==null) throw new Error("No trusted intermediary specified!");
        const swap = await this._chains[chainIdentifier as C].wrappers[SwapType.TRUSTED_FROM_BTCLN].create(recipient, amount, useUrl);
        await swap._save();
        return swap;
    }

    /**
     * Creates a trusted Bitcoin -> Smart chain ({@link SwapType.TRUSTED_FROM_BTC}) gas swap
     *
     * @param chainIdentifier Chain identifier string of the destination smart chain
     * @param recipient Recipient address on the destination chain
     * @param amount Amount of native token to receive, in base units
     * @param refundAddress Bitcoin refund address, in case the swap fails the funds are refunded here
     * @param trustedIntermediaryOrUrl URL or Intermediary object of the trusted intermediary to use, otherwise uses default
     * @throws {Error} If no trusted intermediary specified
     */
    async createTrustedOnchainForGasSwap<C extends ChainIds<T>>(
        chainIdentifier: C, recipient: string,
        amount: bigint, refundAddress?: string,
        trustedIntermediaryOrUrl?: Intermediary | string
    ): Promise<OnchainForGasSwap<T[C]>> {
        if(this._chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier);
        if(!this._chains[chainIdentifier].chainInterface.isValidAddress(recipient, true)) throw new Error("Invalid "+chainIdentifier+" address");
        recipient = this._chains[chainIdentifier].chainInterface.normalizeAddress(recipient);
        const useUrl = trustedIntermediaryOrUrl ?? this.defaultTrustedIntermediary ?? this.options.defaultTrustedIntermediaryUrl;
        if(useUrl==null) throw new Error("No trusted intermediary specified!");
        const swap = await this._chains[chainIdentifier as C].wrappers[SwapType.TRUSTED_FROM_BTC].create(recipient, amount, useUrl, refundAddress);
        await swap._save();
        return swap;
    }

    /**
     * @internal
     */
    create<C extends ChainIds<T>>(signer: string, srcToken: BtcToken<true>, dstToken: SCToken<C>, amount: bigint, exactIn: boolean, lnurlWithdraw?: string | LNURLWithdraw): Promise<(SupportsSwapType<T[C], SwapType.FROM_BTCLN_AUTO> extends true ? FromBTCLNAutoSwap<T[C]> : FromBTCLNSwap<T[C]>)>;
    /**
     * @internal
     */
    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]>)>;
    /**
     * @internal
     */
    create<C extends ChainIds<T>>(signer: string, srcToken: SCToken<C>, dstToken: BtcToken<false>, amount: bigint, exactIn: boolean, address: string): Promise<ToBTCSwap<T[C]>>;
    /**
     * @internal
     */
    create<C extends ChainIds<T>>(signer: string, srcToken: SCToken<C>, dstToken: BtcToken<true>, amount: bigint, exactIn: boolean, lnurlPay: string | LNURLPay): Promise<ToBTCLNSwap<T[C]>>;
    /**
     * @internal
     */
    create<C extends ChainIds<T>>(signer: string, srcToken: SCToken<C>, dstToken: BtcToken<true>, amount: undefined, exactIn: false, lightningInvoice: string): Promise<ToBTCLNSwap<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 {@link 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 | undefined, 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 {@link 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 | undefined, 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);
        }
    }

    /**
     * @internal
     */
    swap<C extends ChainIds<T>>(srcToken: BtcToken<true> | "BTCLN" | "BTC-LN" | "LIGHTNING-BTC", dstToken: SCToken<C> | string, amount: bigint | string, exactIn: boolean | SwapAmountType, src: undefined | string | LNURLWithdraw, dstSmartchainWallet: string, options?: (SupportsSwapType<T[C], SwapType.FROM_BTCLN_AUTO> extends true ? FromBTCLNAutoOptions : FromBTCLNOptions)): Promise<(SupportsSwapType<T[C], SwapType.FROM_BTCLN_AUTO> extends true ? FromBTCLNAutoSwap<T[C]> : FromBTCLNSwap<T[C]>)>;
    /**
     * @internal
     */
    swap<C extends ChainIds<T>>(srcToken: BtcToken<false> | "BTC" | "BITCOIN-BTC", dstToken: SCToken<C> | string, amount: bigint | string, exactIn: boolean | SwapAmountType, 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]>)>;
    /**
     * @internal
     */
    swap<C extends ChainIds<T>>(srcToken: SCToken<C> | string, dstToken: BtcToken<false> | "BTC" | "BITCOIN-BTC", amount: bigint | string, exactIn: boolean | SwapAmountType, src: string, dstAddress: string, options?: ToBTCOptions): Promise<ToBTCSwap<T[C]>>;
    /**
     * @internal
     */
    swap<C extends ChainIds<T>>(srcToken: SCToken<C> | string, dstToken: BtcToken<true> | "BTCLN" | "BTC-LN" | "LIGHTNING-BTC", amount: bigint | string, exactIn: boolean | SwapAmountType, src: string, dstLnurlPayOrInvoiceCreateService: string | LNURLPay | LightningInvoiceCreateService, options?: ToBTCLNOptions & {comment?: string}): Promise<ToBTCLNSwap<T[C]>>;
    /**
     * @internal
     */
    swap<C extends ChainIds<T>>(srcToken: SCToken<C> | string, dstToken: BtcToken<true> | "BTCLN" | "BTC-LN" | "LIGHTNING-BTC", amount: undefined, exactIn: false | SwapAmountType.EXACT_OUT, src: string, dstLightningInvoice: string, options?: ToBTCLNOptions): Promise<ToBTCLNSwap<T[C]>>;
    /**
     * Creates a swap from srcToken to dstToken, of a specific token amount, either specifying input amount (if `exactIn=true`)
     *  or output amount (if `exactIn=false`), NOTE: For regular Smart chain -> BTC-LN (lightning) swaps the passed amount is ignored and
     *  invoice's pre-set amount is used instead, use LNURL-pay links 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 either in base units as {bigint} or in human readable format (with decimals) as {string}
     * @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> | string, dstToken: Token<C> | string, amount: bigint | string | undefined, exactIn: boolean | SwapAmountType, src: undefined | string | LNURLWithdraw, dst: string | LNURLPay | LightningInvoiceCreateService, options?: FromBTCLNOptions | SpvFromBTCOptions | FromBTCOptions | ToBTCOptions | (ToBTCLNOptions & {comment?: string}) | FromBTCLNAutoOptions): Promise<ISwap<T[C]>>;
    /**
     * Creates a swap from srcToken to dstToken, of a specific token amount, either specifying input amount (if `exactIn=true`)
     *  or output amount (if `exactIn=false`), NOTE: For regular Smart chain -> BTC-LN (lightning) swaps the passed amount is ignored and
     *  invoice's pre-set amount is used instead, use LNURL-pay links 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 either in base units as {bigint} or in human readable format (with decimals) as {string}
     * @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> | string,
        _dstToken: Token<C> | string,
        _amount: bigint | string | undefined,
        exactIn: boolean | SwapAmountType,
        src: undefined | string | LNURLWithdraw,
        dst: string |  LNURLPay | LightningInvoiceCreateService,
        options?: FromBTCLNOptions | SpvFromBTCOptions | FromBTCOptions | ToBTCOptions | (ToBTCLNOptions & {comment?: string}) | FromBTCLNAutoOptions
    ): Promise<ISwap<T[C]>> {
        if(typeof(src)==="string") src = this.Utils.stripAddress(src);
        if(typeof(dst)==="string") dst = this.Utils.stripAddress(dst);

        const srcToken = typeof(_srcToken)==="string" ? this.getToken(_srcToken) as Token<C> : _srcToken;
        const dstToken = typeof(_dstToken)==="string" ? this.getToken(_dstToken) as Token<C> : _dstToken;
        const amount = _amount==null ? null : (typeof(_amount)==="bigint" ? _amount : fromDecimal(_amount, exactIn ? srcToken.decimals : dstToken.decimals));
        if(isBtcToken(srcToken)) {
            if(isSCToken<C>(dstToken)) {
                if(typeof(dst)!=="string") throw new Error("Destination for BTC/BTC-LN -> smart chain swaps must be a smart chain address!");
                if(amount==null) throw new Error("Amount cannot be null for from btc swaps!");
                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.supportsSwapType(dstToken.chainId, SwapType.FROM_BTCLN_AUTO) ?
                            this.createFromBTCLNSwapNewViaLNURL(dstToken.chainId, dst, dstToken.address, src, amount, !exactIn, undefined, options as any) :
                            this.createFromBTCLNSwapViaLNURL(dstToken.chainId, dst, dstToken.address, src, amount, !exactIn, undefined, options as any);
                    } else {
                        return this.supportsSwapType(dstToken.chainId, SwapType.FROM_BTCLN_AUTO) ?
                            this.createFromBTCLNSwapNew(dstToken.chainId, dst, dstToken.address, amount, !exactIn, undefined, options as any):
                            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(isSCToken<C>(srcToken)) {
            if(isBtcToken(dstToken)) {
                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)) {
                        if(amount==null) throw new Error("Amount cannot be null for to btcln swaps via LNURL-pay!");
                        return this.createToBTCLNSwapViaLNURL(srcToken.chainId, src, srcToken.address, dst, amount, !!exactIn, undefined, options as any);
                    } else if(isLightningInvoiceCreateService(dst)) {
                        if(amount==null) throw new Error("Amount cannot be null for to btcln swaps via InvoiceCreateService!");
                        return this.createToBTCLNSwapViaInvoiceCreateService(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!");
                    if(amount==null) throw new Error("Amount cannot be null for to btc swaps!");
                    return this.createToBTCSwap(srcToken.chainId, src, srcToken.address, dst, amount, !!exactIn, undefined, options as any);
                }
            }
        }
        throw new Error("Unsupported swap type");
    }

    /**
     * A helper function to sweep all the funds from a given wallet in a single swap, after getting the quote you can
     *  execute the swap by passing the returned `feeRate` and `utxos` to the {@link SpvFromBTCSwap.execute},
     *  {@link SpvFromBTCSwap.getFundedPsbt} or {@link SpvFromBTCSwap.sendBitcoinTransaction} functions along
     *  with `spendFully=true`.
     *
     * @example
     * Create the swap first using this function
     * ```ts
     * const {swap, utxos, btcFeeRate} = await swapper.sweepBitcoinWallet(wallet, Tokens.CITREA.CBTC, dstAddress);
     * ```
     * Then execute it using one of these execution paths - ensure that you supply the returned `utxos`, `btcFeeRate`
     *  params and also set `spendFully` to `true`!
     *
     * a) Execute and pass the returned utxos and btcFeeRate:
     * ```ts
     * await swap.execute(wallet, undefined, {feeRate: btcFeeRate, utxos: utxos, spendFully: true});
     * ```
     *
     * b) Get funded PSBT to sign externally:
     * ```ts
     * const {psbt, psbtHex, psbtBase64, signInputs} = await swap.getFundedPsbt(wallet, btcFeeRate, undefined, utxos, true);
     * // Sign the psbt at the specified signInputs indices
     * const signedPsbt = ...;
     * // Then submit back to the SDK
     * await swap.submitPsbt(signedPsbt);
     * ```
     *
     * c) Only sign and send the signed PSBT with the provided wallet:
     * ```ts
     * await swap.sendBitcoinTransaction(wallet, btcFeeRate, utxos, true);
     * ```
     */
    async sweepBitcoinWallet<C extends ChainIds<T>>(
        srcWallet: IBitcoinWallet | MinimalBitcoinWalletInterface,
        _dstToken: SCToken<C> | string,
        dstAddress: string,
        options?: SpvFromBTCOptions
    ): Promise<{
        swap: SpvFromBTCSwap<T[C]>,
        utxos: BitcoinWalletUtxo[],
        btcFeeRate: number
    }> {
        const dstToken = typeof(_dstToken)==="string" ? this.getToken(_dstToken) as Token<C> : _dstToken;
        if(!isSCToken<C>(dstToken)) throw new Error("Destination token must be a smart chain token!");

        const wallet = toBitcoinWallet(srcWallet, this._bitcoinRpc, this.bitcoinNetwork);
        if(wallet.getUtxoPool==null) throw new Error("Wallet needs to support the `getUtxoPool()` function!");

        const walletUtxosPromise = wallet.getUtxoPool();
        const bitcoinFeeRatePromise = options?.bitcoinFeeRate ?? wallet.getFeeRate();

        const swap = await this.createFromBTCSwapNew(
            dstToken.chainId as C, dstAddress, dstToken.address, null, false, undefined, {
                ...options,
                sourceWalletUtxos: walletUtxosPromise,
                bitcoinFeeRate: bitcoinFeeRatePromise
            }
        );

        return {
            swap,
            utxos: await walletUtxosPromise,
            btcFeeRate: Math.max(swap.minimumBtcFeeRate, await bitcoinFeeRatePromise)
        };
    }

    /**
     * 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: "initiator", 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 which are pending (i.e. not in their final state yet)
     */
    getPendingSwaps(): Promise<ISwap[]>;
    /**
     * Returns swaps which are pending (i.e. not in their final state yet) for the specific chain, and optionally also for a specific signer's address
     */
    getPendingSwaps<C extends ChainIds<T>>(chainId: C, signer?: string): Promise<ISwap<T[C]>[]>;
    async getPendingSwaps<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 as unknown as SwapType];
                    const swapTypeQueryParams: QueryParams[] = [{key: "type", value: wrapper.TYPE}];
                    if(signer!=null) swapTypeQueryParams.push({key: "initiator", value: signer});
                    swapTypeQueryParams.push({key: "state", value: wrapper._pendingSwapStates});
                    queryParams.push(swapTypeQueryParams);
                }
                return unifiedSwapStorage.query(queryParams, reviver);
            }));
            return res.flat();
        } else {
            const {unifiedSwapStorage, reviver, wrappers} = this._chains[chainId];
            const queryParams: Array<QueryParams[]> = [];
            for(let key in wrappers) {
                const wrapper = wrappers[key as unknown as SwapType];
                const swapTypeQueryParams: QueryParams[] = [{key: "type", value: wrapper.TYPE}];
                if(signer!=null) swapTypeQueryParams.push({key: "initiator", value: signer});
                swapTypeQueryParams.push({key: "state", value: wrapper._pendingSwapStates});
                queryParams.push(swapTypeQueryParams);
            }
            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) {
            return (await this.getPendingSwaps()).filter(swap => swap.requiresAction());
        } else {
            return (await this.getPendingSwaps(chainId, signer)).filter(swap => swap.requiresAction());
        }
    }

    /**
     * Returns all swaps that are refundable
     */
    getRefundableSwaps(): Promise<IToBTCSwap[]>;
    /**
     * Returns swaps which are refundable for the specific chain, and optionally also for a specific signer's address
     */
    getRefundableSwaps<C extends ChainIds<T>>(chainId: C, signer?: string): Promise<IToBTCSwap<T[C]>[]>;
    async getRefundableSwaps<C extends ChainIds<T>>(chainId?: C, signer?: string): Promise<IToBTCSwap[]> {
        if(chainId==null) {
            const res: IToBTCSwap[][] = await Promise.all(Object.keys(this._chains).map((chainId) => {
                const {unifiedSwapStorage, reviver, wrappers} = this._chains[chainId];
                const queryParams: Array<QueryParams[]> = [];
                for(let wrapper of [wrappers[SwapType.TO_BTCLN], wrappers[SwapType.TO_BTC]]) {
                    const swapTypeQueryParams: QueryParams[] = [{key: "type", value: wrapper.TYPE}];
                    if(signer!=null) swapTypeQueryParams.push({key: "initiator", value: signer});
                    swapTypeQueryParams.push({key: "state", value: wrapper._refundableSwapStates});
                    queryParams.push(swapTypeQueryParams);
                }
                return unifiedSwapStorage.query<IToBTCSwap<T[C]>>(queryParams, reviver as (val: any) => IToBTCSwap<T[C]>);
            }));
            return res.flat().filter(swap => swap.isRefundable());
        } else {
            const {unifiedSwapStorage, reviver, wrappers} = this._chains[chainId];
            const queryParams: Array<QueryParams[]> = [];
            for(let wrapper of [wrappers[SwapType.TO_BTCLN], wrappers[SwapType.TO_BTC]]) {
                const swapTypeQueryParams: QueryParams[] = [{key: "type", value: wrapper.TYPE}];
                if(signer!=null) swapTypeQueryParams.push({key: "initiator", value: signer});
                swapTypeQueryParams.push({key: "state", value: wrapper._refundableSwapStates});
                queryParams.push(swapTypeQueryParams);
            }
            const result = await unifiedSwapStorage.query<IToBTCSwap<T[C]>>(queryParams, reviver as (val: any) => IToBTCSwap<T[C]>);
            return result.filter(swap => swap.isRefundable());
        }
    }

    /**
     * Returns all swaps that are manually claimable
     */
    getClaimableSwaps(): Promise<IClaimableSwap[]>;
    /**
     * Returns all swaps that are manually claimable for the specific chain, and optionally also for a specific signer's address
     */
    getClaimableSwaps<C extends ChainIds<T>>(chainId: C, signer?: string): Promise<IClaimableSwap<T[C]>[]>;
    async getClaimableSwaps<C extends ChainIds<T>>(chainId?: C, signer?: string): Promise<IClaimableSwap[]> {
        if(chainId==null) {
            const res: IClaimableSwap[][] = await Promise.all(Object.keys(this._chains).map((chainId) => {
                const {unifiedSwapStorage, reviver, wrappers} = this._chains[chainId];
                const queryParams: Array<QueryParams[]> = [];
                for(let wrapper of [wrappers[SwapType.FROM_BTC], wrappers[SwapType.FROM_BTCLN], wrappers[SwapType.SPV_VAULT_FROM_BTC], wrappers[SwapType.FROM_BTCLN_AUTO]]) {
                    if(wrapper==null) continue;
                    const swapTypeQueryParams: QueryParams[] = [{key: "type", value: wrapper.TYPE}];
                    if(signer!=null) swapTypeQueryParams.push({key: "initiator", value: signer});
                    swapTypeQueryParams.push({key: "state", value: wrapper._claimableSwapStates});
                    queryParams.push(swapTypeQueryParams);
                }
                return unifiedSwapStorage.query<IClaimableSwap<T[C]>>(queryParams, reviver as any as (val: any) => IClaimableSwap<T[C]>);
            }));
            return res.flat().filter(swap => swap.isClaimable());
        } else {
            const {unifiedSwapStorage, reviver, wrappers} = this._chains[chainId];
            const queryParams: Array<QueryParams[]> = [];
            for(let wrapper of [wrappers[SwapType.FROM_BTC], wrappers[SwapType.FROM_BTCLN], wrappers[SwapType.SPV_VAULT_FROM_BTC], wrappers[SwapType.FROM_BTCLN_AUTO]]) {
                if(wrapper==null) continue;
                const swapTypeQueryParams: QueryParams[] = [{key: "type", value: wrapper.TYPE}];
                if(signer!=null) swapTypeQueryParams.push({key: "initiator", value: signer});
                swapTypeQueryParams.push({key: "state", value: wrapper._claimableSwapStates});
                queryParams.push(swapTypeQueryParams);
            }
            const result = await unifiedSwapStorage.query<IClaimableSwap<T[C]>>(queryParams, reviver as any as (val: any) => IClaimableSwap<T[C]>);
            return result.filter(swap => swap.isClaimable());
        }
    }

    /**
     * Returns swap with a specific id (identifier)
     */
    getSwapById(id: string): Promise<ISwap>;
    /**
     * Returns swap with a specific id (identifier) on a specific chain and optionally with a signer
     */
    getSwapById<C extends ChainIds<T>>(id: string, chainId: C, signer?: string): Promise<ISwap<T[C]>>;
    async getSwapById<C extends ChainIds<T>>(id: string, chainId?: C, signer?: string): Promise<ISwap> {
        //Check in pending swaps first
        if(chainId!=null) {
            for(let key in this._chains[chainId].wrappers) {
                const wrapper = this._chains[chainId].wrappers[key as unknown as SwapType];
                const result = wrapper._getPendingSwap(id);
                if(result!=null) {
                    if (signer != null) {
                        if (result._getInitiator() === signer) return result;
                    } else {
                        return result;
                    }
                }
            }
        } else {
            for(let chainId in this._chains) {
                for(let key in this._chains[chainId].wrappers) {
                    const wrapper = this._chains[chainId].wrappers[key as unknown as SwapType];
                    const result = wrapper._getPendingSwap(id);
                    if(result!=null) {
                        if(signer!=null) {
                            if(result._getInitiator()===signer) return result;
                        } else {
                            return result;
                        }
                    }
                }
            }
        }

        const queryParams: QueryParams[] = [];
        if(signer!=null) queryParams.push({key: "initiator", value: signer});
        queryParams.push({key: "id", value: id});
        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()[0];
        } else {
            const {unifiedSwapStorage, reviver} = this._chains[chainId];
            return (await unifiedSwapStorage.query([queryParams], reviver))[0];
        }
    }

    /**
     * Returns the swap with a proper return type, or `undefined` if not found or has wrong type
     *
     * @param id An ID of the swap ({@link ISwap.getId})
     * @param chainId Chain identifier of the smart chain where the swap was initiated
     * @param swapType Type of the swap
     * @param signer An optional required smart chain signer address to fetch the swap for
     */
    async getTypedSwapById<C extends ChainIds<T>, S extends SwapType>(id: string, chainId: C, swapType: S, signer?: string): Promise<SwapTypeMapping<T[C]>[S] | undefined> {
        let _swapType: SwapType = swapType;
        if(swapType===SwapType.FROM_BTC && this.supportsSwapType(chainId, SwapType.SPV_VAULT_FROM_BTC))
            _swapType = SwapType.SPV_VAULT_FROM_BTC;
        if(swapType===SwapType.FROM_BTCLN && this.supportsSwapType(chainId, SwapType.FROM_BTCLN_AUTO))
            _swapType = SwapType.FROM_BTCLN_AUTO;

        const wrapper = this._chains[chainId].wrappers[_swapType];
        if(wrapper==null) return;

        const result = wrapper._getPendingSwap(id);
        if(result!=null) {
            if (signer != null) {
                if (result._getInitiator() === signer) return result as any;
            } else {
                return result as any;
            }
        }

        const queryParams: QueryParams[] = [];
        if(signer!=null) queryParams.push({key: "initiator", value: signer});
        queryParams.push({key: "id", value: id});
        const {unifiedSwapStorage, reviver} = this._chains[chainId];
        const swap = (await unifiedSwapStorage.query([queryParams], reviver))[0];
        if(isSwapType(swap, swapType)) return swap;
    }

    private async syncSwapsForChain<C extends ChainIds<T>>(chainId: C, signer?: string): Promise<void> {
        const {unifiedSwapStorage, reviver, wrappers} = this._chains[chainId];
        const queryParams: Array<QueryParams[]> = [];
        for(let key in wrappers) {
            const wrapper = wrappers[key as unknown as SwapType];
            const swapTypeQueryParams: QueryParams[] = [{key: "type", value: wrapper.TYPE}];
            if(signer!=null) swapTypeQueryParams.push({key: "initiator", value: signer});
            swapTypeQueryParams.push({key: "state", value: wrapper._pendingSwapStates});
            queryParams.push(swapTypeQueryParams);
        }
        this.logger.debug("_syncSwaps(): Querying swaps swaps for chain "+chainId+"!");
        const swaps = await unifiedSwapStorage.query(queryParams, reviver);
        this.logger.debug("_syncSwaps(): Syncing "+swaps.length+" swaps!");

        const changedSwaps: ISwap<T[C]>[] = [];
        const removeSwaps: ISwap<T[C]>[] = [];

        const assortedSwaps: {[swapType in SwapType]?: ISwap<T[string]>[]} = {};
        swaps.forEach(swap => {
            (assortedSwaps[swap.getType()] ??= []).push(swap);
        });

        for(let key in assortedSwaps) {
            const swapType = key as unknown as SwapType;
            const wrapperSwaps = assortedSwaps[swapType];
            const wrapper: ISwapWrapper<T[C], any> = wrappers[swapType];
            const result = await wrapper.checkPastSwaps(wrapperSwaps, true);
            changedSwaps.push(...result.changedSwaps);
            removeSwaps.push(...result.removeSwaps);
        }

        this.logger.debug("_syncSwaps(): Done syncing "+swaps.length+" swaps, saving "+changedSwaps.length+" changed swaps, removing "+removeSwaps.length+" swaps!");
        await unifiedSwapStorage.saveAll(changedSwaps, true);
        await unifiedSwapStorage.removeAll(removeSwaps, true);

        changedSwaps.forEach(swap => swap._emitEvent());
        removeSwaps.forEach(swap => swap._emitEvent());
    }

    /**
     * Deletes the swaps from the persistent storage backend. Note that some data (like lightning network
     *  amounts and bolt11 invoices) are purely off-chain and can never be recovered later just from
     *  on-chain data!
     *
     * @param chainId Optional, to only delete swaps for this smart chain
     * @param signer Optional, to only delete swaps for this smart chain signer (`chainId` param must be
     *  set to delete only signer's swaps)
     */
    async wipeStorage<C extends ChainIds<T>>(chainId?: C, signer?: string) {
        if(chainId==null) {
            const swaps = await this.getAllSwaps();
            const chainSwaps: {
                [chainId: string]: ISwap<any>[]
            } = {};
            swaps.forEach(swap => (chainSwaps[swap.chainIdentifier] ??= []).push(swap));
            for(let chainId in chainSwaps) {
                const currentChainSwaps = chainSwaps[chainId];
                if(this._chains[chainId]==null) {
                    this.logger.warn(`wipeStorage(): Attempted to remove ${currentChainSwaps.length} swaps on ${chainId}, but smart chain not known!`);
                    continue;
                }
                await this._chains[chainId].unifiedSwapStorage.removeAll(currentChainSwaps);
                this.logger.debug(`wipeStorage(): Successfully removed ${currentChainSwaps.length} swaps on ${chainId}!`);
            }
        } else {
            if(this._chains[chainId]==null) throw new Error(`wipeStorage(): Smart chain with identifier ${chainId} not found!`);
            const swaps = await this.getAllSwaps(chainId, signer);
            await this._chains[chainId].unifiedSwapStorage.removeAll(swaps);
            this.logger.debug(`wipeStorage(): Successfully removed ${swaps.length} swaps on ${chainId}!`);
        }
    }

    /**
     * Synchronizes swaps from on-chain, this is ran automatically when SDK is initialized, hence
     *  should only be ran manually when `dontCheckPastSwaps=true` is passed in the swapper options,
     *  also deletes expired quotes
     *
     * @param chainId Optional chain identifier to only run swap sync for a single smart chain
     * @param signer Optional signer to only run swap sync for swaps initiated by this signer
     */
    async _syncSwaps<C extends ChainIds<T>>(chainId?: C, signer?: string): Promise<void> {
        if(chainId==null) {
            await Promise.all(Object.keys(this._chains).map((chainId) => {
                return this.syncSwapsForChain(chainId, signer);
            }));
        } else {
            await this.syncSwapsForChain(chainId, signer);
        }
    }

    /**
     * When the swapper is initiated with the `noEvents` config this function allows you to manually poll for on-chain
     *  events. It returns an events cursor which you should save and pass to the next call to the `poll()` function.
     *
     * @param chainId Chain for which to poll the chain events listener for
     * @param lastEventCursorState Event cursor state returned from the last call to the `poll()` function
     */
    async _pollChainEvents<C extends ChainIds<T>>(chainId: C, lastEventCursorState?: any): Promise<any> {
        const chain = this._chains[chainId];
        if(chain==null) throw new Error(`Invalid chain id ${chainId}!`);

        return chain.unifiedChainEvents.poll(lastEventCursorState);
    }

    /**
     * Recovers swaps from on-chain historical data.
     *
     * Please note that the recovered swaps might not be complete (i.e. missing amounts or addresses), as some
     *  of the swap data is purely off-chain and can never be recovered purely from on-chain data. This
     *  functions tries to recover as much swap data as possible.
     *
     * @param chainId Smart chain identifier string to recover the swaps from
     * @param signer Signer address to recover the swaps for
     * @param startBlockheight Optional starting blockheight for swap data recovery, will only check swaps
     *  initiated after this blockheight
     */
    async recoverSwaps<C extends ChainIds<T>>(chainId: C, signer: string, startBlockheight?: number): Promise<ISwap<T[C]>[]> {
        //TODO: Recover swaps from all the known contract versions
        const {versionedContracts, unifiedSwapStorage, reviver, wrappers} = this._chains[chainId];

        const recoveredSwaps: ISwap<T[C]>[] = [];
        let someVersionSupportsRecovery = false;
        const recoveredEscrowStates: {
            [p: string]: {
                init?: {
                    data: SwapData
                    getInitTxId: () => Promise<string>
                    getTxBlock: () => Promise<{
                        blockTime: number
                        blockHeight: number
                    }>
                },
                state: SwapCommitState
                contractVersion: string
            }
        } = {};
        const recoveredSpvStates: {
            [contractVersion: string]: {
                [escrowHash: string]: SpvWithdrawalClaimedState | SpvWithdrawalFrontedState
            }
        } = {};

        for(let contractVersion in versionedContracts) {
            const {swapContract, spvVaultContract} = versionedContracts[contractVersion];

            if(
                swapContract.getHistoricalSwaps==null ||
                (spvVaultContract!=null && spvVaultContract.getHistoricalWithdrawalStates==null)
            ) {
                this.logger.warn(`recoverSwaps(): Swap data recovery not supported on ${chainId}, with contract version ${contractVersion}`);
                continue;
            }

            someVersionSupportsRecovery = true;

            const {swaps} = await swapContract.getHistoricalSwaps(signer, startBlockheight);
            const spvVaultData = wrappers[SwapType.SPV_VAULT_FROM_BTC]==null
                ? undefined
                : await spvVaultContract?.getHistoricalWithdrawalStates!(signer, startBlockheight);

            for(let key in swaps) recoveredEscrowStates[key] = {...swaps[key], contractVersion};
            if(spvVaultData!=null) for(let key in spvVaultData.withdrawals) (recoveredSpvStates[contractVersion] ??= {})[key] = spvVaultData.withdrawals[key];
        }
        if(!someVersionSupportsRecovery) throw new Error(`Historical swap recovery is not supported for ${chainId}`);

        const escrowHashes = Object.keys(recoveredEscrowStates);
        for(let contractVersion in recoveredSpvStates) Object.keys(recoveredSpvStates[contractVersion]).forEach(btcTxId => escrowHashes.push(btcTxId));
        this.logger.debug(`recoverSwaps(): Loaded on-chain data for ${escrowHashes.length} swaps`);
        this.logger.debug(`recoverSwaps(): Fetching if swap escrowHashes are known: ${escrowHashes.join(", ")}`);
        const knownSwapsArray = await unifiedSwapStorage.query([[{key: "escrowHash", value: escrowHashes}]], reviver);
        const knownSwaps: {[escrowHash: string]: ISwap<T[C]>} = {};
        knownSwapsArray.forEach(val => {
            const escrowHash = val._getEscrowHash();
            if(escrowHash!=null) knownSwaps[escrowHash] = val;
        });
        this.logger.debug(`recoverSwaps(): Fetched known swaps escrowHashes: ${Object.keys(knownSwaps).join(", ")}`);

        for(let escrowHash in recoveredEscrowStates) {
            const {init, state, contractVersion} = recoveredEscrowStates[escrowHash];
            const knownSwap = knownSwaps[escrowHash];
            const { swapContract } = versionedContracts[contractVersion];

            if(knownSwap==null) {
                if(init==null) {
                    this.logger.warn(`recoverSwaps(escrow): Fetched ${escrowHash} swap state, but swap not found locally!`);
                    continue;
                }
            } else if(knownSwap instanceof IEscrowSwap) {
                this.logger.debug(`recoverSwaps(escrow): Forcibly updating ${escrowHash} swap: swap already known and in local storage!`);
                if((knownSwap._contractVersion ?? "v1")!==contractVersion) {
                    this.logger.debug(`recoverSwaps(escrow): Skipping ${escrowHash} swap: swap uses contract version ${knownSwap._contractVersion ?? "v1"}, but state comes from ${contractVersion}!`);
                    continue;
                }
                if(await knownSwap._forciblySetOnchainState(state)) {
                    await knownSwap._save();
                }
                continue;
            } else {
                this.logger.debug(`recoverSwaps(escrow): Skipping ${escrowHash} swap: swap already known and in local storage!`);
                continue;
            }

            const data = init.data;

            //Classify swap
            let swap: ISwap<T[C]> | undefined | null;
            let typeIdentified: boolean = false;
            if(data.getType()===ChainSwapType.HTLC) {
                if(data.isOfferer(signer)) {
                    //To BTCLN
                    typeIdentified = true;
                    const lp = this.intermediaryDiscovery.intermediaries.find(val => val.supportsChain(chainId) && data.isClaimer(val.getAddress(chainId)));
                    swap = await wrappers[SwapType.TO_BTCLN].recoverFromSwapDataAndState(init, state, contractVersion, lp);
                } else if(data.isClaimer(signer)) {
                    //From BTCLN
                    typeIdentified = true;
                    const lp = this.intermediaryDiscovery.intermediaries.find(val => val.supportsChain(chainId) && data.isOfferer(val.getAddress(chainId)));
                    if(swapContract.supportsInitWithoutClaimer && wrappers[SwapType.FROM_BTCLN_AUTO]!=null) {
                        swap = await wrappers[SwapType.FROM_BTCLN_AUTO].recoverFromSwapDataAndState(init, state, contractVersion, lp);
                    } else {
                        swap = await wrappers[SwapType.FROM_BTCLN].recoverFromSwapDataAndState(init, state, contractVersion, lp);
                    }
                }
            } else if(data.getType()===ChainSwapType.CHAIN_NONCED) {
                //To BTC
                typeIdentified = true;
                const lp = this.intermediaryDiscovery.intermediaries.find(val => val.supportsChain(chainId) && data.isClaimer(val.getAddress(chainId)));
                swap = await wrappers[SwapType.TO_BTC].recoverFromSwapDataAndState(init, state, contractVersion, lp);
            } else if(data.getType()===ChainSwapType.CHAIN) {
                //From BTC
                typeIdentified = true;
                const lp = this.intermediaryDiscovery.intermediaries.find(val => val.supportsChain(chainId) && data.isOfferer(val.getAddress(chainId)));
                swap = await wrappers[SwapType.FROM_BTC].recoverFromSwapDataAndState(init, state, contractVersion, lp);
            }

            if(swap!=null) {
                recoveredSwaps.push(swap);
            } else {
                if(typeIdentified) this.logger.debug(`recoverSwaps(escrow): Swap data type correctly identified but swap returned is null for swap ${escrowHash}`);
            }
        }

        for(let contractVersion in recoveredSpvStates) {
            const { spvVaultContract } = versionedContracts[contractVersion];
            const spvVaultData = recoveredSpvStates[contractVersion];

            const vaultsData = await spvVaultContract.getMultipleVaultData(
                Object.keys(spvVaultData)
                    .map(btcTxId => ({
                        owner: spvVaultData[btcTxId].owner,
                        vaultId: spvVaultData[btcTxId].vaultId
                    }))
            );

            for(let btcTxId in spvVaultData) {
                const state = spvVaultData[btcTxId];
                const knownSwap = knownSwaps[btcTxId];

                if(knownSwap!=null) {
                    if(knownSwap instanceof SpvFromBTCSwap) {
                        this.logger.debug(`recoverSwaps(spv_vault): Forcibly updating ${btcTxId} swap: swap already known and in local storage!`);
                        //TODO: Forcibly set on-chain state to the swap
                        // if(await knownSwap._forciblySetOnchainState(state)) {
                        //     await knownSwap._save();
                        // }
                        continue;
                    } else {
                        this.logger.debug(`recoverSwaps(spv_vault): Skipping ${btcTxId} swap: swap already known and in local storage!`);
                        continue;
                    }
                }

                const lp = this.intermediaryDiscovery.intermediaries.find(
                    val => val.supportsChain(chainId) && state.owner.toLowerCase()===val.getAddress(chainId).toLowerCase()
                );
                const swap = await wrappers[SwapType.SPV_VAULT_FROM_BTC].recoverFromState(
                    state,
                    contractVersion,
                    vaultsData[state.owner]?.[state.vaultId.toString(10)],
                    lp
                );
                if(swap!=null) {
                    recoveredSwaps.push(swap);
                } else {
                    this.logger.debug(`recoverSwaps(spv_vault): Swap data type correctly identified but swap returned is null for swap ${btcTxId}`);
                }
            }
        }

        this.logger.debug(`recoverSwaps(): Successfully recovered ${recoveredSwaps.length} swaps!`);

        return recoveredSwaps;
    }

    getToken(ticker: "BTC" | "BITCOIN-BTC"): BtcToken<false>;
    getToken(ticker: "BTCLN" | "BTC-LN" | "LIGHTNING-BTC"): BtcToken<true>;
    getToken<ChainIdentifier extends ChainIds<T>>(ticker: `${ChainIdentifier}-${string}`): SCToken<ChainIdentifier>;
    getToken(tickerOrAddress: string): Token<ChainIds<T>>;
    /**
     * Returns the {@link Token} object for a given token
     *
     * @param tickerOrAddress Token to return the object for, can use multiple formats:
     *  - a) token ticker, such as `"BTC"`, `"SOL"`, etc.
     *  - b) token ticker prefixed with smart chain identifier, such as `"SOLANA-SOL"`, `"SOLANA-USDC"`, etc.
     *  - c) token address
     */
    getToken(tickerOrAddress: string): Token<ChainIds<T>> {
        //Btc tokens - BTC, BTCLN, BTC-LN
        if(tickerOrAddress==="BTC" || tickerOrAddress==="BITCOIN-BTC") return BitcoinTokens.BTC;
        if(tickerOrAddress==="BTCLN" || tickerOrAddress==="BTC-LN" || tickerOrAddress==="LIGHTNING-BTC") return BitcoinTokens.BTCLN;

        //Check if the ticker is in format <chainId>-<ticker>, i.e. SOLANA-USDC, STARKNET-WBTC
        if(tickerOrAddress.includes("-")) {
            const [chainId, ticker] = tickerOrAddress.split("-");
            const token = this._tokensByTicker[chainId]?.[ticker];
            if(token==null) throw new UserError(`Not found ticker: ${ticker} for chainId: ${chainId}`);
            return token;
        }

        const possibleTokens: SCToken[] = [];
        for(let chainId in this._chains) {
            const chain = this._chains[chainId];
            if(chain.chainInterface.isValidToken(tickerOrAddress)) {
                //Try to find in known token addresses
                const token = this._tokens[chainId]?.[tickerOrAddress];
                if(token!=null) return token;
            } else {
                //Check in known tickers
                const token = this._tokensByTicker[chainId]?.[tickerOrAddress];
                if(token!=null) possibleTokens.push(token);
            }
        }

        if(possibleTokens.length===0) throw new UserError(`Specified token address or ticker ${tickerOrAddress} not found!`);
        //In case we've found the token in multiple chains
        if(possibleTokens.length>1) throw new UserError(`A ticker ${tickerOrAddress} has been found in multiple chains, narrow it down by using <chainId>-${tickerOrAddress} notation`)
        return possibleTokens[0];
    }

    /**
     * Creates a child swapper instance with a given smart chain
     *
     * @param chainIdentifier Smart chain identifier for the created child swapper instance
     */
    withChain<ChainIdentifier extends ChainIds<T>>(chainIdentifier: ChainIdentifier): SwapperWithChain<T, ChainIdentifier> {
        if(this._chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier);
        return new SwapperWithChain<T, ChainIdentifier>(this, chainIdentifier as ChainIdentifier);
    }

    /**
     * Returns an array of all the supported smart chains
     */
    getSmartChains(): ChainIds<T>[] {
        return Object.keys(this._chains);
    }

    /**
     * Returns whether the SDK supports a given swap type on a given chain based on currently known LPs
     *
     * @param chainId Smart chain identifier string
     * @param swapType Swap protocol type
     */
    supportsSwapType<
        ChainIdentifier extends ChainIds<T>,
        Type extends SwapType
    >(chainId: ChainIdentifier, swapType: Type): SupportsSwapType<T[ChainIdentifier], Type> {
        return (this._chains[chainId]?.wrappers[swapType] != null) as any;
    }

    /**
     * @internal
     */
    getSwapType<C extends ChainIds<T>>(srcToken: BtcToken<true>, dstToken: SCToken<C>): (SupportsSwapType<T[C], SwapType.FROM_BTCLN_AUTO> extends true ? SwapType.FROM_BTCLN_AUTO : SwapType.FROM_BTCLN);
    /**
     * @internal
     */
    getSwapType<C extends ChainIds<T>>(srcToken: BtcToken<false>, dstToken: SCToken<C>): (SupportsSwapType<T[C], SwapType.SPV_VAULT_FROM_BTC> extends true ? SwapType.SPV_VAULT_FROM_BTC : SwapType.FROM_BTC);
    /**
     * @internal
     */
    getSwapType<C extends ChainIds<T>>(srcToken: SCToken<C>, dstToken: BtcToken<false>): SwapType.TO_BTC;
    /**
     * @internal
     */
    getSwapType<C extends ChainIds<T>>(srcToken: SCToken<C>, dstToken: BtcToken<true>): SwapType.TO_BTCLN;
    /**
     * Returns type of the swap based on input and output tokens specified
     *
     * @param srcToken Source token
     * @param dstToken Destination token
     */
    getSwapType<C extends ChainIds<T>>(srcToken: Token<C>, dstToken: Token<C>): SwapType.FROM_BTCLN_AUTO | SwapType.FROM_BTCLN | SwapType.SPV_VAULT_FROM_BTC | SwapType.FROM_BTC | SwapType.TO_BTC | SwapType.TO_BTCLN;
    /**
     * Returns type of the swap based on input and output tokens specified
     *
     * @param srcToken Source token
     * @param dstToken Destination token
     */
    getSwapType<C extends ChainIds<T>>(srcToken: Token<C>, dstToken: Token<C>): SwapType.FROM_BTCLN_AUTO | SwapType.FROM_BTCLN | SwapType.SPV_VAULT_FROM_BTC | SwapType.FROM_BTC | SwapType.TO_BTC | SwapType.TO_BTCLN {
        if(isSCToken(srcToken)) {
            if(!isBtcToken(dstToken)) throw new Error("Swap not supported");
            if(dstToken.lightning) {
                return SwapType.TO_BTCLN;
            } else {
                return SwapType.TO_BTC;
            }
        } else if(isBtcToken(srcToken)) {
            if(!isSCToken(dstToken)) throw new Error("Swap not supported");
            if(srcToken.lightning) {
                if(this.supportsSwapType(dstToken.chainId, SwapType.FROM_BTCLN_AUTO)) {
                    return SwapType.FROM_BTCLN_AUTO;
                } else {
                    return SwapType.FROM_BTCLN;
                }
            } else {
                if(this.supportsSwapType(dstToken.chainId, SwapType.SPV_VAULT_FROM_BTC)) {
                    return SwapType.SPV_VAULT_FROM_BTC;
                } else {
                    return SwapType.FROM_BTC;
                }
            }
        }
        throw new Error("Swap not supported");
    }

    /**
     * Helper information about various swap protocol and their features:
     * - `requiresInputWallet`: Whether a swap requires a connected wallet on the input chain able to sign
     *  arbitrary transaction
     * - `requiresOutputWallet`: Whether a swap requires a connected wallet on the output chain able to sign
     *  arbitrary transactions
     * - `supportsGasDrop`: Whether a swap supports the "gas drop" feature, allowing to user to receive a small
     *  amount of native token as part of the swap when swapping to smart chains
     *
     * Uses a `Record` type here, use the {@link SwapProtocolInfo} import for a literal readonly type, with
     *  pre-filled exact values in the type.
     */
    readonly SwapTypeInfo = SwapProtocolInfo as Record<SwapType, {
        requiresInputWallet: boolean,
        requiresOutputWallet: boolean,
        supportsGasDrop: boolean
    }>;

    /**
     * Returns minimum/maximum limits for inputs and outputs for a swap between given tokens
     *
     * @param srcToken Source token
     * @param dstToken Destination token
     */
    getSwapLimits<C extends ChainIds<T>, A extends Token<C>, B extends Token<C>>(srcToken: A, dstToken: B): {
        input: {min: TokenAmount<A>, max?: TokenAmount<A>},
        output: {min: TokenAmount<B>, max?: TokenAmount<B>}
    } {
        const swapType = this.getSwapType(srcToken, dstToken);
        const scToken = isSCToken(srcToken) ? srcToken : isSCToken(dstToken) ? dstToken : null;
        if(scToken==null) throw new Error("At least one token needs to be a smart chain token!");
        const result: {input: {min?: bigint, max?: bigint}, output: {min?: bigint, max?: bigint}} = {
            input: {},
            output: {}
        };
        for(let lp of this.intermediaryDiscovery.intermediaries) {
            const lpMinMax = lp.getSwapLimits(swapType, scToken.chainId, scToken.address);
            if(lpMinMax==null) continue;
            result.input.min = result.input.min==null ? lpMinMax.input.min : bigIntMin(result.input.min, lpMinMax.input.min);
            result.input.max = result.input.max==null ? lpMinMax.input.max : bigIntMax(result.input.max, lpMinMax.input.max);
            result.output.min = result.output.min==null ? lpMinMax.output.min : bigIntMin(result.output.min, lpMinMax.output.min);
            result.output.max = result.output.max==null ? lpMinMax.output.max : bigIntMax(result.output.max, lpMinMax.output.max);
        }
        return {
            input: {
                min: toTokenAmount(result.input.min ?? 1n, srcToken, this.prices),
                max: result.input.max==null ? undefined : toTokenAmount(result.input.max, srcToken, this.prices),
            },
            output: {
                min: toTokenAmount(result.output.min ?? 1n, dstToken, this.prices),
                max: result.output.max==null ? undefined : toTokenAmount(result.output.max, dstToken, this.prices),
            }
        }
    }

    /**
     * Returns an array of supported tokens either on the input or on the output of a swap
     *
     * @param input Whether to return input tokens or output tokens
     */
    getSupportedTokens(input: SwapSide | boolean): Token[] {
        const tokens: {[chainId: string]: Set<string>} = {};
        let lightning = false;
        let btc = false;
        this.intermediaryDiscovery.intermediaries.forEach(lp => {
            for(let swapType of [SwapType.TO_BTC, SwapType.TO_BTCLN, SwapType.FROM_BTC, SwapType.FROM_BTCLN, SwapType.SPV_VAULT_FROM_BTC, SwapType.FROM_BTCLN_AUTO]) {
                if(lp.services[swapType]?.chainTokens==null) continue;
                for(let chainId of this.getSmartChains()) {
                    if(this.supportsSwapType(chainId, SwapType.SPV_VAULT_FROM_BTC) ? swapType===SwapType.FROM_BTC : swapType===SwapType.SPV_VAULT_FROM_BTC) continue;
                    if(this.supportsSwapType(chainId, SwapType.FROM_BTCLN_AUTO) ? swapType===SwapType.FROM_BTCLN : swapType===SwapType.FROM_BTCLN_AUTO) continue;
                    const chainTokens = lp.services[swapType]?.chainTokens?.[chainId];
                    if(chainTokens==null) continue;
                    for (let tokenAddress of chainTokens) {
                        if(input) {
                            if(swapType===SwapType.TO_BTC || swapType===SwapType.TO_BTCLN) {
                                tokens[chainId] ??= new Set();
                                tokens[chainId].add(tokenAddress);
                            }
                            if(swapType===SwapType.FROM_BTCLN || swapType===SwapType.FROM_BTCLN_AUTO) {
                                lightning = true;
                            }
                            if(swapType===SwapType.FROM_BTC || swapType===SwapType.SPV_VAULT_FROM_BTC) {
                                btc = true;
                            }
                        } else {
                            if(swapType===SwapType.FROM_BTCLN || swapType===SwapType.FROM_BTC || swapType===SwapType.SPV_VAULT_FROM_BTC || swapType===SwapType.FROM_BTCLN_AUTO) {
                                tokens[chainId] ??= new Set();
                                tokens[chainId].add(tokenAddress);
                            }
                            if(swapType===SwapType.TO_BTCLN) {
                                lightning = true;
                            }
                            if(swapType===SwapType.TO_BTC) {
                                btc = true;
                            }
                        }
                    }
                }
            }
        });
        const output: Token[] = [];
        if(lightning) output.push(BitcoinTokens.BTCLN);
        if(btc) output.push(BitcoinTokens.BTC);
        for(let chainId in tokens) {
            tokens[chainId].forEach(tokenAddress => {
                const token = this._tokens?.[chainId]?.[tokenAddress];
                if(token!=null) output.push(token);
            })
        }
        return output;
    }

    /**
     * Returns a set of supported tokens by all the intermediaries offering a specific swap service
     *
     * @param _swapType Swap service type to check supported tokens for
     */
    private getSupportedTokensForSwapType(_swapType: SwapType): SCToken[] {
        const tokens: {[chainId: string]: Set<string>} = {};
        this.intermediaryDiscovery.intermediaries.forEach(lp => {
            for(let chainId of this.getSmartChains()) {
                let swapType = _swapType;
                if(swapType===SwapType.FROM_BTC && this.supportsSwapType(chainId, SwapType.SPV_VAULT_FROM_BTC)) swapType = SwapType.SPV_VAULT_FROM_BTC;
                if(swapType===SwapType.FROM_BTCLN && this.supportsSwapType(chainId, SwapType.FROM_BTCLN_AUTO)) swapType = SwapType.FROM_BTCLN_AUTO;
                if(lp.services[swapType]?.chainTokens==null) break;
                const chainTokens = lp.services[swapType]?.chainTokens?.[chainId];
                if(chainTokens==null) continue;
                for(let tokenAddress of chainTokens) {
                    tokens[chainId] ??= new Set();
                    tokens[chainId].add(tokenAddress);
                }
            }
        });
        const output: SCToken[] = [];
        for(let chainId in tokens) {
            tokens[chainId].forEach(tokenAddress => {
                const token = this._tokens?.[chainId]?.[tokenAddress];
                if(token!=null) output.push(token);
            })
        }
        return output;
    }

    /**
     * Returns the set of supported token addresses by all the intermediaries we know of offering a specific swapType service
     *
     * @param chainIdentifier Chain identifier string
     * @param swapType Specific swap type for which to obtain supported tokens
     */
    private getSupportedTokenAddresses<ChainIdentifier extends ChainIds<T>>(chainIdentifier: ChainIdentifier, swapType: SwapType): Set<string> {
        const set = new Set<string>();
        this.intermediaryDiscovery.intermediaries.forEach(lp => {
            const chainTokens = lp.services[swapType]?.chainTokens?.[chainIdentifier];
            if(chainTokens==null) return;
            chainTokens.forEach(token => set.add(token));
        });
        return set;
    }

    /**
     * Returns tokens that you can swap to (if input=true) from a given token,
     *  or tokens that you can swap from (if input=false) to a given token
     */
    getSwapCounterTokens(token: Token, input: SwapSide | boolean): Token[] {
        if(isSCToken(token)) {
            const result: Token[] = [];
            if(input) {
                //TO_BTC or TO_BTCLN
                if(this.getSupportedTokenAddresses(token.chainId, SwapType.TO_BTCLN).has(token.address)) {
                    result.push(BitcoinTokens.BTCLN);
                }
                if(this.getSupportedTokenAddresses(token.chainId, SwapType.TO_BTC).has(token.address)) {
                    result.push(BitcoinTokens.BTC);
                }
            } else {
                //FROM_BTC or FROM_BTCLN
                const fromLightningSwapType = this.supportsSwapType(token.chainId, SwapType.FROM_BTCLN_AUTO) ? SwapType.FROM_BTCLN_AUTO : SwapType.FROM_BTCLN;
                if(this.getSupportedTokenAddresses(token.chainId, fromLightningSwapType).has(token.address)) {
                    result.push(BitcoinTokens.BTCLN);
                }
                const fromOnchainSwapType = this.supportsSwapType(token.chainId, SwapType.SPV_VAULT_FROM_BTC) ? SwapType.SPV_VAULT_FROM_BTC : SwapType.FROM_BTC;
                if(this.getSupportedTokenAddresses(token.chainId, fromOnchainSwapType).has(token.address)) {
                    result.push(BitcoinTokens.BTC);
                }
            }
            return result;
        } else {
            if(input) {
                if(token.lightning) {
                    return this.getSupportedTokensForSwapType(SwapType.FROM_BTCLN);
                } else {
                    return this.getSupportedTokensForSwapType(SwapType.FROM_BTC);
                }
            } else {
                if(token.lightning) {
                    return this.getSupportedTokensForSwapType(SwapType.TO_BTCLN);
                } else {
                    return this.getSupportedTokensForSwapType(SwapType.TO_BTC);
                }
            }
        }
    }

}
