import {isISwapInit, ISwap, ISwapInit} from "../ISwap";
import {
    BtcTx,
    BtcTxWithBlockheight,
    ChainType,
    isAbstractSigner,
    SpvWithdrawalClaimedState,
    SpvWithdrawalClosedState,
    SpvWithdrawalFrontedState,
    SpvWithdrawalState,
    SpvWithdrawalStateType
} from "@atomiqlabs/base";
import {SwapType} from "../../enums/SwapType";
import {SpvFromBTCTypeDefinition, SpvFromBTCWrapper} from "./SpvFromBTCWrapper";
import {extendAbortController} from "../../utils/Utils";
import {parsePsbtTransaction, toCoinselectAddressType, toOutputScript} from "../../utils/BitcoinUtils";
import {getInputType, Transaction} from "@scure/btc-signer";
import {Buffer} from "buffer";
import {Fee} from "../../types/fees/Fee";
import {BitcoinWalletUtxo, IBitcoinWallet, isIBitcoinWallet} from "../../bitcoin/wallet/IBitcoinWallet";
import {IntermediaryAPI} from "../../intermediaries/apis/IntermediaryAPI";
import {IBTCWalletSwap} from "../IBTCWalletSwap";
import {ISwapWithGasDrop} from "../ISwapWithGasDrop";
import {
    MinimalBitcoinWalletInterface,
    MinimalBitcoinWalletInterfaceWithSigner
} from "../../types/wallets/MinimalBitcoinWalletInterface";
import {IClaimableSwap} from "../IClaimableSwap";
import {FeeType} from "../../enums/FeeType";
import {ppmToPercentage} from "../../types/fees/PercentagePPM";
import {TokenAmount, toTokenAmount} from "../../types/TokenAmount";
import {BitcoinTokens, BtcToken, SCToken} from "../../types/Token";
import {getLogger, LoggerType} from "../../utils/Logger";
import {timeoutPromise} from "../../utils/TimeoutUtils";
import {
    deserializePriceInfoType,
    isPriceInfoType,
    PriceInfoType,
    serializePriceInfoType
} from "../../types/PriceInfoType";
import {toBitcoinWallet} from "../../utils/BitcoinWalletUtils";
import {
    SwapExecutionAction,
    SwapExecutionActionSignPSBT, SwapExecutionActionSignSmartChainTx,
    SwapExecutionActionWait
} from "../../types/SwapExecutionAction";
import {
    SwapExecutionStepPayment,
    SwapExecutionStepSettlement
} from "../../types/SwapExecutionStep";
import {SwapStateInfo} from "../../types/SwapStateInfo";

/**
 * State enum for SPV vault (UTXO-controlled vault) based swaps
 * @category Swaps/Bitcoin → Smart chain
 */
export enum SpvFromBTCSwapState {
    /**
     * Catastrophic failure has occurred when processing the swap on the smart chain side,
     *  this implies a bug in the smart contract code or the user and intermediary deliberately
     *  creating a bitcoin transaction with invalid format unparsable by the smart contract.
     */
    CLOSED = -5,
    /**
     * Some of the bitcoin swap transaction inputs were double-spent, this means the swap
     *  has failed and no BTC was sent
     */
    FAILED = -4,
    /**
     * The intermediary (LP) declined to co-sign the submitted PSBT, hence the swap failed
     */
    DECLINED = -3,
    /**
     * Swap has expired for good and there is no way how it can be executed anymore
     */
    QUOTE_EXPIRED = -2,
    /**
     * A swap is almost expired, and it should be presented to the user as expired, though
     *  there is still a chance that it will be processed
     */
    QUOTE_SOFT_EXPIRED = -1,
    /**
     * Swap was created, use the {@link SpvFromBTCSwap.getFundedPsbt} or {@link SpvFromBTCSwap.getPsbt} functions
     *  to get the bitcoin swap PSBT that should be signed by the user's wallet and then submitted via the
     *  {@link SpvFromBTCSwap.submitPsbt} function.
     */
    CREATED = 0,
    /**
     * Swap bitcoin PSBT was submitted by the client to the SDK
     */
    SIGNED = 1,
    /**
     * Swap bitcoin PSBT sent to the intermediary (LP), waiting for the intermediary co-sign
     *  it and broadcast. You can use the {@link SpvFromBTCSwap.waitTillClaimedOrFronted}
     *  function to wait till the intermediary broadcasts the transaction and the transaction
     *  confirms.
     */
    POSTED = 2,
    /**
     * Intermediary (LP) has co-signed and broadcasted the bitcoin transaction. You can use the
     *  {@link SpvFromBTCSwap.waitTillClaimedOrFronted} function to wait till the transaction
     *  confirms.
     */
    BROADCASTED = 3,
    /**
     * Settlement on the destination smart chain was fronted and funds were already received
     *  by the user, even before the final settlement.
     */
    FRONTED = 4,
    /**
     * Bitcoin transaction confirmed with necessary amount of confirmations, wait for automatic
     *  settlement by the watchtower with the {@link waitTillClaimedOrFronted} function, or settle manually
     *  using the {@link FromBTCSwap.claim} or {@link FromBTCSwap.txsClaim} function.
     */
    BTC_TX_CONFIRMED = 5,
    /**
     * Swap settled on the smart chain and funds received
     */
    CLAIMED = 6
}

const SpvFromBTCSwapStateDescription = {
    [SpvFromBTCSwapState.CLOSED]: "Catastrophic failure has occurred when processing the swap on the smart chain side, this implies a bug in the smart contract code or the user and intermediary deliberately creating a bitcoin transaction with invalid format unparsable by the smart contract.",
    [SpvFromBTCSwapState.FAILED]: "Some of the bitcoin swap transaction inputs were double-spent, this means the swap has failed and no BTC was sent",
    [SpvFromBTCSwapState.DECLINED]: "The intermediary (LP) declined to co-sign the submitted PSBT, hence the swap failed",
    [SpvFromBTCSwapState.QUOTE_EXPIRED]: "Swap has expired for good and there is no way how it can be executed anymore",
    [SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED]: "A swap is almost expired, and it should be presented to the user as expired, though there is still a chance that it will be processed",
    [SpvFromBTCSwapState.CREATED]: "Swap was created, get the bitcoin swap PSBT that should be signed by the user's wallet and then submit it back to the SDK.",
    [SpvFromBTCSwapState.SIGNED]: "Swap bitcoin PSBT was submitted by the client to the SDK",
    [SpvFromBTCSwapState.POSTED]: "Swap bitcoin PSBT sent to the intermediary (LP), waiting for the intermediary co-sign it and broadcast.",
    [SpvFromBTCSwapState.BROADCASTED]: "Intermediary (LP) has co-signed and broadcasted the bitcoin transaction.",
    [SpvFromBTCSwapState.FRONTED]: "Settlement on the destination smart chain was fronted and funds were already received by the user, even before the final settlement.",
    [SpvFromBTCSwapState.BTC_TX_CONFIRMED]: "Bitcoin transaction confirmed with necessary amount of confirmations, wait for automatic settlement by the watchtower or settle manually.",
    [SpvFromBTCSwapState.CLAIMED]: "Swap settled on the smart chain and funds received"
}

export type SpvFromBTCSwapInit = ISwapInit & {
    quoteId: string;
    recipient: string;
    vaultOwner: string;
    vaultId: bigint;
    vaultRequiredConfirmations: number;
    vaultTokenMultipliers: bigint[];
    vaultBtcAddress: string;
    vaultUtxo: string;
    vaultUtxoValue: bigint;
    btcDestinationAddress: string;
    btcAmount: bigint;
    btcAmountSwap: bigint;
    btcAmountGas: bigint;
    minimumBtcFeeRate: number;
    outputTotalSwap: bigint;
    outputSwapToken: string;
    outputTotalGas: bigint;
    outputGasToken: string;
    gasSwapFeeBtc: bigint;
    gasSwapFee: bigint;
    callerFeeShare: bigint;
    frontingFeeShare: bigint;
    executionFeeShare: bigint;
    genesisSmartChainBlockHeight: number;
    gasPricingInfo?: PriceInfoType;
};

export function isSpvFromBTCSwapInit(obj: any): obj is SpvFromBTCSwapInit {
    return typeof obj === "object" &&
        typeof(obj.quoteId)==="string" &&
        typeof(obj.recipient)==="string" &&
        typeof(obj.vaultOwner)==="string" &&
        typeof(obj.vaultId)==="bigint" &&
        typeof(obj.vaultRequiredConfirmations)==="number" &&
        Array.isArray(obj.vaultTokenMultipliers) && obj.vaultTokenMultipliers.reduce((prev: boolean, curr: any) => prev && typeof(curr)==="bigint", true) &&
        typeof(obj.vaultBtcAddress)==="string" &&
        typeof(obj.vaultUtxo)==="string" &&
        typeof(obj.vaultUtxoValue)==="bigint" &&
        typeof(obj.btcDestinationAddress)==="string" &&
        typeof(obj.btcAmount)==="bigint" &&
        typeof(obj.btcAmountSwap)==="bigint" &&
        typeof(obj.btcAmountGas)==="bigint" &&
        typeof(obj.minimumBtcFeeRate)==="number" &&
        typeof(obj.outputTotalSwap)==="bigint" &&
        typeof(obj.outputSwapToken)==="string" &&
        typeof(obj.outputTotalGas)==="bigint" &&
        typeof(obj.outputGasToken)==="string" &&
        typeof(obj.gasSwapFeeBtc)==="bigint" &&
        typeof(obj.gasSwapFee)==="bigint" &&
        typeof(obj.callerFeeShare)==="bigint" &&
        typeof(obj.frontingFeeShare)==="bigint" &&
        typeof(obj.executionFeeShare)==="bigint" &&
        typeof(obj.genesisSmartChainBlockHeight)==="number" &&
        (obj.gasPricingInfo==null || isPriceInfoType(obj.gasPricingInfo)) &&
        isISwapInit(obj);
}

/**
 * New spv vault (UTXO-controlled vault) based swaps for Bitcoin -> Smart chain swaps not requiring
 *  any initiation on the destination chain, and with the added possibility for the user to receive
 *  a native token on the destination chain as part of the swap (a "gas drop" feature).
 *
 * @category Swaps/Bitcoin → Smart chain
 */
export class SpvFromBTCSwap<T extends ChainType>
    extends ISwap<T, SpvFromBTCTypeDefinition<T>>
    implements IBTCWalletSwap, ISwapWithGasDrop<T>, IClaimableSwap<T, SpvFromBTCTypeDefinition<T>, SpvFromBTCSwapState> {

    protected readonly currentVersion: number = 2;

    readonly TYPE: SwapType.SPV_VAULT_FROM_BTC = SwapType.SPV_VAULT_FROM_BTC;

    /**
     * @internal
     */
    protected readonly swapStateDescription = SpvFromBTCSwapStateDescription;
    /**
     * @internal
     */
    protected readonly swapStateName = (state: number) => SpvFromBTCSwapState[state];
    /**
     * @inheritDoc
     * @internal
     */
    protected readonly logger: LoggerType;

    private readonly quoteId: string;
    private readonly recipient: string;

    private readonly vaultOwner: string;
    private readonly vaultId: bigint;
    private readonly vaultRequiredConfirmations: number;
    private readonly vaultTokenMultipliers: bigint[];

    private readonly vaultBtcAddress: string;
    private readonly vaultUtxo: string;
    private readonly vaultUtxoValue: bigint;

    private readonly btcDestinationAddress: string;
    private readonly btcAmount: bigint;
    private readonly btcAmountSwap: bigint;
    private readonly btcAmountGas: bigint;

    private readonly outputTotalSwap: bigint;
    private readonly outputSwapToken: string;
    private readonly outputTotalGas: bigint;
    private readonly outputGasToken: string;

    private readonly gasSwapFeeBtc: bigint;
    private readonly gasSwapFee: bigint;

    private readonly callerFeeShare: bigint;
    private readonly frontingFeeShare: bigint;
    private readonly executionFeeShare: bigint;

    private readonly gasPricingInfo?: PriceInfoType;

    private posted?: boolean;

    /**
     * @internal
     */
    readonly _genesisSmartChainBlockHeight: number;
    /**
     * @internal
     */
    _senderAddress?: string;
    /**
     * @internal
     */
    _claimTxId?: string;
    /**
     * @internal
     */
    _frontTxId?: string;
    /**
     * @internal
     */
    _data?: T["SpvVaultWithdrawalData"];

    /**
     * Minimum fee rate in sats/vB that the input bitcoin transaction needs to pay
     */
    readonly minimumBtcFeeRate: number;

    /**
     * Time at which the SDK realized the bitcoin transaction was confirmed
     * @private
     */
    private btcTxConfirmedAt?: number;

    private _contract: T["SpvVaultContract"];

    constructor(wrapper: SpvFromBTCWrapper<T>, init: SpvFromBTCSwapInit);
    constructor(wrapper: SpvFromBTCWrapper<T>, obj: any);
    constructor(wrapper: SpvFromBTCWrapper<T>, initOrObject: SpvFromBTCSwapInit | any) {
        if(isSpvFromBTCSwapInit(initOrObject) && initOrObject.url!=null) initOrObject.url += "/frombtc_spv";
        super(wrapper, initOrObject);
        if(isSpvFromBTCSwapInit(initOrObject)) {
            this._state = SpvFromBTCSwapState.CREATED;
            this.quoteId = initOrObject.quoteId;
            this.recipient = initOrObject.recipient;
            this.vaultOwner = initOrObject.vaultOwner;
            this.vaultId = initOrObject.vaultId;
            this.vaultRequiredConfirmations = initOrObject.vaultRequiredConfirmations;
            this.vaultTokenMultipliers = initOrObject.vaultTokenMultipliers;
            this.vaultBtcAddress = initOrObject.vaultBtcAddress;
            this.vaultUtxo = initOrObject.vaultUtxo;
            this.vaultUtxoValue = initOrObject.vaultUtxoValue;
            this.btcDestinationAddress = initOrObject.btcDestinationAddress;
            this.btcAmount = initOrObject.btcAmount;
            this.btcAmountSwap = initOrObject.btcAmountSwap;
            this.btcAmountGas = initOrObject.btcAmountGas;
            this.minimumBtcFeeRate = initOrObject.minimumBtcFeeRate;
            this.outputTotalSwap = initOrObject.outputTotalSwap;
            this.outputSwapToken = initOrObject.outputSwapToken;
            this.outputTotalGas = initOrObject.outputTotalGas;
            this.outputGasToken = initOrObject.outputGasToken;
            this.gasSwapFeeBtc = initOrObject.gasSwapFeeBtc;
            this.gasSwapFee = initOrObject.gasSwapFee;
            this.callerFeeShare = initOrObject.callerFeeShare;
            this.frontingFeeShare = initOrObject.frontingFeeShare;
            this.executionFeeShare = initOrObject.executionFeeShare;
            this._genesisSmartChainBlockHeight = initOrObject.genesisSmartChainBlockHeight;
            this.gasPricingInfo = initOrObject.gasPricingInfo;
            const vaultAddressType = toCoinselectAddressType(toOutputScript(this.wrapper._options.bitcoinNetwork, this.vaultBtcAddress));
            if(vaultAddressType!=="p2tr" && vaultAddressType!=="p2wpkh" && vaultAddressType!=="p2wsh")
                throw new Error("Vault address type must be of witness type: p2tr, p2wpkh, p2wsh");
        } else {
            this.quoteId = initOrObject.quoteId;
            this.recipient = initOrObject.recipient;
            this.vaultOwner = initOrObject.vaultOwner;
            this.vaultId = BigInt(initOrObject.vaultId);
            this.vaultRequiredConfirmations = initOrObject.vaultRequiredConfirmations;
            this.vaultTokenMultipliers = initOrObject.vaultTokenMultipliers.map((val: string) => BigInt(val));
            this.vaultBtcAddress = initOrObject.vaultBtcAddress;
            this.vaultUtxo = initOrObject.vaultUtxo;
            this.vaultUtxoValue = BigInt(initOrObject.vaultUtxoValue);
            this.btcDestinationAddress = initOrObject.btcDestinationAddress;
            this.btcAmount = BigInt(initOrObject.btcAmount);
            this.btcAmountSwap = BigInt(initOrObject.btcAmountSwap);
            this.btcAmountGas = BigInt(initOrObject.btcAmountGas);
            this.minimumBtcFeeRate = initOrObject.minimumBtcFeeRate;
            this.outputTotalSwap = BigInt(initOrObject.outputTotalSwap);
            this.outputSwapToken = initOrObject.outputSwapToken;
            this.outputTotalGas = BigInt(initOrObject.outputTotalGas);
            this.outputGasToken = initOrObject.outputGasToken;
            this.gasSwapFeeBtc = BigInt(initOrObject.gasSwapFeeBtc);
            this.gasSwapFee = BigInt(initOrObject.gasSwapFee);
            this.callerFeeShare = BigInt(initOrObject.callerFeeShare);
            this.frontingFeeShare = BigInt(initOrObject.frontingFeeShare);
            this.executionFeeShare = BigInt(initOrObject.executionFeeShare);
            this._genesisSmartChainBlockHeight = initOrObject.genesisSmartChainBlockHeight;
            this._senderAddress = initOrObject.senderAddress;
            this._claimTxId = initOrObject.claimTxId;
            this._frontTxId = initOrObject.frontTxId;
            this.gasPricingInfo = deserializePriceInfoType(initOrObject.gasPricingInfo);
            this.btcTxConfirmedAt = initOrObject.btcTxConfirmedAt;
            this.posted = initOrObject.posted;
            if(initOrObject.data!=null) this._data = new (this.wrapper._spvWithdrawalDataDeserializer(this._contractVersion))(initOrObject.data);
        }
        this.tryCalculateSwapFee();
        this.logger = getLogger("SPVFromBTC("+this.getId()+"): ");

        this._contract = wrapper._contract(this._contractVersion);
    }

    /**
     * @inheritDoc
     * @internal
     */
    protected upgradeVersion() {
        if(this.version===1) {
            this.posted = this.initiated && this._data!=null;
            this.version = 2;
        }
    }

    /**
     * @inheritDoc
     * @internal
     */
    protected tryCalculateSwapFee() {
        if(this.swapFeeBtc==null && this.swapFee!=null) {
            this.swapFeeBtc = this.swapFee * this.btcAmountSwap / this.getOutputWithoutFee().rawAmount;
        }

        if(this.pricingInfo!=null && this.pricingInfo.swapPriceUSatPerToken==null) {
            const priceUsdPerBtc = this.pricingInfo.realPriceUsdPerBitcoin;
            this.pricingInfo = this.wrapper._prices.recomputePriceInfoReceive(
                this.chainIdentifier,
                this.btcAmountSwap,
                this.pricingInfo.satsBaseFee,
                this.pricingInfo.feePPM,
                this.getOutputWithoutFee().rawAmount,
                this.outputSwapToken
            );
            this.pricingInfo.realPriceUsdPerBitcoin = priceUsdPerBtc;
        }
    }


    //////////////////////////////
    //// Pricing

    /**
     * @inheritDoc
     */
    async refreshPriceData(): Promise<void> {
        if(this.pricingInfo==null) return;
        const usdPricePerBtc = this.pricingInfo.realPriceUsdPerBitcoin;
        this.pricingInfo = await this.wrapper._prices.isValidAmountReceive(
            this.chainIdentifier,
            this.btcAmountSwap,
            this.pricingInfo.satsBaseFee,
            this.pricingInfo.feePPM,
            this.getOutputWithoutFee().rawAmount,
            this.outputSwapToken,
            undefined,
            undefined,
            this.swapFeeBtc
        );
        this.pricingInfo.realPriceUsdPerBitcoin = usdPricePerBtc;
    }


    //////////////////////////////
    //// Getters & utils

    /**
     * @inheritDoc
     * @internal
     */
    _getInitiator(): string {
        return this.recipient;
    }

    /**
     * @inheritDoc
     * @internal
     */
    _getEscrowHash(): string | null {
        return this._data?.btcTx?.txid ?? null;
    }

    /**
     * @inheritDoc
     */
    getId(): string {
        return this.quoteId+this._randomNonce;
    }

    /**
     * @inheritDoc
     */
    getQuoteExpiry(): number {
        return this.expiry - 20*1000;
    }

    /**
     * @inheritDoc
     * @internal
     */
    _verifyQuoteDefinitelyExpired(): Promise<boolean> {
        return Promise.resolve(this.expiry<Date.now());
    }

    /**
     * @inheritDoc
     * @internal
     */
    _verifyQuoteValid(): Promise<boolean> {
        return Promise.resolve(this.expiry>Date.now() && (this._state===SpvFromBTCSwapState.CREATED || this._state===SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED));
    }

    /**
     * @inheritDoc
     */
    getOutputAddress(): string | null {
        return this.recipient;
    }

    /**
     * @inheritDoc
     */
    getOutputTxId(): string | null {
        return this._frontTxId ?? this._claimTxId ?? null;
    }

    /**
     * @inheritDoc
     */
    getInputAddress(): string | null {
        return this._senderAddress ?? null;
    }

    /**
     * @inheritDoc
     */
    getInputTxId(): string | null {
        return this._data?.btcTx?.txid ?? null;
    }

    /**
     * @inheritDoc
     */
    requiresAction(): boolean {
        return this._state===SpvFromBTCSwapState.BTC_TX_CONFIRMED;
    }

    /**
     * @inheritDoc
     */
    isFinished(): boolean {
        return this.isSuccessful() || this.isFailed() || this.isQuoteExpired();
    }

    /**
     * @inheritDoc
     */
    isClaimable(): boolean {
        return this._state===SpvFromBTCSwapState.BTC_TX_CONFIRMED;
    }

    /**
     * @inheritDoc
     */
    isSuccessful(): boolean {
        return this._state===SpvFromBTCSwapState.FRONTED || this._state===SpvFromBTCSwapState.CLAIMED;
    }

    /**
     * @inheritDoc
     */
    isFailed(): boolean {
        return this._state===SpvFromBTCSwapState.FAILED || this._state===SpvFromBTCSwapState.DECLINED || this._state===SpvFromBTCSwapState.CLOSED;
    }

    /**
     * @inheritDoc
     */
    isInProgress(): boolean {
        return this._state===SpvFromBTCSwapState.POSTED ||
            this._state===SpvFromBTCSwapState.BROADCASTED ||
            this._state===SpvFromBTCSwapState.BTC_TX_CONFIRMED;
    }

    /**
     * @inheritDoc
     */
    isQuoteExpired(): boolean {
        return this._state===SpvFromBTCSwapState.QUOTE_EXPIRED;
    }

    /**
     * @inheritDoc
     */
    isQuoteSoftExpired(): boolean {
        return this._state===SpvFromBTCSwapState.QUOTE_EXPIRED || this._state===SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED;
    }

    /**
     * Returns the data about used spv vault (UTXO-controlled vault) to perform the swap
     */
    getSpvVaultData(): {
        owner: string,
        vaultId: bigint,
        utxo: string
    } {
        return {
            owner: this.vaultOwner,
            vaultId: this.vaultId,
            utxo: this.vaultUtxo
        }
    }


    //////////////////////////////
    //// Amounts & fees

    /**
     * Returns the input BTC amount in sats without any fees
     *
     * @internal
     */
    protected getInputSwapAmountWithoutFee(): bigint {
        return (this.btcAmountSwap - this.swapFeeBtc) * 100_000n / (100_000n + this.callerFeeShare + this.frontingFeeShare + this.executionFeeShare);
    }

    /**
     * Returns the input gas BTC amount in sats without any fees
     *
     * @internal
     */
    protected getInputGasAmountWithoutFee(): bigint {
        return (this.btcAmountGas - this.gasSwapFeeBtc) * 100_000n / (100_000n + this.callerFeeShare + this.frontingFeeShare);
    }

    /**
     * Returns to total input BTC amount in sats without any fees (this is BTC amount for the swap + BTC amount
     *  for the gas drop).
     *
     * @internal
     */
    protected getInputAmountWithoutFee(): bigint {
        return this.getInputSwapAmountWithoutFee() + this.getInputGasAmountWithoutFee();
    }

    /**
     * Returns the swap output amount without any fees, this value is therefore always higher than
     *  the actual received output.
     *
     * @internal
     */
    protected getOutputWithoutFee(): TokenAmount<SCToken<T["ChainId"]>, true> {
        return toTokenAmount(
            (this.outputTotalSwap * (100_000n + this.callerFeeShare + this.frontingFeeShare + this.executionFeeShare) / 100_000n) + (this.swapFee ?? 0n),
            this.wrapper._tokens[this.outputSwapToken], this.wrapper._prices, this.pricingInfo
        );
    }

    /**
     * Returns the swap fee charged by the intermediary (LP) on this swap
     *
     * @internal
     */
    protected getSwapFee(): Fee<T["ChainId"], BtcToken<false>, SCToken<T["ChainId"]>> {
        if(this.pricingInfo==null) throw new Error("No pricing info known, cannot estimate fee!");

        const outputToken = this.wrapper._tokens[this.outputSwapToken];
        const gasSwapFeeInOutputToken = this.gasSwapFeeBtc
            * (10n ** BigInt(outputToken.decimals))
            * 1_000_000n
            / this.pricingInfo.swapPriceUSatPerToken;

        const feeWithoutBaseFee = this.swapFeeBtc - this.pricingInfo.satsBaseFee;
        const swapFeePPM = feeWithoutBaseFee * 1000000n / (this.btcAmount - this.swapFeeBtc - this.gasSwapFeeBtc);

        const amountInSrcToken = toTokenAmount(
            this.swapFeeBtc + this.gasSwapFeeBtc, BitcoinTokens.BTC, this.wrapper._prices, this.pricingInfo
        );
        return {
            amountInSrcToken,
            amountInDstToken: toTokenAmount(this.swapFee + gasSwapFeeInOutputToken, outputToken, this.wrapper._prices, this.pricingInfo),
            currentUsdValue: amountInSrcToken.currentUsdValue,
            usdValue: amountInSrcToken.usdValue,
            pastUsdValue: amountInSrcToken.pastUsdValue,
            composition: {
                base: toTokenAmount(this.pricingInfo.satsBaseFee, BitcoinTokens.BTC, this.wrapper._prices, this.pricingInfo),
                percentage: ppmToPercentage(swapFeePPM)
            }
        };
    }

    /**
     * Returns the fee to be paid to watchtowers on the destination chain to automatically
     *  process and settle this swap without requiring any user interaction
     *
     * @internal
     */
    protected getWatchtowerFee(): Fee<T["ChainId"], BtcToken<false>, SCToken<T["ChainId"]>> {
        if(this.pricingInfo==null) throw new Error("No pricing info known, cannot estimate fee!");

        const totalFeeShare = this.callerFeeShare + this.frontingFeeShare;
        const outputToken = this.wrapper._tokens[this.outputSwapToken];
        const watchtowerFeeInOutputToken = this.getInputGasAmountWithoutFee() * totalFeeShare
            * (10n ** BigInt(outputToken.decimals))
            * 1_000_000n
            / this.pricingInfo.swapPriceUSatPerToken
            / 100_000n;
        const feeBtc = this.getInputAmountWithoutFee() * (totalFeeShare + this.executionFeeShare) / 100_000n;
        const amountInSrcToken = toTokenAmount(feeBtc, BitcoinTokens.BTC, this.wrapper._prices, this.pricingInfo);
        return {
            amountInSrcToken,
            amountInDstToken: toTokenAmount(
                (this.outputTotalSwap * (totalFeeShare + this.executionFeeShare) / 100_000n) + watchtowerFeeInOutputToken,
                outputToken, this.wrapper._prices, this.pricingInfo
            ),
            currentUsdValue: amountInSrcToken.currentUsdValue,
            usdValue: amountInSrcToken.usdValue,
            pastUsdValue: amountInSrcToken.pastUsdValue
        };
    }

    /**
     * @inheritDoc
     */
    getFee(): Fee<T["ChainId"], BtcToken<false>, SCToken<T["ChainId"]>> {
        const swapFee = this.getSwapFee();
        const watchtowerFee = this.getWatchtowerFee();

        const amountInSrcToken = toTokenAmount(
          swapFee.amountInSrcToken.rawAmount + watchtowerFee.amountInSrcToken.rawAmount,
            BitcoinTokens.BTC, this.wrapper._prices, this.pricingInfo
        );
        return {
            amountInSrcToken,
            amountInDstToken: toTokenAmount(
              swapFee.amountInDstToken.rawAmount + watchtowerFee.amountInDstToken.rawAmount,
                this.wrapper._tokens[this.outputSwapToken], this.wrapper._prices, this.pricingInfo
            ),
            currentUsdValue: amountInSrcToken.currentUsdValue,
            usdValue: amountInSrcToken.usdValue,
            pastUsdValue: amountInSrcToken.pastUsdValue
        };
    }

    /**
     * @inheritDoc
     */
    getFeeBreakdown(): [
        {type: FeeType.SWAP, fee: Fee<T["ChainId"], BtcToken<false>, SCToken<T["ChainId"]>>},
        {type: FeeType.NETWORK_OUTPUT, fee: Fee<T["ChainId"], BtcToken<false>, SCToken<T["ChainId"]>>}
    ] {
        return [
            {
                type: FeeType.SWAP,
                fee: this.getSwapFee()
            },
            {
                type: FeeType.NETWORK_OUTPUT,
                fee: this.getWatchtowerFee()
            }
        ];
    }

    /**
     * @inheritDoc
     */
    getOutputToken(): SCToken<T["ChainId"]> {
        return this.wrapper._tokens[this.outputSwapToken];
    }

    /**
     * @inheritDoc
     */
    getOutput(): TokenAmount<SCToken<T["ChainId"]>, true> {
        return toTokenAmount(this.outputTotalSwap, this.wrapper._tokens[this.outputSwapToken], this.wrapper._prices, this.pricingInfo);
    }

    /**
     * @inheritDoc
     */
    getGasDropOutput(): TokenAmount<SCToken<T["ChainId"]>, true> {
        return toTokenAmount(this.outputTotalGas, this.wrapper._tokens[this.outputGasToken], this.wrapper._prices, this.gasPricingInfo);
    }

    /**
     * @inheritDoc
     */
    getInputWithoutFee(): TokenAmount<BtcToken<false>, true> {
        return toTokenAmount(this.getInputAmountWithoutFee(), BitcoinTokens.BTC, this.wrapper._prices, this.pricingInfo);
    }

    /**
     * @inheritDoc
     */
    getInputToken(): BtcToken<false> {
        return BitcoinTokens.BTC;
    }

    /**
     * @inheritDoc
     */
    getInput(): TokenAmount<BtcToken<false>, true> {
        return toTokenAmount(this.btcAmount, BitcoinTokens.BTC, this.wrapper._prices, this.pricingInfo);
    }


    //////////////////////////////
    //// Bitcoin tx

    /**
     * @inheritDoc
     */
    getRequiredConfirmationsCount(): number {
        return this.vaultRequiredConfirmations;
    }

    /**
     * Returns raw transaction details that can be used to manually create a swap PSBT. It is better to use
     *  the {@link getPsbt} or {@link getFundedPsbt} function retrieve an already prepared PSBT.
     */
    async getTransactionDetails(): Promise<{
        in0txid: string,
        in0vout: number,
        in0sequence: number,
        vaultAmount: bigint,
        vaultScript: Uint8Array,
        in1sequence: number,
        out1script: Uint8Array,
        out2amount: bigint,
        out2script: Uint8Array,
        locktime: number
    }> {
        const [txId, voutStr] = this.vaultUtxo.split(":");

        const vaultScript = toOutputScript(this.wrapper._options.bitcoinNetwork, this.vaultBtcAddress);

        const out2script = toOutputScript(this.wrapper._options.bitcoinNetwork, this.btcDestinationAddress);

        const opReturnData = this._contract.toOpReturnData(
            this.recipient,
            [
                this.outputTotalSwap / this.vaultTokenMultipliers[0],
                this.outputTotalGas / this.vaultTokenMultipliers[1]
            ]
        );
        const out1script = Buffer.concat([
            opReturnData.length > 75 ? Buffer.from([0x6a, 0x4c, opReturnData.length]) : Buffer.from([0x6a, opReturnData.length]),
            opReturnData
        ]);

        if(this.callerFeeShare<0n || this.callerFeeShare>0xFFFFFn) throw new Error("Caller fee out of bounds!");
        if(this.frontingFeeShare<0n || this.frontingFeeShare>0xFFFFFn) throw new Error("Fronting fee out of bounds!");
        if(this.executionFeeShare<0n || this.executionFeeShare>0xFFFFFn) throw new Error("Execution fee out of bounds!");

        const nSequence0 = 0x80000000n | (this.callerFeeShare & 0xFFFFFn) | (this.frontingFeeShare & 0b1111_1111_1100_0000_0000n) << 10n;
        const nSequence1 = 0x80000000n | (this.executionFeeShare & 0xFFFFFn) | (this.frontingFeeShare & 0b0000_0000_0011_1111_1111n) << 20n;

        return {
            in0txid: txId,
            in0vout: parseInt(voutStr),
            in0sequence: Number(nSequence0),
            vaultAmount: this.vaultUtxoValue,
            vaultScript,
            in1sequence: Number(nSequence1),
            out1script,
            out2amount: this.btcAmount,
            out2script,
            locktime: 500_000_000 + Math.floor(Math.random() * 1_000_000_000) //Use this as a random salt to make the btc txId unique!
        };
    }

    /**
     * Returns the raw PSBT (not funded), the wallet should fund the PSBT (add its inputs) and importantly **set the nSequence field of the
     *  2nd input** (input 1 - indexing from 0) to the value returned in `in1sequence`, sign the PSBT and then pass
     *  it back to the swap with {@link submitPsbt} function. The transaction should use at least the returned `feeRate`
     *  sats/vB as the transaction fee.
     */
    async getPsbt(): Promise<{
        psbt: Transaction,
        psbtHex: string,
        psbtBase64: string,
        in1sequence: number,
        feeRate: number
    }> {
        const res = await this.getTransactionDetails();
        const psbt = new Transaction({
            allowUnknownOutputs: true,
            allowLegacyWitnessUtxo: true,
            lockTime: res.locktime
        });
        psbt.addInput({
            txid: res.in0txid,
            index: res.in0vout,
            witnessUtxo: {
                amount: res.vaultAmount,
                script: res.vaultScript
            },
            sequence: res.in0sequence
        });
        psbt.addOutput({
            amount: res.vaultAmount,
            script: res.vaultScript
        });
        psbt.addOutput({
            amount: 0n,
            script: res.out1script
        });
        psbt.addOutput({
            amount: res.out2amount,
            script: res.out2script
        });
        const serializedPsbt = Buffer.from(psbt.toPSBT());
        return {
            psbt,
            psbtHex: serializedPsbt.toString("hex"),
            psbtBase64: serializedPsbt.toString("base64"),
            in1sequence: res.in1sequence,
            feeRate: this.minimumBtcFeeRate
        };
    }

    /**
     * Returns the PSBT that is already funded with wallet's UTXOs (runs a coin-selection algorithm to choose UTXOs to use),
     *  also returns inputs indices that need to be signed by the wallet before submitting the PSBT back to the SDK with
     *  {@link submitPsbt}
     *
     * @remarks
     * Note that when passing the `feeRate` argument, the fee must be at least {@link minimumBtcFeeRate} sats/vB.
     *
     * @param _bitcoinWallet Sender's bitcoin wallet
     * @param feeRate Optional fee rate in sats/vB for the transaction
     * @param additionalOutputs additional outputs to add to the PSBT - can be used to collect fees from users
     * @param utxos Pre-fetched list of UTXOs to spend from
     * @param spendFully Instructs the wallet to spend all the passed UTXOs in the transaction without creating any
     *  change output, if the `feeRate` is passed, it will also enforce that the feeRate in sats/vB for the resulting
     *  transaction is not more than 50% and 10 sats/vB larger (considering also the CPFP adjustments)
     */
    async getFundedPsbt(
        _bitcoinWallet: IBitcoinWallet | MinimalBitcoinWalletInterface,
        feeRate?: number,
        additionalOutputs?: ({amount: bigint, outputScript: Uint8Array} | {amount: bigint, address: string})[],
        utxos?: BitcoinWalletUtxo[],
        spendFully?: boolean
    ): Promise<{
        psbt: Transaction,
        psbtHex: string,
        psbtBase64: string,
        signInputs: number[],
        feeRate: number
    }> {
        const bitcoinWallet: IBitcoinWallet = toBitcoinWallet(_bitcoinWallet, this.wrapper._btcRpc, this.wrapper._options.bitcoinNetwork);
        if(feeRate!=null) {
            if(feeRate<this.minimumBtcFeeRate) throw new Error("Bitcoin tx fee needs to be at least "+this.minimumBtcFeeRate+" sats/vB");
        } else {
            feeRate = Math.max(this.minimumBtcFeeRate, await bitcoinWallet.getFeeRate());
        }
        let {psbt, in1sequence} = await this.getPsbt();
        if(additionalOutputs!=null) additionalOutputs.forEach(output => {
            psbt.addOutput({
                amount: output.amount,
                script: (output as {outputScript: Uint8Array}).outputScript ?? toOutputScript(this.wrapper._options.bitcoinNetwork, (output as {address: string}).address)
            });
        });
        psbt = await bitcoinWallet.fundPsbt(psbt, feeRate, utxos, spendFully);
        psbt.updateInput(1, {sequence: in1sequence});
        //Sign every input except the first one
        const signInputs: number[] = [];
        for(let i=1;i<psbt.inputsLength;i++) {
            signInputs.push(i);
        }
        const serializedPsbt = Buffer.from(psbt.toPSBT());
        return {
            psbt,
            psbtHex: serializedPsbt.toString("hex"),
            psbtBase64: serializedPsbt.toString("base64"),
            signInputs,
            feeRate
        };
    }

    /**
     * @inheritDoc
     */
    async submitPsbt(_psbt: Transaction | string): Promise<string> {
        const psbt = parsePsbtTransaction(_psbt);

        //Ensure not expired
        if(this.expiry<Date.now()) {
            throw new Error("Quote expired!");
        }

        //Ensure valid state
        if(this._state!==SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED && this._state!==SpvFromBTCSwapState.CREATED) {
            throw new Error("Invalid swap state!");
        }
        if(this.url==null) throw new Error("LP URL not known, cannot submit PSBT!");

        //Ensure all inputs except the 1st are finalized
        for(let i=1;i<psbt.inputsLength;i++) {
            if(getInputType(psbt.getInput(i)).txType==="legacy")
                throw new Error("Legacy (non-segwit) inputs are not allowed in the transaction!");
            psbt.finalizeIdx(i);
        }
        const btcTx = await this.wrapper._btcRpc.parseTransaction(Buffer.from(psbt.toBytes(true)).toString("hex"));
        const data = await this._contract.getWithdrawalData(btcTx);

        this.logger.debug("submitPsbt(): parsed withdrawal data: ", data);

        //Verify correct withdrawal data
        if(
            !data.isRecipient(this.recipient) ||
            data.rawAmounts[0]*this.vaultTokenMultipliers[0] !== this.outputTotalSwap ||
            (data.rawAmounts[1] ?? 0n)*this.vaultTokenMultipliers[1] !== this.outputTotalGas ||
            data.callerFeeRate!==this.callerFeeShare ||
            data.frontingFeeRate!==this.frontingFeeShare ||
            data.executionFeeRate!==this.executionFeeShare ||
            data.getSpentVaultUtxo()!==this.vaultUtxo ||
            BigInt(data.getNewVaultBtcAmount())!==this.vaultUtxoValue ||
            !data.getNewVaultScript().equals(toOutputScript(this.wrapper._options.bitcoinNetwork, this.vaultBtcAddress)) ||
            data.getExecutionData()!=null
        ) {
            throw new Error("Invalid withdrawal tx data submitted!");
        }

        //Verify correct LP output
        const lpOutput = psbt.getOutput(2);
        if(
            lpOutput.script==null ||
            lpOutput.amount!==this.btcAmount ||
            !toOutputScript(this.wrapper._options.bitcoinNetwork, this.btcDestinationAddress).equals(Buffer.from(lpOutput.script))
        ) {
            throw new Error("Invalid LP bitcoin output in transaction!");
        }

        //Verify vault utxo not spent yet
        if(await this.wrapper._btcRpc.isSpent(this.vaultUtxo)) {
            throw new Error("Vault UTXO already spent, please create new swap!");
        }

        //Verify tx is parsable by the contract
        try {
            await this._contract.checkWithdrawalTx(data);
        } catch (e: any) {
            throw new Error("Transaction not parsable by the contract: "+(e.message ?? e.toString()));
        }

        //Ensure still not expired
        if(this.expiry<Date.now()) {
            throw new Error("Quote expired!");
        }

        this._data = data;
        this.initiated = true;
        this.posted = true;
        await this._saveAndEmit(SpvFromBTCSwapState.SIGNED);

        try {
            await this.wrapper._lpApi.initSpvFromBTC(
                this.chainIdentifier,
                this.url,
                {
                    quoteId: this.quoteId,
                    psbtHex: Buffer.from(psbt.toPSBT(0)).toString("hex")
                }
            );
            await this._saveAndEmit(SpvFromBTCSwapState.POSTED);
        } catch (e) {
            await this._saveAndEmit(SpvFromBTCSwapState.DECLINED);
            throw e;
        }

        return this._data.getTxId();
    }

    /**
     * @inheritDoc
     */
    async estimateBitcoinFee(_bitcoinWallet: IBitcoinWallet | MinimalBitcoinWalletInterface, feeRate?: number): Promise<TokenAmount<BtcToken<false>, true> | null> {
        const bitcoinWallet: IBitcoinWallet = toBitcoinWallet(_bitcoinWallet, this.wrapper._btcRpc, this.wrapper._options.bitcoinNetwork);
        const txFee = await bitcoinWallet.getFundedPsbtFee((await this.getPsbt()).psbt, feeRate);
        if(txFee==null) return null;
        return toTokenAmount(BigInt(txFee), BitcoinTokens.BTC, this.wrapper._prices, this.pricingInfo);
    }

    /**
     * @inheritDoc
     */
    async sendBitcoinTransaction(
        wallet: IBitcoinWallet | MinimalBitcoinWalletInterfaceWithSigner,
        feeRate?: number,
        utxos?: BitcoinWalletUtxo[],
        spendFully?: boolean
    ): Promise<string> {
        const {psbt, psbtBase64, psbtHex, signInputs} = await this.getFundedPsbt(wallet, feeRate, undefined, utxos, spendFully);
        let signedPsbt: Transaction | string;
        if(isIBitcoinWallet(wallet)) {
            signedPsbt = await wallet.signPsbt(psbt, signInputs);
        } else {
            signedPsbt = await wallet.signPsbt({
                psbt, psbtHex, psbtBase64
            }, signInputs);
        }
        return await this.submitPsbt(signedPsbt);
    }

    /**
     * Executes the swap with the provided bitcoin wallet
     *
     * @param wallet Bitcoin wallet to use to sign the bitcoin transaction
     * @param callbacks Callbacks to track the progress of the swap
     * @param options Optional options for the swap like feeRate, AbortSignal, and timeouts/intervals
     *
     * @returns {boolean} Whether a swap was settled automatically by swap watchtowers or requires manual claim by the
     *  user, in case `false` is returned the user should call the {@link claim} function to settle the swap on the
     *  destination manually
     */
    async execute(
        wallet: IBitcoinWallet | MinimalBitcoinWalletInterfaceWithSigner,
        callbacks?: {
            onSourceTransactionSent?: (sourceTxId: string) => void,
            onSourceTransactionConfirmationStatus?: (sourceTxId?: string, confirmations?: number, targetConfirations?: number, etaMs?: number) => void,
            onSourceTransactionConfirmed?: (sourceTxId: string) => void,
            onSwapSettled?: (destinationTxId: string) => void
        },
        options?: {
            feeRate?: number,
            abortSignal?: AbortSignal,
            btcTxCheckIntervalSeconds?: number,
            maxWaitTillAutomaticSettlementSeconds?: number,
            utxos?: BitcoinWalletUtxo[],
            spendFully?: boolean
        }
    ): Promise<boolean> {
        if(this._state===SpvFromBTCSwapState.CLOSED) throw new Error("Swap encountered a catastrophic failure!");
        if(this._state===SpvFromBTCSwapState.FAILED) throw new Error("Swap failed!");
        if(this._state===SpvFromBTCSwapState.DECLINED) throw new Error("Swap execution already declined by the LP!");
        if(this._state===SpvFromBTCSwapState.QUOTE_EXPIRED) throw new Error("Swap quote expired!");
        if(this._state===SpvFromBTCSwapState.CLAIMED || this._state===SpvFromBTCSwapState.FRONTED) throw new Error("Swap already settled or fronted!");

        if(this._state===SpvFromBTCSwapState.CREATED) {
            const txId = await this.sendBitcoinTransaction(wallet, options?.feeRate, options?.utxos, options?.spendFully);
            if(callbacks?.onSourceTransactionSent!=null) callbacks.onSourceTransactionSent(txId);
        }
        if(this._state===SpvFromBTCSwapState.POSTED || this._state===SpvFromBTCSwapState.BROADCASTED) {
            const txId = await this.waitForBitcoinTransaction(callbacks?.onSourceTransactionConfirmationStatus, options?.btcTxCheckIntervalSeconds, options?.abortSignal);
            if (callbacks?.onSourceTransactionConfirmed != null) callbacks.onSourceTransactionConfirmed(txId);
        }
        // @ts-ignore
        if(this._state===SpvFromBTCSwapState.CLAIMED || this._state===SpvFromBTCSwapState.FRONTED) return true;
        if(this._state===SpvFromBTCSwapState.BTC_TX_CONFIRMED) {
            const success = await this.waitTillClaimedOrFronted(options?.maxWaitTillAutomaticSettlementSeconds ?? 60, options?.abortSignal);
            if(success && callbacks?.onSwapSettled!=null) callbacks.onSwapSettled(this.getOutputTxId()!);
            return success;
        }

        throw new Error("Unexpected state reached!");
    }

    /**
     * @internal
     */
    protected async _getExecutionStatus(options?: {
        bitcoinFeeRate?: number,
        bitcoinWallet?: MinimalBitcoinWalletInterface,
        manualSettlementSmartChainSigner?: string | T["Signer"] | T["NativeSigner"],
        maxWaitTillAutomaticSettlementSeconds?: number
    }) {
        const state = this._state;
        const now = Date.now();

        let confirmations: {
            current: number,
            target: number,
            etaSeconds: number
        } | undefined;

        let bitcoinPaymentStatus: SwapExecutionStepPayment<"BITCOIN">["status"] = "inactive";
        let destinationSettlementStatus: SwapExecutionStepSettlement<T["ChainId"], "awaiting_automatic" | "awaiting_manual">["status"] = "inactive";
        let buildCurrentAction: (actionOptions?: {
            bitcoinFeeRate?: number,
            bitcoinWallet?: MinimalBitcoinWalletInterface,
            manualSettlementSmartChainSigner?: string | T["Signer"] | T["NativeSigner"],
            maxWaitTillAutomaticSettlementSeconds?: number
        }) => Promise<
            SwapExecutionActionSignPSBT |
            SwapExecutionActionWait<"BITCOIN_CONFS" | "SETTLEMENT"> |
            SwapExecutionActionSignSmartChainTx<T> |
            undefined
        > = async () => undefined;

        switch(state) {
            case SpvFromBTCSwapState.QUOTE_EXPIRED:
            case SpvFromBTCSwapState.DECLINED:
            case SpvFromBTCSwapState.FAILED:
                bitcoinPaymentStatus = "expired";
                break;
            case SpvFromBTCSwapState.CREATED: {
                const quoteValid = await this._verifyQuoteValid();
                bitcoinPaymentStatus = quoteValid ? "awaiting" : "soft_expired";
                if(quoteValid) {
                    buildCurrentAction = this._buildDepositPsbtAction.bind(this);
                }
                break;
            }
            case SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED:
            case SpvFromBTCSwapState.SIGNED:
            case SpvFromBTCSwapState.POSTED:
            case SpvFromBTCSwapState.BROADCASTED:
            case SpvFromBTCSwapState.FRONTED: {
                const bitcoinPayment = await this.getBitcoinPayment();
                let bitcoinConfirmationDelay = -1;
                let knownBitcoinPaymentStatus: "received" | "confirmed" | undefined;
                if(bitcoinPayment!=null) {
                    if(bitcoinPayment.confirmations >= bitcoinPayment.targetConfirmations) {
                        knownBitcoinPaymentStatus = "confirmed";
                    } else {
                        const result = await this.wrapper._btcRpc.getConfirmationDelay(
                            bitcoinPayment.btcTx,
                            bitcoinPayment.targetConfirmations
                        );
                        confirmations = {
                            current: bitcoinPayment.confirmations,
                            target: bitcoinPayment.targetConfirmations,
                            etaSeconds: result ?? -1
                        };
                        knownBitcoinPaymentStatus = "received";
                        bitcoinConfirmationDelay = result ?? -1;
                    }
                }
                if(state===SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED)
                    bitcoinPaymentStatus = knownBitcoinPaymentStatus ?? "soft_expired";
                if(state===SpvFromBTCSwapState.POSTED || state===SpvFromBTCSwapState.SIGNED)
                    bitcoinPaymentStatus = knownBitcoinPaymentStatus ?? "awaiting";
                if(state===SpvFromBTCSwapState.BROADCASTED)
                    bitcoinPaymentStatus = knownBitcoinPaymentStatus ?? "received";
                destinationSettlementStatus = "inactive";

                if(state===SpvFromBTCSwapState.FRONTED) {
                    bitcoinPaymentStatus = knownBitcoinPaymentStatus ?? "received";
                    destinationSettlementStatus = "settled";
                }

                if(
                    state===SpvFromBTCSwapState.SIGNED ||
                    state===SpvFromBTCSwapState.POSTED ||
                    state===SpvFromBTCSwapState.BROADCASTED
                ) {
                    buildCurrentAction = this._buildWaitBitcoinConfirmationsAction.bind(this, bitcoinConfirmationDelay);
                }
                break;
            }
            case SpvFromBTCSwapState.BTC_TX_CONFIRMED:
                bitcoinPaymentStatus = "confirmed";
                if(
                    this.btcTxConfirmedAt==null ||
                    options?.maxWaitTillAutomaticSettlementSeconds===0 ||
                    (now - this.btcTxConfirmedAt) > (options?.maxWaitTillAutomaticSettlementSeconds ?? 60)*1000
                ) {
                    destinationSettlementStatus = "awaiting_manual";
                    buildCurrentAction = this._buildClaimSmartChainTxAction.bind(this);
                } else {
                    destinationSettlementStatus = "awaiting_automatic";
                    buildCurrentAction = this._buildWaitSettlementAction.bind(this, options?.maxWaitTillAutomaticSettlementSeconds);
                }
                break;
            case SpvFromBTCSwapState.CLAIMED:
                bitcoinPaymentStatus = "confirmed";
                destinationSettlementStatus = "settled";
                break;
            case SpvFromBTCSwapState.CLOSED:
                bitcoinPaymentStatus = "confirmed";
                destinationSettlementStatus = "expired";
                break;
        }

        if(bitcoinPaymentStatus==="confirmed") {
            confirmations = {
                current: this.getRequiredConfirmationsCount(),
                target: this.getRequiredConfirmationsCount(),
                etaSeconds: 0
            };
        }

        return {
            steps: [
                {
                    type: "Payment",
                    side: "source",
                    chain: "BITCOIN",
                    title: "Bitcoin payment",
                    description: "Sign and submit the Bitcoin swap PSBT, then wait for the bitcoin transaction to confirm",
                    status: bitcoinPaymentStatus,
                    confirmations,
                    initTxId: this._data?.btcTx?.txid,
                    settleTxId: bitcoinPaymentStatus==="confirmed" ? this._data?.btcTx?.txid : undefined
                },
                {
                    type: "Settlement",
                    side: "destination",
                    chain: this.chainIdentifier,
                    title: "Destination settlement",
                    description: `Wait for automatic settlement on the ${this.chainIdentifier} side, or settle manually if it takes too long`,
                    status: destinationSettlementStatus,
                    initTxId: this._frontTxId ?? this._claimTxId,
                    settleTxId: this._frontTxId ?? this._claimTxId
                }
            ] as [
                SwapExecutionStepPayment<"BITCOIN">,
                SwapExecutionStepSettlement<T["ChainId"], "awaiting_automatic" | "awaiting_manual">
            ],
            buildCurrentAction,
            state
        };
    }

    /**
     * @inheritDoc
     * @internal
     */
    async _submitExecutionTransactions(txs: (T["SignedTXType"] | Transaction | string)[], abortSignal?: AbortSignal, requiredStates?: SpvFromBTCSwapState[], idempotent?: boolean): Promise<string[]> {
        if(txs.length===0) throw new Error("Need to submit at least 1 transaction in the array, submitted empty array of transactions!");

        // Handle idempotent calls
        if(idempotent) {
            let idempotencyTriggered = false;
            const txIds: string[] = [];
            for(let tx of txs) {
                let parsedTx: T["SignedTXType"] | Transaction | undefined;
                if(typeof(tx)==="string") {
                    try {
                        parsedTx = await this.wrapper._chain.deserializeSignedTx(tx);
                    } catch (e) {}
                    try {
                        parsedTx = parsePsbtTransaction(tx);
                    } catch (e) {}
                } else {
                    parsedTx = tx;
                }

                if(parsedTx==null) {
                    this.logger.debug("_submitExecutionTransactions(): Failed to parse provided execution transaction: ", tx);
                    continue;
                }

                if(parsedTx instanceof Transaction) {
                    // Bitcoin tx
                    const btcTx = await this.wrapper._btcRpc.parseTransaction(Buffer.from(parsedTx.toBytes(true)).toString("hex"));
                    if(btcTx.txid===this._data?.getTxId()) {
                        if(this._state!==SpvFromBTCSwapState.SIGNED && this._state!==SpvFromBTCSwapState.DECLINED)
                            idempotencyTriggered = true;
                    }
                    txIds.push(btcTx.txid);
                } else {
                    // SC tx
                    if(this.wrapper._chain.getTxId!=null) {
                        const txId = await this.wrapper._chain.getTxId(parsedTx);
                        if(this._claimTxId===txId) idempotencyTriggered = true;
                        txIds.push(txId);
                    }
                }
            }
            if(idempotencyTriggered) return txIds;
        }

        if(requiredStates!=null && !requiredStates.includes(this._state)) throw new Error("Swap state has changed before transactions were submitted!");

        if(this._state===SpvFromBTCSwapState.CREATED || this._state===SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED) {
            let psbt: string | Transaction;
            if(txs.length!==1) throw new Error("Need to submit exactly 1 signed PSBT!");
            if(typeof(txs[0])!=="string" && !(txs[0] instanceof Transaction))
                throw new Error("Must submit a valid PSBT as hex/base64 string or `@scure/btc-signer` Transaction object!");
            psbt = txs[0];
            return [await this.submitPsbt(psbt)];
        }

        if(this._state===SpvFromBTCSwapState.BTC_TX_CONFIRMED) {
            const parsedTxs: T["SignedTXType"][] = [];
            for(let tx of txs) {
                parsedTxs.push(typeof(tx)==="string" ? await this.wrapper._chain.deserializeSignedTx(tx) : tx);
            }
            const txIds = await this.wrapper._chain.sendSignedAndConfirm(parsedTxs, true, abortSignal, false);
            await this.waitTillClaimed(undefined, abortSignal);
            return txIds;
        }

        throw new Error("Invalid swap state for transaction submission!");
    }

    /**
     * @internal
     */
    private async _buildDepositPsbtAction(actionOptions?: {
        bitcoinFeeRate?: number,
        bitcoinWallet?: MinimalBitcoinWalletInterface
    }): Promise<SwapExecutionActionSignPSBT> {
        return {
            type: "SignPSBT",
            name: "Deposit on Bitcoin",
            description: "Send funds to the bitcoin swap address",
            chain: "BITCOIN",
            txs: [
                actionOptions?.bitcoinWallet==null
                    ? {...await this.getPsbt(), type: "RAW_PSBT"}
                    : {...await this.getFundedPsbt(actionOptions.bitcoinWallet, actionOptions?.bitcoinFeeRate), type: "FUNDED_PSBT"}
            ],
            submitPsbt: async (signedPsbt: string | Transaction | (string | Transaction)[], idempotent) => {
                return this._submitExecutionTransactions(
                    Array.isArray(signedPsbt) ? signedPsbt : [signedPsbt],
                    undefined,
                    [SpvFromBTCSwapState.CREATED, SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED],
                    idempotent
                );
            }
        } as SwapExecutionActionSignPSBT;
    }

    /**
     * @internal
     */
    private async _buildWaitBitcoinConfirmationsAction(confirmationDelay: number): Promise<SwapExecutionActionWait<"BITCOIN_CONFS">> {
        return {
            type: "Wait",
            name: "Bitcoin confirmations",
            description: "Wait for bitcoin transaction to confirm",
            pollTimeSeconds: 10,
            expectedTimeSeconds: confirmationDelay===-1 ? -1 : Math.floor(confirmationDelay/1000),
            wait: async (
                maxWaitTimeSeconds?: number, pollIntervalSeconds?: number, abortSignal?: AbortSignal,
                btcConfirmationsCallback?: (txId?: string, confirmations?: number, targetConfirmations?: number, txEtaMs?: number) => void
            ) => {
                const abortController = extendAbortController(
                    abortSignal, maxWaitTimeSeconds, "Timed out waiting for bitcoin transaction"
                );
                await this.waitForBitcoinTransaction(btcConfirmationsCallback, pollIntervalSeconds, abortController.signal);
            }
        } as SwapExecutionActionWait<"BITCOIN_CONFS">;
    }

    /**
     * @internal
     */
    private async _buildWaitSettlementAction(maxWaitTillAutomaticSettlementSeconds?: number): Promise<SwapExecutionActionWait<"SETTLEMENT">> {
        return {
            type: "Wait",
            name: "Automatic settlement",
            description: "Wait for bitcoin transaction to confirm",
            pollTimeSeconds: 5,
            expectedTimeSeconds: 10,
            wait: async (
                maxWaitTimeSeconds?: number, pollIntervalSeconds?: number, abortSignal?: AbortSignal
            ) => {
                await this.waitTillClaimedOrFronted(maxWaitTimeSeconds ?? maxWaitTillAutomaticSettlementSeconds ?? 60, abortSignal, pollIntervalSeconds);
            }
        } as SwapExecutionActionWait<"SETTLEMENT">;
    }

    /**
     * @internal
     */
    private async _buildClaimSmartChainTxAction(actionOptions?: {
        manualSettlementSmartChainSigner?: string | T["Signer"] | T["NativeSigner"]
    }): Promise<SwapExecutionActionSignSmartChainTx<T>> {
        const signerAddress =
            await this.wrapper._getSignerAddress(actionOptions?.manualSettlementSmartChainSigner);

        return {
            type: "SignSmartChainTransaction",
            name: "Settle manually",
            description: "Manually settle the swap on the destination smart chain",
            chain: this.chainIdentifier,
            txs: await this.prepareTransactions(this.txsClaim(actionOptions?.manualSettlementSmartChainSigner)),
            submitTransactions: async (txs: (T["SignedTXType"] | string)[], abortSignal?: AbortSignal, idempotent?: boolean) => {
                return this._submitExecutionTransactions(
                    txs,
                    abortSignal,
                    [SpvFromBTCSwapState.BTC_TX_CONFIRMED],
                    idempotent
                );
            },
            requiredSigner: signerAddress ?? this._getInitiator()
        } as SwapExecutionActionSignSmartChainTx<T>;
    }

    /**
     * @inheritDoc
     *
     * @param options.bitcoinFeeRate Optional fee rate to use for the created Bitcoin transaction
     * @param options.bitcoinWallet Optional bitcoin wallet address specification to return a funded PSBT,
     *  if not provided a raw PSBT is returned instead which necessitates the implementor to manually add
     *  inputs to the bitcoin transaction and **set the nSequence field of the 2nd input** (input 1 -
     *  indexing from 0) to the value returned in `in1sequence`
     * @param options.manualSettlementSmartChainSigner Optional smart chain signer to create a manual claim (settlement) transaction
     * @param options.maxWaitTillAutomaticSettlementSeconds Maximum time to wait for an automatic settlement after
     *  the bitcoin transaction is confirmed (defaults to 60 seconds)
     */
    async getExecutionAction(options?: {
        bitcoinFeeRate?: number,
        bitcoinWallet?: MinimalBitcoinWalletInterface,
        manualSettlementSmartChainSigner?: string | T["Signer"] | T["NativeSigner"],
        maxWaitTillAutomaticSettlementSeconds?: number
    }): Promise<
        SwapExecutionActionSignPSBT |
        SwapExecutionActionWait<"BITCOIN_CONFS" | "SETTLEMENT"> |
        SwapExecutionActionSignSmartChainTx<T> |
        undefined
    > {
        const executionStatus = await this._getExecutionStatus(options);
        return executionStatus.buildCurrentAction(options);
    }

    /**
     * @inheritDoc
     */
    async getExecutionStatus(options?: {
        skipBuildingAction?: boolean,
        bitcoinFeeRate?: number,
        bitcoinWallet?: MinimalBitcoinWalletInterface,
        manualSettlementSmartChainSigner?: string | T["Signer"] | T["NativeSigner"],
        maxWaitTillAutomaticSettlementSeconds?: number
    }): Promise<{
        steps: [
            SwapExecutionStepPayment<"BITCOIN">,
            SwapExecutionStepSettlement<T["ChainId"], "awaiting_automatic" | "awaiting_manual">
        ],
        currentAction:
            SwapExecutionActionSignPSBT |
            SwapExecutionActionWait<"BITCOIN_CONFS" | "SETTLEMENT"> |
            SwapExecutionActionSignSmartChainTx<T> |
            undefined,
        stateInfo: SwapStateInfo<SpvFromBTCSwapState>
    }> {
        const executionStatus = await this._getExecutionStatus(options);
        return {
            steps: executionStatus.steps,
            currentAction: options?.skipBuildingAction ? undefined : await executionStatus.buildCurrentAction(options),
            stateInfo: this._getStateInfo(executionStatus.state)
        };
    }

    /**
     * @inheritDoc
     */
    async getExecutionSteps(options?: {
        maxWaitTillAutomaticSettlementSeconds?: number
    }): Promise<[
        SwapExecutionStepPayment<"BITCOIN">,
        SwapExecutionStepSettlement<T["ChainId"], "awaiting_automatic" | "awaiting_manual">
    ]> {
        return (await this._getExecutionStatus(options)).steps;
    }


    //////////////////////////////
    //// Bitcoin tx listener

    /**
     * Checks whether a bitcoin payment was already made, returns the payment or null when no payment has been made.
     * @internal
     */
    protected async getBitcoinPayment(): Promise<{
        txId: string,
        confirmations: number,
        targetConfirmations: number,
        btcTx: BtcTx,
        inputAddresses?: string[]
    } | null> {
        if(this._data?.btcTx?.txid==null) return null;

        const result = await this.wrapper._btcRpc.getTransaction(this._data?.btcTx?.txid);
        if(result==null) return null;

        return {
            txId: result.txid,
            confirmations: result.confirmations ?? 0,
            targetConfirmations: this.vaultRequiredConfirmations,
            btcTx: result,
            inputAddresses: result.inputAddresses
        }
    }

    /**
     * @inheritDoc
     *
     * @throws {Error} if in invalid state (must be {@link SpvFromBTCSwapState.POSTED} or
     *  {@link SpvFromBTCSwapState.BROADCASTED} states)
     */
    async waitForBitcoinTransaction(
        updateCallback?: (txId?: string, confirmations?: number, targetConfirmations?: number, txEtaMs?: number) => void,
        checkIntervalSeconds?: number,
        abortSignal?: AbortSignal
    ): Promise<string> {
        if(
            this._state!==SpvFromBTCSwapState.POSTED &&
            this._state!==SpvFromBTCSwapState.BROADCASTED &&
            !(this._state===SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED && this.posted)
        ) throw new Error("Must be in POSTED or BROADCASTED state!");
        if(this._data==null) throw new Error("Expected swap to have withdrawal data filled!");

        const result = await this.wrapper._btcRpc.waitForTransaction(
            this._data.btcTx.txid,
            this.vaultRequiredConfirmations,
            (btcTx?: BtcTxWithBlockheight, txEtaMs?: number) => {
                if(updateCallback!=null) updateCallback(btcTx?.txid, btcTx?.confirmations, this.vaultRequiredConfirmations, txEtaMs);
                if(btcTx==null) return;
                let save = false;
                if(btcTx.inputAddresses!=null && this._senderAddress==null) {
                    this._senderAddress = btcTx.inputAddresses[1];
                    save = true;
                }
                if(this._state===SpvFromBTCSwapState.POSTED || this._state==SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED) {
                    this._state = SpvFromBTCSwapState.BROADCASTED;
                    save = true;
                }
                if(save) this._saveAndEmit();
            },
            abortSignal,
            checkIntervalSeconds
        );

        if(abortSignal!=null) abortSignal.throwIfAborted();

        let save = false;
        if(result.inputAddresses!=null && this._senderAddress==null) {
            this._senderAddress = result.inputAddresses[1];
            save = true;
        }
        if(
            (this._state as SpvFromBTCSwapState)!==SpvFromBTCSwapState.FRONTED &&
            (this._state as SpvFromBTCSwapState)!==SpvFromBTCSwapState.CLAIMED
        ) {
            this.btcTxConfirmedAt ??= Date.now();
            this._state = SpvFromBTCSwapState.BTC_TX_CONFIRMED;
            save = true;
        }
        if(save) await this._saveAndEmit();

        return result.txid;
    }


    //////////////////////////////
    //// Claim

    /**
     * Returns transactions for settling (claiming) the swap if the swap requires manual settlement, you can check so
     *  with isClaimable. After sending the transaction manually be sure to call the waitTillClaimed function to wait
     *  till the claim transaction is observed, processed by the SDK and state of the swap properly updated.
     *
     * @remarks
     * Might also return transactions necessary to sync the bitcoin light client.
     *
     * @param _signer Address of the signer to create the claim transactions for, can also be different to the recipient
     *
     * @throws {Error} If the swap is in invalid state (must be {@link SpvFromBTCSwapState.BTC_TX_CONFIRMED})
     */
    async txsClaim(_signer?: string | T["Signer"] | T["NativeSigner"]): Promise<T["TX"][]> {
        let address: string | undefined = undefined;
        if(_signer!=null) {
            if (typeof (_signer) === "string") {
                address = _signer;
            } else if (isAbstractSigner(_signer)) {
                address = _signer.getAddress();
            } else {
                address = (await this.wrapper._chain.wrapSigner(_signer)).getAddress();
            }
        }

        if(!this.isClaimable()) throw new Error("Must be in BTC_TX_CONFIRMED state!");
        if(this._data==null) throw new Error("Expected swap to have withdrawal data filled!");

        const vaultData = await this._contract.getVaultData(this.vaultOwner, this.vaultId);
        if(vaultData==null) throw new Error(`Vault data for ${this.vaultOwner}:${this.vaultId.toString(10)} not found (already closed???)!`);

        const btcTx = await this.wrapper._btcRpc.getTransaction(this._data.btcTx.txid);
        if(btcTx==null) throw new Error(`Bitcoin transaction ${this._data.btcTx.txid} not found!`);
        const txs = [btcTx];

        //Trace back from current tx to the vaultData-specified UTXO
        const vaultUtxo = vaultData.getUtxo();
        while(txs[0].ins[0].txid+":"+txs[0].ins[0].vout!==vaultUtxo) {
            const btcTx = await this.wrapper._btcRpc.getTransaction(txs[0].ins[0].txid);
            if(btcTx==null) throw new Error(`Prior withdrawal bitcoin transaction ${this._data.btcTx.txid} not found!`);
            txs.unshift(btcTx);
        }

        //Parse transactions to withdrawal data
        const withdrawalData: T["SpvVaultWithdrawalData"][] = [];
        for(let tx of txs) {
            withdrawalData.push(await this._contract.getWithdrawalData(tx));
        }

        return await this._contract.txsClaim(
            address ?? this._getInitiator(), vaultData,
            withdrawalData.map(tx => {return {tx}}),
            this.wrapper._synchronizer(this._contractVersion), true
        );
    }

    /**
     * Settles the swap by claiming the funds on the destination chain if the swap requires manual settlement, you can
     *  check so with isClaimable.
     *
     * @remarks
     * Might also sync the bitcoin light client during the process.
     *
     * @param _signer Signer to use for signing the settlement transactions, can also be different to the recipient
     * @param abortSignal Abort signal
     * @param onBeforeTxSent Optional callback triggered before the claim transaction is broadcasted
     *
     * @throws {Error} If the swap is in invalid state (must be {@link SpvFromBTCSwapState.BTC_TX_CONFIRMED})
     */
    async claim(_signer: T["Signer"] | T["NativeSigner"], abortSignal?: AbortSignal, onBeforeTxSent?: (txId: string) => void): Promise<string> {
        const signer = isAbstractSigner(_signer) ? _signer : await this.wrapper._chain.wrapSigner(_signer);
        let txIds: string[];
        try {
            let txCount = 0;
            const txs = await this.txsClaim(signer);
            txIds = await this.wrapper._chain.sendAndConfirm(
                signer, txs, true, abortSignal, undefined, (txId: string) => {
                    txCount++;
                    if(onBeforeTxSent!=null && txCount===txs.length) onBeforeTxSent(txId);
                    return Promise.resolve();
                }
            );
        } catch (e) {
            if(this._data==null) throw e;

            this.logger.info("claim(): Failed to claim ourselves, checking swap claim state...");
            if(this._state===SpvFromBTCSwapState.CLAIMED) {
                this.logger.info("claim(): Transaction state is CLAIMED, swap was successfully claimed by the watchtower");
                return this._claimTxId!;
            }
            const withdrawalState = await this._contract.getWithdrawalState(this._data, this._genesisSmartChainBlockHeight);
            if(withdrawalState!=null && withdrawalState.type===SpvWithdrawalStateType.CLAIMED) {
                this.logger.info("claim(): Transaction status is CLAIMED, swap was successfully claimed by the watchtower");
                this._claimTxId = withdrawalState.txId;
                await this._saveAndEmit(SpvFromBTCSwapState.CLAIMED);
                return withdrawalState.txId;
            }
            throw e;
        }

        this._claimTxId = txIds[0];
        if(
            this._state===SpvFromBTCSwapState.POSTED || this._state===SpvFromBTCSwapState.BROADCASTED ||
            this._state===SpvFromBTCSwapState.BTC_TX_CONFIRMED || this._state===SpvFromBTCSwapState.FAILED ||
            this._state===SpvFromBTCSwapState.FRONTED
        ) {
            await this._saveAndEmit(SpvFromBTCSwapState.CLAIMED);
        }
        return txIds[0];
    }

    /**
     * Periodically checks the chain to see whether the swap was finished (claimed or refunded)
     *
     * @param interval How often to check (in seconds), default to 5s
     * @param abortSignal
     * @internal
     */
    protected async watchdogWaitTillResult(interval: number = 5, abortSignal?: AbortSignal): Promise<
        SpvWithdrawalClaimedState | SpvWithdrawalFrontedState | SpvWithdrawalClosedState
    > {
        if(this._data==null) throw new Error("Cannot await the result before the btc transaction is sent!");

        let status: SpvWithdrawalState = {type: SpvWithdrawalStateType.NOT_FOUND};
        while(status.type===SpvWithdrawalStateType.NOT_FOUND) {
            await timeoutPromise(interval*1000, abortSignal);
            try {
                //Be smart about checking withdrawal state
                if(await this._shouldCheckWithdrawalState()) {
                    status = await this._contract.getWithdrawalState(
                        this._data, this._genesisSmartChainBlockHeight
                    ) ?? {type: SpvWithdrawalStateType.NOT_FOUND};
                }
            } catch (e) {
                this.logger.error("watchdogWaitTillResult(): Error when fetching commit status: ", e);
            }
        }
        if(abortSignal!=null) abortSignal.throwIfAborted();
        return status;
    }

    /**
     * Waits till the swap is successfully settled (claimed), should be called after sending the claim (settlement)
     *  transactions manually to wait till the SDK processes the settlement and updates the swap state accordingly.
     *
     * @remarks
     * This is an alias for the {@link waitTillClaimedOrFronted} function and will also resolve if the swap has
     *  been fronted (not necessarily claimed)
     *
     * @param maxWaitTimeSeconds – Maximum time in seconds to wait for the swap to be settled
     * @param abortSignal – AbortSignal
     *
     * @returns Whether the swap was claimed in time or not
     */
    waitTillClaimed(maxWaitTimeSeconds?: number, abortSignal?: AbortSignal): Promise<boolean> {
        return this.waitTillClaimedOrFronted(maxWaitTimeSeconds, abortSignal);
    }

    /**
     * Waits till the swap is successfully fronted or settled on the destination chain
     *
     * @param maxWaitTimeSeconds Maximum time in seconds to wait for the swap to be settled (by default
     *  it waits indefinitely)
     * @param abortSignal Abort signal
     * @param pollIntervalSeconds How often to poll via the watchdog
     *
     * @returns {boolean} whether the swap was claimed or fronted automatically or not, if the swap was not claimed
     *  the user can claim manually through the {@link claim} function
     */
    async waitTillClaimedOrFronted(maxWaitTimeSeconds?: number, abortSignal?: AbortSignal, pollIntervalSeconds?: number): Promise<boolean> {
        if(this._state===SpvFromBTCSwapState.CLAIMED || this._state===SpvFromBTCSwapState.FRONTED) return Promise.resolve(true);

        const abortController = extendAbortController(abortSignal);

        let timedOut: boolean = false;
        if(maxWaitTimeSeconds!=null) {
            const timeout = setTimeout(() => {
                timedOut = true;
                abortController.abort();
            }, maxWaitTimeSeconds * 1000);
            abortController.signal.addEventListener("abort", () => clearTimeout(timeout));
        }

        let res: number | SpvWithdrawalState;
        try {
            res = await Promise.race([
                this.watchdogWaitTillResult(pollIntervalSeconds, abortController.signal),
                this.waitTillState(SpvFromBTCSwapState.CLAIMED, "eq", abortController.signal).then(() => 0),
                this.waitTillState(SpvFromBTCSwapState.FRONTED, "eq", abortController.signal).then(() => 1),
                this.waitTillState(SpvFromBTCSwapState.FAILED, "eq", abortController.signal).then(() => 2),
            ]);
            abortController.abort();
        } catch (e) {
            abortController.abort();
            if(timedOut) return false;
            throw e;
        }

        if(typeof(res)==="number") {
            if(res===0) {
                this.logger.debug("waitTillClaimedOrFronted(): Resolved from state change (CLAIMED)");
                return true;
            }
            if(res===1) {
                this.logger.debug("waitTillClaimedOrFronted(): Resolved from state change (FRONTED)");
                return true;
            }
            if(res===2) {
                this.logger.debug("waitTillClaimedOrFronted(): Resolved from state change (FAILED)");
                throw new Error("Swap failed while waiting for claim or front");
            }
            throw new Error("Invalid numeric response, this should never happen!");
        }
        this.logger.debug("waitTillClaimedOrFronted(): Resolved from watchdog");

        if(res.type===SpvWithdrawalStateType.FRONTED) {
            if(
                (this._state as SpvFromBTCSwapState)!==SpvFromBTCSwapState.FRONTED ||
                (this._state as SpvFromBTCSwapState)!==SpvFromBTCSwapState.CLAIMED
            ) {
                this._frontTxId = res.txId;
                await this._saveAndEmit(SpvFromBTCSwapState.FRONTED);
            }
        }
        if(res.type===SpvWithdrawalStateType.CLAIMED) {
            if(
                (this._state as SpvFromBTCSwapState)!==SpvFromBTCSwapState.CLAIMED
            ) {
                this._claimTxId = res.txId;
                await this._saveAndEmit(SpvFromBTCSwapState.CLAIMED);
            }
        }
        if(res.type===SpvWithdrawalStateType.CLOSED) {
            if(
                (this._state as SpvFromBTCSwapState)!==SpvFromBTCSwapState.CLOSED
            ) await this._saveAndEmit(SpvFromBTCSwapState.CLOSED);
            throw new Error("Swap failed with catastrophic error!");
        }

        return true;
    }

    /**
     * Waits till the bitcoin transaction confirms and swap settled on the destination chain
     *
     * @param updateCallback Callback called when txId is found, and also called with subsequent confirmations
     * @param checkIntervalSeconds How often to check the bitcoin transaction (5 seconds by default)
     * @param abortSignal Abort signal
     *
     * @throws {Error} if in invalid state (must be {@link SpvFromBTCSwapState.POSTED} or
     *  {@link SpvFromBTCSwapState.BROADCASTED} states)
     */
    async waitTillExecuted(
        updateCallback?: (txId?: string, confirmations?: number, targetConfirmations?: number, txEtaMs?: number) => void,
        checkIntervalSeconds?: number,
        abortSignal?: AbortSignal
    ): Promise<void> {
        await this.waitForBitcoinTransaction(updateCallback, checkIntervalSeconds, abortSignal);
        await this.waitTillClaimedOrFronted(undefined, abortSignal);
    }


    //////////////////////////////
    //// Storage

    /**
     * @inheritDoc
     */
    serialize(): any {
        return {
            ...super.serialize(),
            quoteId: this.quoteId,
            recipient: this.recipient,
            vaultOwner: this.vaultOwner,
            vaultId: this.vaultId.toString(10),
            vaultRequiredConfirmations: this.vaultRequiredConfirmations,
            vaultTokenMultipliers: this.vaultTokenMultipliers.map(val => val.toString(10)),
            vaultBtcAddress: this.vaultBtcAddress,
            vaultUtxo: this.vaultUtxo,
            vaultUtxoValue: this.vaultUtxoValue.toString(10),
            btcDestinationAddress: this.btcDestinationAddress,
            btcAmount: this.btcAmount.toString(10),
            btcAmountSwap: this.btcAmountSwap.toString(10),
            btcAmountGas: this.btcAmountGas.toString(10),
            minimumBtcFeeRate: this.minimumBtcFeeRate,
            outputTotalSwap: this.outputTotalSwap.toString(10),
            outputSwapToken: this.outputSwapToken,
            outputTotalGas: this.outputTotalGas.toString(10),
            outputGasToken: this.outputGasToken,
            gasSwapFeeBtc: this.gasSwapFeeBtc.toString(10),
            gasSwapFee: this.gasSwapFee.toString(10),
            callerFeeShare: this.callerFeeShare.toString(10),
            frontingFeeShare: this.frontingFeeShare.toString(10),
            executionFeeShare: this.executionFeeShare.toString(10),
            genesisSmartChainBlockHeight: this._genesisSmartChainBlockHeight,
            gasPricingInfo: serializePriceInfoType(this.gasPricingInfo),
            posted: this.posted,

            senderAddress: this._senderAddress,
            claimTxId: this._claimTxId,
            frontTxId: this._frontTxId,
            data: this._data?.serialize(),
            btcTxConfirmedAt: this.btcTxConfirmedAt
        };
    }


    //////////////////////////////
    //// Swap ticks & sync

    /**
     * Used to set the txId of the bitcoin payment from the on-chain events listener
     *
     * @param txId
     * @internal
     */
    async _setBitcoinTxId(txId: string) {
        if(this._data==null) return;
        if(txId!=this._data.btcTx.txid) return;

        if(this._senderAddress!=null) return;
        const btcTx = await this.wrapper._btcRpc.getTransaction(txId);
        if(btcTx==null || btcTx.inputAddresses==null) return;

        this._senderAddress = btcTx.inputAddresses[1];
    }

    private btcTxLastChecked?: number;

    /**
     * @internal
     */
    async _syncStateFromBitcoin(save?: boolean) {
        if(this._data?.btcTx==null) return false;

        //Check if bitcoin payment was confirmed
        this.btcTxLastChecked = Date.now();
        const res = await this.getBitcoinPayment();
        if(res==null) {
            //Check inputs double-spent
            for(let input of this._data.btcTx.ins) {
                if(await this.wrapper._btcRpc.isSpent(input.txid+":"+input.vout, true)) {
                    if(
                        this._state===SpvFromBTCSwapState.SIGNED ||
                        this._state===SpvFromBTCSwapState.POSTED ||
                        this._state===SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED ||
                        this._state===SpvFromBTCSwapState.DECLINED
                    ) {
                        //One of the inputs was double-spent
                        this._state = SpvFromBTCSwapState.QUOTE_EXPIRED;
                    } else {
                        //One of the inputs was double-spent
                        this._state = SpvFromBTCSwapState.FAILED;
                    }
                    if(save) await this._saveAndEmit();
                    return true;
                }
            }
        } else {
            let needsSave = false;
            if(res.inputAddresses!=null && this._senderAddress==null) {
                this._senderAddress = res.inputAddresses[1];
                needsSave = true;
            }
            if(res.confirmations>=this.vaultRequiredConfirmations) {
                if(
                    this._state!==SpvFromBTCSwapState.BTC_TX_CONFIRMED &&
                    this._state!==SpvFromBTCSwapState.FRONTED &&
                    this._state!==SpvFromBTCSwapState.CLAIMED
                ) {
                    this.btcTxConfirmedAt ??= Date.now();
                    this._state = SpvFromBTCSwapState.BTC_TX_CONFIRMED;
                    needsSave = true;
                }
            } else if(
                this._state===SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED ||
                this._state===SpvFromBTCSwapState.POSTED ||
                this._state===SpvFromBTCSwapState.SIGNED ||
                this._state===SpvFromBTCSwapState.DECLINED
            ) {
                this._state = SpvFromBTCSwapState.BROADCASTED;
                needsSave = true;
            }
            if(needsSave && save) await this._saveAndEmit();
            return needsSave;
        }
        return false;
    }

    /**
     * Checks the swap's state on-chain and compares it to its internal state, updates/changes it according to on-chain
     *  data
     */
    private async syncStateFromChain(): Promise<boolean> {
        let changed: boolean = false;

        if(
            this._state===SpvFromBTCSwapState.SIGNED ||
            this._state===SpvFromBTCSwapState.POSTED ||
            this._state===SpvFromBTCSwapState.BROADCASTED ||
            this._state===SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED ||
            this._state===SpvFromBTCSwapState.DECLINED ||
            this._state===SpvFromBTCSwapState.BTC_TX_CONFIRMED
        ) {
            //Check BTC transaction
            if(await this._syncStateFromBitcoin(false)) changed ||= true;
        }

        if(this._state===SpvFromBTCSwapState.BROADCASTED || this._state===SpvFromBTCSwapState.BTC_TX_CONFIRMED) {
            if(await this._shouldCheckWithdrawalState()) {
                const status = await this._contract.getWithdrawalState(this._data!, this._genesisSmartChainBlockHeight);
                this.logger.debug("syncStateFromChain(): status of "+this._data!.btcTx.txid, status);
                switch(status?.type) {
                    case SpvWithdrawalStateType.FRONTED:
                        this._frontTxId = status.txId;
                        this._state = SpvFromBTCSwapState.FRONTED;
                        changed ||= true;
                        break;
                    case SpvWithdrawalStateType.CLAIMED:
                        this._claimTxId = status.txId;
                        this._state = SpvFromBTCSwapState.CLAIMED;
                        changed ||= true;
                        break;
                    case SpvWithdrawalStateType.CLOSED:
                        this._state = SpvFromBTCSwapState.CLOSED;
                        changed ||= true;
                        break;
                }
            }
        }

        if(
            this._state===SpvFromBTCSwapState.CREATED ||
            this._state===SpvFromBTCSwapState.SIGNED ||
            this._state===SpvFromBTCSwapState.POSTED
        ) {
            if(this.expiry<Date.now()) {
                if(this._state===SpvFromBTCSwapState.CREATED) {
                    this._state = SpvFromBTCSwapState.QUOTE_EXPIRED;
                } else {
                    this._state = SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED;
                }
                changed ||= true;
            }
        }

        return changed;
    }

    /**
     * @inheritDoc
     * @internal
     */
    async _sync(save?: boolean): Promise<boolean> {
        const changed = await this.syncStateFromChain();
        if(changed && save) await this._saveAndEmit();
        return changed;
    }

    /**
     * @inheritDoc
     * @internal
     */
    async _tick(save?: boolean): Promise<boolean> {
        if(
            this._state===SpvFromBTCSwapState.CREATED ||
            this._state===SpvFromBTCSwapState.SIGNED
        ) {
            if(this.getQuoteExpiry()<Date.now()) {
                this._state = SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED;
                if(save) await this._saveAndEmit();
                return true;
            }
        }

        if(this._state===SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED && !this.posted) {
            if(this.expiry<Date.now()) {
                this._state = SpvFromBTCSwapState.QUOTE_EXPIRED;
                if(save) await this._saveAndEmit();
                return true;
            }
        }

        if(this.btcTxLastChecked==null || Date.now() - this.btcTxLastChecked > 120_000) {
            if (
                this._state === SpvFromBTCSwapState.POSTED ||
                this._state === SpvFromBTCSwapState.BROADCASTED
            ) {
                try {
                    //Check if bitcoin payment was confirmed
                    return await this._syncStateFromBitcoin(save);
                } catch (e) {
                    this.logger.error("tickSwap("+this.getId()+"): ", e);
                }
            }
        }

        return false;
    }

    /**
     * Checks whether an on-chain withdrawal state should be fetched for this specific swap
     *
     * @internal
     */
    async _shouldCheckWithdrawalState(frontingAddress?: string | null, vaultDataUtxo?: string | null) {
        if(frontingAddress===undefined) frontingAddress = await this._contract.getFronterAddress(this.vaultOwner, this.vaultId, this._data!);
        if(vaultDataUtxo===undefined) vaultDataUtxo = await this._contract.getVaultLatestUtxo(this.vaultOwner, this.vaultId);

        if(frontingAddress != null) return true; //In case the swap is fronted there will for sure be a fronted event
        if(vaultDataUtxo == null) return true; //Vault UTXO is null (the vault closed)

        const [txId, _] = vaultDataUtxo.split(":");
        //Don't check both txns if their txId is equal
        if(this._data!.btcTx.txid===txId) return true;
        const [btcTx, latestVaultTx] = await Promise.all([
            this.wrapper._btcRpc.getTransaction(this._data!.btcTx.txid),
            this.wrapper._btcRpc.getTransaction(txId)
        ]);

        if(latestVaultTx==null || latestVaultTx.blockheight==null) {
            //Something must've gone horribly wrong, the latest vault utxo tx of the vault either
            // cannot be found on bitcoin network or is not even confirmed yet
            this.logger.debug(`_shouldCheckWithdrawalState(): Latest vault utxo not found or not confirmed on bitcoin ${txId}`);
            return false;
        }

        if(btcTx!=null) {
            const btcTxHeight = btcTx.blockheight;
            const latestVaultTxHeight = latestVaultTx.blockheight;
            //We also need to cover the case where bitcoin tx isn't confirmed yet (hence btxTxHeight==null)
            if(btcTxHeight==null || latestVaultTxHeight < btcTxHeight) {
                //Definitely not claimed!
                this.logger.debug(`_shouldCheckWithdrawalState(): Skipped checking withdrawal state, latestVaultTxHeight: ${latestVaultTx.blockheight}, btcTxHeight: ${btcTxHeight} and not fronted!`);
                return false;
            }
        } else {
            //Definitely not claimed because the transaction was probably double-spent (or evicted from mempool)
            this.logger.debug(`_shouldCheckWithdrawalState(): Skipped checking withdrawal state, btc tx probably replaced or evicted: ${this._data!.btcTx.txid} and not fronted`);
            return false;
        }

        return true;
    }

}
