import {MultiChain, Swapper} from "../swapper/Swapper";
import {ApiEndpoint, createApiEndpoint, toApiAmount, toApiLNURL, toApiToken} from "./ApiTypes";
import {ISwap} from "../swaps/ISwap";
import {serializeAction} from "./SerializedAction";
import {FeeType} from "../enums/FeeType";
import {SwapSide} from "../enums/SwapSide";
import {SwapType} from "../enums/SwapType";
import {MinimalBitcoinWalletInterface} from "../types/wallets/MinimalBitcoinWalletInterface";
import {FromBTCLNSwap, FromBTCLNSwapState} from "../swaps/escrow_swaps/frombtc/ln/FromBTCLNSwap";
import {FromBTCLNAutoSwap, FromBTCLNAutoSwapState} from "../swaps/escrow_swaps/frombtc/ln_auto/FromBTCLNAutoSwap";
import {
    CreateSwapInput,
    CreateSwapOutput,
    GetSpendableBalanceInput,
    GetSpendableBalanceOutput,
    GetSupportedTokensInput,
    GetSupportedTokensOutput,
    GetSwapCounterTokensInput,
    GetSwapCounterTokensOutput,
    GetSwapLimitsInput,
    GetSwapLimitsOutput,
    GetSwapStatusInput,
    GetSwapStatusOutput,
    ListPendingSwapsInput,
    ListPendingSwapsOutput,
    ListSwapOutput,
    ListSwapsInput,
    ListSwapsOutput,
    ParseAddressInput,
    ParseAddressOutput,
    SettleWithLnurlInput,
    SettleWithLnurlOutput,
    SubmitTransactionInput,
    SubmitTransactionOutput,
    SwapOutputBase
} from "./ApiEndpoints";
import {SwapExecutionStep} from "../types/SwapExecutionStep";
import {SwapStateInfo} from "../types/SwapStateInfo";
import {IEscrowSwap} from "../swaps/escrow_swaps/IEscrowSwap";
import {ToBTCLNSwap} from "../swaps/escrow_swaps/tobtc/ln/ToBTCLNSwap";
import {isSwapType} from "../utils/SwapUtils";

function requiresSecretRevealForApi(swap: ISwap, state: number): boolean | undefined {
    if(swap instanceof FromBTCLNSwap) {
        if(swap.hasSecretPreimage()) return false;
        return state===FromBTCLNSwapState.PR_PAID || state===FromBTCLNSwapState.CLAIM_COMMITED;
    }
    if(swap instanceof FromBTCLNAutoSwap) {
        if(swap.hasSecretPreimage()) return false;
        return state===FromBTCLNAutoSwapState.CLAIM_COMMITED;
    }
}

function createSwapOutputBase(
    swap: ISwap,
    steps: SwapExecutionStep[],
    stateInfo: SwapStateInfo<number>
): SwapOutputBase {
    const input = swap.getInput();
    const output = swap.getOutput();
    const feeBreakdown = swap.getFeeBreakdown();

    // Build fees from breakdown
    const swapFeeEntry = feeBreakdown.find(f => f.type === FeeType.SWAP);
    const networkFeeEntry = feeBreakdown.find(f => f.type === FeeType.NETWORK_OUTPUT);

    const flags: any = {};

    if(isSwapType(swap, SwapType.TO_BTCLN)) {
        flags.lightningRecipientIsNonCustodialWallet = swap.isPayingToNonCustodialWallet();
        flags.lightningPaymentWillLikelyFail = swap.willLikelyFail();
        flags.lightningPaymentWithLongHTLCExpiration = swap.hasLongExpiration();
    }

    return {
        swapId: swap.getId(),
        swapType: SwapType[swap.getType()],

        state: {
            number: stateInfo.state,
            name: stateInfo.name,
            description: stateInfo.description
        },

        quote: {
            inputAmount: toApiAmount(input),
            outputAmount: toApiAmount(output),
            fees: {
                swap: swapFeeEntry
                    ? toApiAmount(swapFeeEntry.fee.amountInSrcToken)
                    : { amount: "0", rawAmount: "0", decimals: 0, symbol: "", chain: "" },
                ...(networkFeeEntry ? {
                    networkOutput: toApiAmount(networkFeeEntry.fee.amountInSrcToken)
                } : {})
            },
            expiry: swap.getQuoteExpiry(),
            outputAddress: swap.getOutputAddress()!
        },

        flags,

        createdAt: swap.createdAt,

        steps,

        ...(swap instanceof ToBTCLNSwap && swap.isLNURL() ? {
            lnurl: {
                pay: swap.getLNURL()!,
                successAction: swap.getSuccessAction() ?? undefined
            }
        } : (swap instanceof FromBTCLNSwap || swap instanceof FromBTCLNAutoSwap) && swap.isLNURL() ? {
            lnurl: {
                withdraw: swap.getLNURL()!,
            }
        } : {})
    };
}

function createListSwapOutput(
    swap: ISwap,
    steps: SwapExecutionStep[],
    stateInfo: SwapStateInfo<number>
): ListSwapOutput {
    return {
        ...createSwapOutputBase(swap, steps, stateInfo),

        isFinished: swap.isFinished(),
        isSuccess: swap.isSuccessful(),
        isFailed: swap.isFailed(),
        isExpired: swap.isQuoteExpired()
    };
}

function parseSwapSide(side: "INPUT" | "OUTPUT"): SwapSide {
    return side === "INPUT" ? SwapSide.INPUT : SwapSide.OUTPUT;
}

export type SwapperApiConfig = {
    syncOnGetStatus?: boolean,
    idempotentTxSubmission?: boolean
};

export class SwapperApi<T extends MultiChain> {

    readonly endpoints: {
        createSwap: ApiEndpoint<CreateSwapInput, CreateSwapOutput, "POST">;
        listSwaps: ApiEndpoint<ListSwapsInput, ListSwapsOutput, "GET">;
        listPendingSwaps: ApiEndpoint<ListPendingSwapsInput, ListPendingSwapsOutput, "GET">;
        getSupportedTokens: ApiEndpoint<GetSupportedTokensInput, GetSupportedTokensOutput, "GET">;
        getSwapCounterTokens: ApiEndpoint<GetSwapCounterTokensInput, GetSwapCounterTokensOutput, "GET">;
        getSwapLimits: ApiEndpoint<GetSwapLimitsInput, GetSwapLimitsOutput, "GET">;
        parseAddress: ApiEndpoint<ParseAddressInput, ParseAddressOutput, "GET">;
        getSpendableBalance: ApiEndpoint<GetSpendableBalanceInput, GetSpendableBalanceOutput, "GET">;
        getSwapStatus: ApiEndpoint<GetSwapStatusInput, GetSwapStatusOutput, "GET">;
        submitTransaction: ApiEndpoint<SubmitTransactionInput, SubmitTransactionOutput, "POST">;
        settleWithLnurl: ApiEndpoint<SettleWithLnurlInput, SettleWithLnurlOutput, "POST">;
    };

    constructor(private swapper: Swapper<T>, private readonly config?: SwapperApiConfig) {
        this.config ??= {};
        this.config.syncOnGetStatus ??= true;
        this.endpoints = {
            createSwap: createApiEndpoint<CreateSwapInput, CreateSwapOutput, "POST">("POST", "Create a new cross-chain atomic swap. Returns a swap object with swapId, swapType, state, createdAt, quote (with inputAmount and outputAmount as ApiAmount objects each having amount/rawAmount/decimals/symbol/chain, a fees breakdown {swap, networkOutput?} of ApiAmount values, expiry, and outputAddress), a steps array, and optional lnurl. After creation, poll getSwapStatus periodically to get the next required action.", this.createSwap.bind(this), {
                srcToken: { type: "string", required: true, description: "Source token ticker (e.g. 'BITCOIN-BTC', 'LIGHTNING-BTC', 'STARKNET-STRK', 'SOLANA-SOL')" },
                dstToken: { type: "string", required: true, description: "Destination token ticker" },
                amount: { type: "bigint", required: true, description: "Amount in base units as an integer" },
                amountType: { type: "string", required: true, description: "EXACT_IN or EXACT_OUT", allowedValues: ["EXACT_IN", "EXACT_OUT"] },
                srcAddress: { type: "string", required: false, description: "Source address (only required for Smart chain -> BTC/Lightning swaps)" },
                dstAddress: { type: "string", required: true, description: "Destination address" },
                gasAmount: { type: "bigint", required: false, description: "Gas token amount to receive on destination chain, in base units" },
                paymentHash: { type: "string", required: false, description: "Custom payment hash for Lightning swaps" },
                lightningInvoiceDescription: { type: "string", required: false, description: "Description for Lightning invoice" },
                lightningInvoiceDescriptionHash: { type: "string", required: false, description: "Description hash for Lightning invoice (hex)" },
                lightningPaymentHTLCTimeout: { type: "number", required: false, description: "Custom expiry time in seconds" },
                lightningPaymentMaxHTLCTimeout: { type: "number", required: false, description: "Custom maximum expiry time in seconds" }
            }),
            listSwaps: createApiEndpoint<ListSwapsInput, ListSwapsOutput, "GET">("GET", "List all swaps for a given signer address. Returns an array of swap objects, each with swapId, swapType, state, quote, steps, and terminal state flags (isFinished, isSuccess, isFailed, isExpired). Optionally filter by smart chain.", this.listSwaps.bind(this), {
                signer: { type: "string", required: true, description: "Smart chain signer address to filter swaps for" },
                chainId: { type: "string", required: false, description: "Optional smart chain identifier to filter swaps" }
            }),
            listPendingSwaps: createApiEndpoint<ListPendingSwapsInput, ListPendingSwapsOutput, "GET">("GET", "List swaps that require user action for a given signer address. Returns an array of swap objects with the same structure as listSwaps.", this.listPendingSwaps.bind(this), {
                signer: { type: "string", required: true, description: "Smart chain signer address to filter pending swaps for" },
                chainId: { type: "string", required: false, description: "Optional smart chain identifier to filter pending swaps" }
            }),
            getSupportedTokens: createApiEndpoint<GetSupportedTokensInput, GetSupportedTokensOutput, "GET">("GET", "List all tokens available as swap input or output. Returns an array of ApiToken objects, each with id (e.g. BITCOIN-BTC, LIGHTNING-BTC, STARKNET-STRK), chainId, ticker, name, decimals, and address.", this.getSupportedTokens.bind(this), {
                side: {
                    type: "string",
                    required: true,
                    description: "Whether to list valid source tokens (INPUT) or destination tokens (OUTPUT)",
                    allowedValues: ["INPUT", "OUTPUT"]
                }
            }),
            getSwapCounterTokens: createApiEndpoint<GetSwapCounterTokensInput, GetSwapCounterTokensOutput, "GET">("GET", "Get tokens that can be swapped against a given token. Returns an array of ApiToken objects (id, chainId, ticker, name, decimals, address). Use to discover valid trading pairs.", this.getSwapCounterTokens.bind(this), {
                token: {
                    type: "string",
                    required: true,
                    description: "Token identifier accepted by the API, e.g. BITCOIN-BTC, LIGHTNING-BTC, STARKNET-STRK, or a token address"
                },
                side: {
                    type: "string",
                    required: true,
                    description: "Treat the provided token as a source token (INPUT) or destination token (OUTPUT)",
                    allowedValues: ["INPUT", "OUTPUT"]
                }
            }),
            getSwapLimits: createApiEndpoint<GetSwapLimitsInput, GetSwapLimitsOutput, "GET">("GET", "Get minimum and maximum swap amounts for a source/destination token pair. Returns {input: {min, max?}, output: {min, max?}} where each value is an ApiAmount object with amount (decimal string), rawAmount (base units string), decimals, symbol, and chain.", this.getSwapLimits.bind(this), {
                srcToken: { type: "string", required: true, description: "Source token identifier accepted by the API, e.g. BITCOIN-BTC, LIGHTNING-BTC, STARKNET-STRK" },
                dstToken: { type: "string", required: true, description: "Destination token identifier accepted by the API, e.g. BITCOIN-BTC, LIGHTNING-BTC, STARKNET-STRK" }
            }),
            parseAddress: createApiEndpoint<ParseAddressInput, ParseAddressOutput, "GET">("GET", "Parse and validate an address, Lightning invoice, LNURL, or Bitcoin URI. Returns {address, type} and optionally lnurl (ApiLNURL with pay/withdraw details), min/max/amount (as ApiAmount objects).", this.parseAddress.bind(this), {
                address: { type: "string", required: true, description: "Address, invoice, LNURL, or URI string to parse" }
            }),
            getSpendableBalance: createApiEndpoint<GetSpendableBalanceInput, GetSpendableBalanceOutput, "GET">("GET", "Get the spendable balance for a wallet address and token, accounting for chain fees. Returns {balance: ApiAmount, feeRate?} where ApiAmount has amount (decimal string), rawAmount (base units string), decimals, symbol, and chain.", this.getSpendableBalance.bind(this), {
                wallet: { type: "string", required: true, description: "Wallet address to query" },
                token: { type: "string", required: true, description: "Token identifier accepted by the API, e.g. BITCOIN-BTC, STARKNET-STRK, or a token address" },
                targetChain: { type: "string", required: false, description: "Destination smart chain for Bitcoin SPV-vault fee estimation" },
                gasDrop: { type: "boolean", required: false, description: "Whether to include gas-drop footprint when estimating Bitcoin SPV-vault spendable balance" },
                feeRate: { type: "string", required: false, description: "Manual fee rate override" },
                minBitcoinFeeRate: { type: "number", required: false, description: "Minimum Bitcoin fee rate to enforce" },
                feeMultiplier: { type: "number", required: false, description: "Multiplier applied to smart-chain native token commit fee estimate" }
            }),
            getSwapStatus: createApiEndpoint<GetSwapStatusInput, GetSwapStatusOutput, "GET">("GET", "Get the current status and next required action for a swap. Returns swap state, terminal flags (isFinished, isSuccess, isFailed, isExpired), and currentAction (an action object, or null when no action is currently required). For Lightning-to-smart-chain swaps it may also return requiresSecretReveal: true — when set, pass the secret parameter in subsequent getSwapStatus calls so the HTLC can be claimed. Handle each action type: SignPSBT — ask user to sign the Bitcoin PSBT with their wallet, then submit via submitTransaction. SignSmartChainTransaction — ask user to sign with their Solana/Starknet/EVM wallet, then submit via submitTransaction. SendToAddress — show the address and amount to the user, they pay externally, keep polling. Wait — poll again after pollTimeSeconds. Poll repeatedly until isFinished is true.", this.getSwapStatus.bind(this), {
                swapId: { type: "string", required: true, description: "The swap identifier" },
                secret: { type: "string", required: false, description: "Revealed swap secret pre-image (in hexadecimal format) for lightning network swaps" },
                bitcoinAddress: { type: "string", required: false, description: "Bitcoin wallet address to obtain funded PSBT" },
                bitcoinPublicKey: { type: "string", required: false, description: "Bitcoin wallet public key (in hexadecimal format) to obtain funded PSBT" },
                bitcoinFeeRate: { type: "number", required: false, description: "Fee rate to use when creating a funded PSBT" },
                signer: { type: "string", required: false, description: "Alternative different smart chain signer to use for refunds and manual settlement" }
            }),
            submitTransaction: createApiEndpoint<SubmitTransactionInput, SubmitTransactionOutput, "POST">("POST", "Submit signed transaction(s) for a swap. Call this after the user has signed the transaction returned by getSwapStatus. Returns {txHashes: string[]} with the submitted transaction hashes. After submission, continue polling getSwapStatus.", this.submitTransaction.bind(this), {
                swapId: { type: "string", required: true, description: "The swap identifier" },
                signedTxs: {
                    type: "array",
                    required: true,
                    description: "Array of signed transaction data",
                    items: {type: "string", required: true, description: "Single string-serialized & signed transaction"}
                }
            }),
            settleWithLnurl: createApiEndpoint<SettleWithLnurlInput, SettleWithLnurlOutput, "POST">("POST", "Settle a Lightning Network swap using an LNURL-withdraw link. Returns {paymentHash: string} on success.", this.settleWithLnurl.bind(this), {
                swapId: { type: "string", required: true, description: "The swap identifier" },
                lnurlWithdraw: { type: "string", required: false, description: "LNURL-withdraw link to use to settle the Lightning network swap, if the swap was already created with the LNURL-withdraw link, this is optional" }
            })
        };
    }

    private txSerializer(chainId: string, tx: any): Promise<string> {
        const chain = (this.swapper._chains as any)[chainId];
        if (chain == null) throw new Error("Unknown chain: " + chainId);
        return chain.chainInterface.serializeTx(tx);
    }

    async init(): Promise<void> {
        await this.swapper.init();
    }

    /**
     * Should be ran periodically, this synchronizes the swap's state with the on-chain data and also purges
     *  expired swaps from the persistent storage
     */
    async sync(): Promise<void> {
        await this.swapper._syncSwaps();
    }

    /**
     * Optionally good to run this periodically, such that any LPs that are dropped off because they are unresponsive
     *  can be found again.
     */
    async reloadLps(): Promise<void> {
        await this.swapper.intermediaryDiscovery.reloadIntermediaries();
    }

    private async createSwap(input: CreateSwapInput): Promise<CreateSwapOutput> {
        const exactIn = input.amountType === "EXACT_IN";

        // Build options from input
        const options: any = {};
        if (input.gasAmount != null) options.gasAmount = input.gasAmount;
        if (input.paymentHash != null) options.paymentHash = Buffer.from(input.paymentHash, "hex");
        if (input.lightningInvoiceDescription != null) options.description = input.lightningInvoiceDescription;
        if (input.lightningInvoiceDescriptionHash != null) options.descriptionHash = Buffer.from(input.lightningInvoiceDescriptionHash, "hex");
        if (input.lightningPaymentHTLCTimeout != null) options.expirySeconds = input.lightningPaymentHTLCTimeout;
        if (input.lightningPaymentMaxHTLCTimeout != null) options.maxExpirySeconds = input.lightningPaymentMaxHTLCTimeout;

        // swapper.swap() handles routing based on token types
        const swap = await this.swapper.swap(
            input.srcToken,
            input.dstToken,
            input.amount,
            exactIn,
            input.srcAddress,
            input.dstAddress,
            Object.keys(options).length > 0 ? options : undefined
        );

        const {steps, stateInfo} = await swap.getExecutionStatus({skipBuildingAction: true});

        return createSwapOutputBase(swap, steps, stateInfo);
    }

    private validateSwapListInput(input: ListSwapsInput): void {
        if (input.chainId != null && !this.swapper.getSmartChains().includes(input.chainId as any)) {
            throw new Error("Unknown chainId: " + input.chainId);
        }

        if (!this.swapper.Utils.isValidSmartChainAddress(input.signer, input.chainId as any)) {
            throw new Error(
                input.chainId != null
                    ? `Invalid ${input.chainId} signer address: ` + input.signer
                    : `Invalid smart chain signer address: ` + input.signer
            );
        }
    }

    private async createListedSwapOutputs(swaps: ISwap[]): Promise<ListSwapsOutput> {
        return Promise.all(
            swaps
                .filter(swap => swap.getType() !== SwapType.TRUSTED_FROM_BTC)
                .map(async swap => {
                    const {steps, stateInfo} = await swap.getExecutionStatus({skipBuildingAction: true});
                    return createListSwapOutput(swap, steps, stateInfo);
                })
        );
    }

    private async listSwaps(input: ListSwapsInput): Promise<ListSwapsOutput> {
        this.validateSwapListInput(input);

        const swaps = await this.swapper.getAllSwaps(input.chainId as any, input.signer);
        return this.createListedSwapOutputs(swaps);
    }

    private async listPendingSwaps(input: ListPendingSwapsInput): Promise<ListPendingSwapsOutput> {
        this.validateSwapListInput(input);

        const swaps = await this.swapper.getPendingSwaps(input.chainId as any, input.signer);
        return this.createListedSwapOutputs(swaps);
    }

    private async getSupportedTokens(input: GetSupportedTokensInput): Promise<GetSupportedTokensOutput> {
        return this.swapper.getSupportedTokens(parseSwapSide(input.side)).map(toApiToken);
    }

    private async getSwapCounterTokens(input: GetSwapCounterTokensInput): Promise<GetSwapCounterTokensOutput> {
        const token = this.swapper.getToken(input.token);
        return this.swapper.getSwapCounterTokens(token, parseSwapSide(input.side)).map(toApiToken);
    }

    private async getSwapLimits(input: GetSwapLimitsInput): Promise<GetSwapLimitsOutput> {
        const srcToken = this.swapper.getToken(input.srcToken);
        const dstToken = this.swapper.getToken(input.dstToken);
        let limits = this.swapper.getSwapLimits(srcToken, dstToken);

        if(dstToken.chainId!=="LIGHTNING") {
            if(limits.input.min.rawAmount===1n || limits.output.min.rawAmount===1n) {
                // Execute a dummy swap to get the proper limits
                try {
                    await this.swapper.swap(
                        srcToken, dstToken,
                        1n, limits.input.min.rawAmount===1n,
                        srcToken.chainId==="LIGHTNING" ? undefined : this.swapper.Utils.randomAddress(srcToken.chainId),
                        this.swapper.Utils.randomAddress(dstToken.chainId)
                    );
                } catch (e) {}
                limits = this.swapper.getSwapLimits(srcToken, dstToken);
            }
        }

        return {
            input: {
                min: toApiAmount(limits.input.min),
                ...(limits.input.max != null ? {max: toApiAmount(limits.input.max)} : {})
            },
            output: {
                min: toApiAmount(limits.output.min),
                ...(limits.output.max != null ? {max: toApiAmount(limits.output.max)} : {})
            }
        };
    }

    private async parseAddress(input: ParseAddressInput): Promise<ParseAddressOutput> {
        const result = await this.swapper.Utils.parseAddress(input.address);
        if(result == null) throw new Error("Invalid address");

        return {
            address: result.address,
            type: result.type,
            ...(result.lnurl != null ? {lnurl: toApiLNURL(result.lnurl, this.swapper)} : {}),
            ...(result.min != null ? {min: toApiAmount(result.min)} : {}),
            ...(result.max != null ? {max: toApiAmount(result.max)} : {}),
            ...(result.amount != null ? {amount: toApiAmount(result.amount)} : {})
        };
    }

    private async getSpendableBalance(input: GetSpendableBalanceInput): Promise<GetSpendableBalanceOutput> {
        const token = this.swapper.getToken(input.token);

        if(token.chainId === "LIGHTNING")
            throw new Error("Lightning wallet spendable balance is not supported by this endpoint.");

        if(input.feeRate != null && input.feeMultiplier != null)
            throw new Error("`feeMultiplier` cannot be specified alongside the `feeRate` parameter.");

        if(token.chainId === "BITCOIN") {
            if(input.targetChain != null && !this.swapper.getSmartChains().includes(input.targetChain as any)) {
                throw new Error("Unknown targetChain: " + input.targetChain);
            }

            if (!this.swapper.Utils.isValidBitcoinAddress(input.wallet))
                throw new Error(`Invalid BITCOIN wallet address: ` + input.wallet);

            let btcFeeRate: number;
            if(input.feeRate != null) {
                btcFeeRate = parseFloat(input.feeRate);
                if(isNaN(btcFeeRate) || btcFeeRate <= 0) throw new Error("Bitcoin `feeRate` must be a valid positive number!");
            } else btcFeeRate = await this.swapper._bitcoinRpc.getFeeRate()
            if(input.feeMultiplier != null) btcFeeRate *= input.feeMultiplier;

            const {balance, feeRate} = await this.swapper.Utils.getBitcoinSpendableBalance(input.wallet, input.targetChain as any, {
                gasDrop: input.gasDrop,
                feeRate: btcFeeRate,
                minFeeRate: input.minBitcoinFeeRate
            });

            return {
                balance: toApiAmount(balance),
                feeRate
            };
        }

        if(input.gasDrop === true) throw new Error("`gasDrop` is only supported for Bitcoin balances.");
        if(input.minBitcoinFeeRate != null) throw new Error("`minBitcoinFeeRate` is only supported for Bitcoin balances.");

        if (!this.swapper.Utils.isValidSmartChainAddress(input.wallet, token.chainId))
            throw new Error(`Invalid ${token.chainId} wallet address: ` + input.wallet);

        const balance = await this.swapper.Utils.getSpendableBalance(input.wallet, token as any, {
            feeMultiplier: input.feeMultiplier,
            feeRate: input.feeRate
        });

        return {
            balance: toApiAmount(balance)
        };
    }

    private async getSwapStatus(input: GetSwapStatusInput): Promise<GetSwapStatusOutput> {
        const swap = await this.swapper.getSwapById(input.swapId);
        if (swap == null) {
            throw new Error("Swap not found: " + input.swapId);
        }

        if (input.signer != null && !this.swapper.Utils.isValidSmartChainAddress(input.signer, swap.chainIdentifier)) {
            throw new Error(`Invalid ${swap.chainIdentifier} signer address: ` + input.signer);
        }

        if (input.secret != null) {
            try {
                Buffer.from(input.secret, "hex");
            } catch (e) {
                throw new Error(`Invalid secret passed, has to be a hexadecimal string!`);
            }
        }

        let bitcoinWallet: MinimalBitcoinWalletInterface | undefined;
        if (input.bitcoinAddress != null && input.bitcoinPublicKey != null) {
            bitcoinWallet = {
                publicKey: input.bitcoinPublicKey,
                address: input.bitcoinAddress
            };
        } else if(input.bitcoinAddress != null || input.bitcoinPublicKey != null) {
            throw new Error("When specifying bitcoin wallet you have to pass both `bitcoinAddress` and `bitcoinPublicKey` params!");
        }

        if (input.bitcoinFeeRate != null) {
            if(isNaN(input.bitcoinFeeRate)) throw new Error("Bitcoin fee rate passed cannot be NaN!");
            if(input.bitcoinFeeRate <= 0) throw new Error("Bitcoin fee rate passed cannot be negative or 0!");
        }

        if(this.config?.syncOnGetStatus) await swap._sync(true);

        const {steps, stateInfo, currentAction} = await swap.getExecutionStatus({
            secret: input.secret,

            bitcoinWallet,
            bitcoinFeeRate: input.bitcoinFeeRate,

            manualSettlementSmartChainSigner: input.signer,
            refundSmartChainSigner: input.signer
        });

        return {
            ...createListSwapOutput(swap, steps, stateInfo),

            currentAction: currentAction ? await serializeAction(currentAction, this.txSerializer.bind(this)) : null,
            requiresSecretReveal: requiresSecretRevealForApi(swap, stateInfo.state),

            escrow: swap instanceof IEscrowSwap && swap._data!=null ? {
                data: swap._data.getEscrowStruct(),
                initTxId: swap._commitTxId
            } : undefined
        };
    }

    private async submitTransaction(input: SubmitTransactionInput, abortSignal?: AbortSignal): Promise<SubmitTransactionOutput> {
        const swap = await this.swapper.getSwapById(input.swapId);
        if (swap == null) {
            throw new Error("Swap not found: " + input.swapId);
        }

        return {
            txHashes: await swap._submitExecutionTransactions(input.signedTxs, abortSignal, undefined, this.config?.idempotentTxSubmission)
        }
    }

    private async settleWithLnurl(input: SettleWithLnurlInput, abortSignal?: AbortSignal): Promise<SettleWithLnurlOutput> {
        const swap = await this.swapper.getSwapById(input.swapId);
        if (swap == null) throw new Error("Swap not found: " + input.swapId);

        if (swap instanceof FromBTCLNAutoSwap) {
            if(swap._state!==FromBTCLNAutoSwapState.PR_CREATED)
                throw new Error("Invalid swap state, must be in PR_CREATED state!");
        } else if (swap instanceof FromBTCLNSwap) {
            if(swap._state!==FromBTCLNSwapState.PR_CREATED)
                throw new Error("Invalid swap state, must be in PR_CREATED state!");
        } else {
            throw new Error("Endpoint only supports swaps from Lightning");
        }

        if (!swap.isLNURL()) {
            if (input.lnurlWithdraw==null)
                throw new Error("The swap is not configured to use LNURL, please pass the `lnurlWithdraw` parameter!");

            if (!this.swapper.Utils.isValidLNURL(input.lnurlWithdraw))
                throw new Error("Invalid LNURL-withdraw link provided: " + input.lnurlWithdraw);

            await swap.settleWithLNURLWithdraw(input.lnurlWithdraw);
        } else {
            if (input.lnurlWithdraw!=null)
                throw new Error("The swap is already configured with an LNURL link, don't pass the `lnurlWithdraw` parameter!");
        }

        let success: boolean;
        if (swap instanceof FromBTCLNAutoSwap) {
            // For non-legacy swap, we don't need to wait till the swap advances all the way to committed state
            success = await swap._waitForLpPaymentReceived(2, abortSignal);
        } else {
            // For legacy swap waitForPayment waits just for the swap to transition into PR_PAID
            success = await swap.waitForPayment(undefined, 2, abortSignal);
        }

        if(!success) throw new Error("Failed to settle the swap with the LNURL-withdraw link!");

        return {
            paymentHash: swap.getInputTxId()!
        };
    }

}
