import {
    BitcoinRpc,
    BtcTx,
    RelaySynchronizer,
    SpvVaultContract,
    SpvVaultTokenData, SpvWithdrawalClaimedState, SpvWithdrawalClosedState, SpvWithdrawalFrontedState,
    SpvWithdrawalState,
    SpvWithdrawalStateType,
    SpvWithdrawalTransactionData,
    TransactionConfirmationOptions
} from "@atomiqlabs/base";
import {Buffer} from "buffer";
import {StarknetTx} from "../chain/modules/StarknetTransactions";
import {StarknetContractBase} from "../contract/StarknetContractBase";
import {StarknetChainInterface} from "../chain/StarknetChainInterface";
import {StarknetBtcRelay} from "../btcrelay/StarknetBtcRelay";
import {cairo, constants} from "starknet";
import {StarknetAction} from "../chain/StarknetAction";
import {SpvVaultContractAbi} from "./SpvVaultContractAbi";
import {StarknetSigner} from "../wallet/StarknetSigner";
import {StarknetSpvVaultData} from "./StarknetSpvVaultData";
import {StarknetSpvWithdrawalData} from "./StarknetSpvWithdrawalData";
import {bigNumberishToBuffer, bufferToByteArray, bufferToU32Array, getLogger, toBigInt, toHex} from "../../utils/Utils";
import {StarknetBtcStoredHeader} from "../btcrelay/headers/StarknetBtcStoredHeader";
import {StarknetAddresses} from "../chain/modules/StarknetAddresses";
import {StarknetFees} from "../chain/modules/StarknetFees";
import {StarknetAbiEvent} from "../contract/modules/StarknetContractEvents";

const spvVaultContractAddreses = {
    [constants.StarknetChainId.SN_SEPOLIA]: "0x02d581ea838cd5ca46ba08660eddd064d50a0392f618e95310432147928d572e",
    [constants.StarknetChainId.SN_MAIN]: "0x01932042992647771f3d0aa6ee526e65359c891fe05a285faaf4d3ffa373e132"
};

const spvVaultContractDeploymentHeights = {
    [constants.StarknetChainId.SN_SEPOLIA]: 1118191,
    [constants.StarknetChainId.SN_MAIN]: 1617295
};

const STARK_PRIME_MOD: bigint = 2n**251n + 17n * 2n**192n + 1n;

function decodeUtxo(utxo: string): {txHash: bigint, vout: bigint} {
    const [txId, vout] = utxo.split(":");
    return {
        txHash: BigInt("0x"+Buffer.from(txId, "hex").reverse().toString("hex")),
        vout: BigInt(vout)
    }
}

/**
 * Starknet SPV vault (UTXO-controlled vault) contract representation
 *
 * @category Swaps
 */
export class StarknetSpvVaultContract
    extends StarknetContractBase<typeof SpvVaultContractAbi>
    implements SpvVaultContract<
        StarknetTx,
        StarknetSigner,
        "STARKNET",
        StarknetSpvWithdrawalData,
        StarknetSpvVaultData
    >
{
    private static readonly GasCosts = {
        DEPOSIT: {l1DataGas: 400, l2Gas: 4_000_000, l1Gas: 0},
        OPEN: {l1DataGas: 1200, l2Gas: 8_000_000, l1Gas: 0},
        FRONT: {l1DataGas: 800, l2Gas: 12_000_000, l1Gas: 0},
        CLAIM: {l1DataGas: 1000, l2Gas: 400_000_000, l1Gas: 0},
        CLAIM_OPTIMISTIC_ESTIMATE: {l1DataGas: 1000, l2Gas: 80_000_000, l1Gas: 0} //If claimer uses sierra 1.7.0 or later
    };

    readonly chainId = "STARKNET";

    readonly claimTimeout: number = 180;
    readonly maxClaimsPerTx: number = 10;

    private readonly btcRelay: StarknetBtcRelay<any>;
    private readonly bitcoinRpc: BitcoinRpc<any>;
    private readonly logger = getLogger("StarknetSpvVaultContract: ");

    constructor(
        chainInterface: StarknetChainInterface,
        btcRelay: StarknetBtcRelay<any>,
        bitcoinRpc: BitcoinRpc<any>,
        contractAddress: string = spvVaultContractAddreses[chainInterface.starknetChainId],
        contractDeploymentHeight?: number
    ) {
        super(
            chainInterface, contractAddress, SpvVaultContractAbi,
            contractDeploymentHeight ??
            (spvVaultContractAddreses[chainInterface.starknetChainId]===contractAddress
                ? spvVaultContractDeploymentHeights[chainInterface.starknetChainId]
                : undefined)
        );
        this.btcRelay = btcRelay;
        this.bitcoinRpc = bitcoinRpc;
    }

    /**
     * Returns a {@link StarknetAction} that opens up the spv vault with the passed data
     *
     * @param signer A starknet signer's address
     * @param vault Vault data and configuration
     */
    public Open(signer: string, vault: StarknetSpvVaultData): StarknetAction {
        const {txHash, vout} = decodeUtxo(vault.getUtxo());

        const tokens = vault.getTokenData();
        if(tokens.length!==2) throw new Error("Must specify exactly 2 tokens for vault!");

        return new StarknetAction(signer, this.Chain,
            this.contract.populateTransaction.open(
                vault.getVaultId(), this.btcRelay.contract.address,
                cairo.tuple(cairo.uint256(txHash), vout), vault.getConfirmations(),
                tokens[0].token, tokens[1].token, tokens[0].multiplier, tokens[1].multiplier
            ),
            StarknetSpvVaultContract.GasCosts.OPEN
        );
    }

    /**
     * Returns a {@link StarknetAction} that deposits assets to the spv vault, amounts have to be already scaled!
     *  This also doesn't add the approval call!
     *
     * @param signer A starknet signer's address
     * @param vault Vault data and configuration
     * @param rawAmounts An array of amounts to deposit, since the vault supports 2 tokens, up to 2 amounts are allowed
     */
    public Deposit(signer: string, vault: StarknetSpvVaultData, rawAmounts: bigint[]): StarknetAction {
        return new StarknetAction(signer, this.Chain,
            this.contract.populateTransaction.deposit(vault.getOwner(), vault.getVaultId(), rawAmounts[0], rawAmounts[1] ?? 0n),
            StarknetSpvVaultContract.GasCosts.DEPOSIT
        );
    }

    /**
     * Returns a {@link StarknetAction} that fronts the vault withdrawal. This doesn't add the approval call!
     *
     * @param signer A starknet signer's address
     * @param vault Vault data and configuration
     * @param data Vault withdrawal transaction data to front
     * @param withdrawalSequence Which withdrawal in sequence is this, used to prevent race conditions when 2 parties
     *  were to front at the same time
     */
    public Front(signer: string, vault: StarknetSpvVaultData, data: StarknetSpvWithdrawalData, withdrawalSequence: number) {
        return new StarknetAction(signer, this.Chain,
            this.contract.populateTransaction.front(
                vault.getOwner(), vault.getVaultId(), BigInt(withdrawalSequence),
                data.getTxHash(), data.serializeToStruct()
            ),
            StarknetSpvVaultContract.GasCosts.FRONT
        );
    }

    /**
     * Returns a {@link StarknetAction} that submits the withdrawal data and executes the vault withdrawal
     *
     * @param signer A starknet signer's address
     * @param vault Vault data and configuration
     * @param data Vault withdrawal transaction data to execute and claim assets based on it
     * @param blockheader A stored and committed bitcoin blockheader where the bitcoin transaction got confirmed
     * @param merkle Merkle proof for the bitcoin transaction
     * @param position Position of the bitcoin transaction in the block - used for the merkle proof verification
     */
    public Claim(
        signer: string, vault: StarknetSpvVaultData, data: StarknetSpvWithdrawalData,
        blockheader: StarknetBtcStoredHeader, merkle: Buffer[], position: number
    ) {
        return new StarknetAction(signer, this.Chain,
            {
                contractAddress: this.contract.address,
                entrypoint: "claim",
                calldata: [
                    vault.getOwner(),
                    vault.getVaultId(),
                    ...bufferToByteArray(Buffer.from(data.btcTx.hex, "hex")),
                    ...blockheader.serialize(),
                    merkle.length,
                    ...merkle.map(bufferToU32Array).flat(),
                    position,
                ].map(val => toHex(val, 0))
            },
            StarknetSpvVaultContract.GasCosts.CLAIM
        );
    }

    /**
     * @inheritDoc
     */
    async checkWithdrawalTx(tx: SpvWithdrawalTransactionData): Promise<void> {
        const result = await this.Chain.provider.callContract({
            contractAddress: this.contract.address,
            entrypoint: "parse_bitcoin_tx",
            calldata: bufferToByteArray(Buffer.from(tx.btcTx.hex, "hex"))
        });
        if(result==null) throw new Error("Failed to parse transaction!");
    }

    /**
     * @inheritDoc
     */
    createVaultData(owner: string, vaultId: bigint, utxo: string, confirmations: number, tokenData: SpvVaultTokenData[]): Promise<StarknetSpvVaultData> {
        if(tokenData.length!==2) throw new Error("Must specify 2 tokens in tokenData!");
        return Promise.resolve(new StarknetSpvVaultData({
            owner,
            vaultId,
            struct: {
                relay_contract: this.btcRelay.contract.address,
                token_0: tokenData[0].token,
                token_1: tokenData[1].token,
                token_0_multiplier: tokenData[0].multiplier,
                token_1_multiplier: tokenData[1].multiplier,
                utxo: cairo.tuple(cairo.uint256(0), 0),
                confirmations: confirmations,
                withdraw_count: 0,
                deposit_count: 0,
                token_0_amount: 0n,
                token_1_amount: 0n
            },
            initialUtxo: utxo
        }));
    }

    //Getters
    /**
     * @inheritDoc
     */
    async getVaultData(owner: string, vaultId: bigint): Promise<StarknetSpvVaultData | null> {
        const struct = await this.contract.get_vault(owner, vaultId);
        if(toHex(struct.relay_contract)!==toHex(this.btcRelay.contract.address)) return null;
        return new StarknetSpvVaultData({
            owner, vaultId, struct
        });
    }

    /**
     * @inheritDoc
     */
    async getMultipleVaultData(vaults: {owner: string, vaultId: bigint}[]): Promise<{[owner: string]: {[vaultId: string]: StarknetSpvVaultData | null}}> {
        const result: {[owner: string]: {[vaultId: string]: StarknetSpvVaultData | null}} = {};
        let promises: Promise<void>[] = [];
        //TODO: We can upgrade this to use multicall
        for(let {owner, vaultId} of vaults) {
            promises.push(this.getVaultData(owner, vaultId).then(val => {
                result[owner] ??= {};
                result[owner][vaultId.toString(10)] = val;
            }));
            if(promises.length>=this.Chain.config.maxParallelCalls!) {
                await Promise.all(promises);
                promises = [];
            }
        }
        await Promise.all(promises);
        return result;
    }

    /**
     * @inheritDoc
     */
    async getVaultLatestUtxo(owner: string, vaultId: bigint): Promise<string | null> {
        const vault = await this.getVaultData(owner, vaultId);
        if(vault==null) return null;
        if(!vault.isOpened()) return null;
        return vault.getUtxo();
    }

    /**
     * @inheritDoc
     */
    async getVaultLatestUtxos(vaults: {owner: string, vaultId: bigint}[]): Promise<{[owner: string]: {[vaultId: string]: string | null}}> {
        const result: {[owner: string]: {[vaultId: string]: string | null}} = {};
        let promises: Promise<void>[] = [];
        //TODO: We can upgrade this to use multicall
        for(let {owner, vaultId} of vaults) {
            promises.push(this.getVaultLatestUtxo(owner, vaultId).then(val => {
                result[owner] ??= {};
                result[owner][vaultId.toString(10)] = val;
            }));
            if(promises.length>=this.Chain.config.maxParallelCalls!) {
                await Promise.all(promises);
                promises = [];
            }
        }
        await Promise.all(promises);
        return result;
    }

    /**
     * @inheritDoc
     */
    async getAllVaults(owner?: string): Promise<StarknetSpvVaultData[]> {
        const openedVaults = new Set<string>();
        await this._Events.findInContractEventsForward(
            ["spv_swap_vault::events::Opened", "spv_swap_vault::events::Closed"],
            owner==null ? null : [null, null, owner],
            (event) => {
                const owner = toHex(event.params.owner);
                const vaultId = toBigInt(event.params.vault_id);
                const vaultIdentifier = owner+":"+vaultId.toString(10);
                if(event.name==="spv_swap_vault::events::Opened") {
                    openedVaults.add(vaultIdentifier);
                } else {
                    openedVaults.delete(vaultIdentifier);
                }
                return Promise.resolve(null);
            }
        );

        const fetchedVaultData = await this.getMultipleVaultData([...openedVaults.keys()].map(identifier => {
            const [owner, vaultIdStr] = identifier.split(":");
            return {owner, vaultId: BigInt(vaultIdStr)}
        }));

        const vaults: StarknetSpvVaultData[] = [];
        for(let owner in fetchedVaultData) {
            for(let vaultIdStr in fetchedVaultData[owner]) {
                const vault = fetchedVaultData[owner][vaultIdStr];
                if(vault!=null) vaults.push(vault);
            }
        }

        return vaults;
    }

    /**
     * @inheritDoc
     */
    async getFronterAddress(owner: string, vaultId: bigint, withdrawal: StarknetSpvWithdrawalData): Promise<string | null> {
        const fronterAddress = await this.contract.get_fronter_address_by_id(owner, vaultId, "0x"+withdrawal.getFrontingId());
        if(toHex(fronterAddress, 64)==="0x0000000000000000000000000000000000000000000000000000000000000000") return null;
        return fronterAddress;
    }

    /**
     * @inheritDoc
     */
    async getFronterAddresses(withdrawals: {owner: string, vaultId: bigint, withdrawal: StarknetSpvWithdrawalData}[]): Promise<{[btcTxId: string]: string | null}> {
        const result: {
            [btcTxId: string]: string | null
        } = {};
        let promises: Promise<void>[] = [];
        //TODO: We can upgrade this to use multicall
        for(let {owner, vaultId, withdrawal} of withdrawals) {
            promises.push(this.getFronterAddress(owner, vaultId, withdrawal).then(val => {
                result[withdrawal.getTxId()] = val;
            }));
            if(promises.length>=this.Chain.config.maxParallelCalls!) {
                await Promise.all(promises);
                promises = [];
            }
        }
        await Promise.all(promises);
        return result;
    }

    /**
     *
     * @param event
     * @private
     */
    private parseWithdrawalEvent(
        event: StarknetAbiEvent<typeof SpvVaultContractAbi, "spv_swap_vault::events::Fronted"> |
            StarknetAbiEvent<typeof SpvVaultContractAbi, "spv_swap_vault::events::Claimed"> |
            StarknetAbiEvent<typeof SpvVaultContractAbi, "spv_swap_vault::events::Closed">
    ): ((SpvWithdrawalFrontedState | SpvWithdrawalClaimedState | SpvWithdrawalClosedState) & {btcTxId: string}) | null {
        switch(event.name) {
            case "spv_swap_vault::events::Fronted":
                return {
                    type: SpvWithdrawalStateType.FRONTED,
                    btcTxId: bigNumberishToBuffer(event.params.btc_tx_hash, 32).reverse().toString("hex"),
                    owner: toHex(event.params.owner),
                    vaultId: toBigInt(event.params.vault_id),
                    recipient: toHex(event.params.recipient),
                    fronter: toHex(event.params.caller),
                    txId: event.txHash,
                    getTxBlock: async() => ({
                        blockHeight: event.blockNumber!,
                        blockTime: await this.Chain.Blocks.getBlockTime(event.blockNumber!)
                    })
                };
            case "spv_swap_vault::events::Claimed":
                return {
                    type: SpvWithdrawalStateType.CLAIMED,
                    btcTxId: bigNumberishToBuffer(event.params.btc_tx_hash, 32).reverse().toString("hex"),
                    owner: toHex(event.params.owner),
                    vaultId: toBigInt(event.params.vault_id),
                    recipient: toHex(event.params.recipient),
                    claimer: toHex(event.params.caller),
                    fronter: toHex(event.params.fronting_address),
                    txId: event.txHash,
                    getTxBlock: async() => ({
                        blockHeight: event.blockNumber!,
                        blockTime: await this.Chain.Blocks.getBlockTime(event.blockNumber!)
                    })
                };
            case "spv_swap_vault::events::Closed":
                return {
                    type: SpvWithdrawalStateType.CLOSED,
                    btcTxId: bigNumberishToBuffer(event.params.btc_tx_hash, 32).reverse().toString("hex"),
                    owner: toHex(event.params.owner),
                    vaultId: toBigInt(event.params.vault_id),
                    error: bigNumberishToBuffer(event.params.error).toString(),
                    txId: event.txHash,
                    getTxBlock: async() => ({
                        blockHeight: event.blockNumber!,
                        blockTime: await this.Chain.Blocks.getBlockTime(event.blockNumber!)
                    })
                };
            default:
                return null;
        }
    }

    /**
     * @inheritDoc
     */
    async getWithdrawalStates(withdrawalTxs: {withdrawal: StarknetSpvWithdrawalData, scStartBlockheight?: number}[]): Promise<{[btcTxId: string]: SpvWithdrawalState}> {
        const result: {[btcTxId: string]: SpvWithdrawalState} = {};
        withdrawalTxs.forEach(withdrawalTx => {
            result[withdrawalTx.withdrawal.getTxId()] = {
                type: SpvWithdrawalStateType.NOT_FOUND
            };
        });

        const events: ["spv_swap_vault::events::Fronted", "spv_swap_vault::events::Claimed", "spv_swap_vault::events::Closed"] =
            ["spv_swap_vault::events::Fronted", "spv_swap_vault::events::Claimed", "spv_swap_vault::events::Closed"];

        for(let i=0;i<withdrawalTxs.length;i+=this.Chain.config.maxGetLogKeys!) {
            const checkWithdrawalTxs = withdrawalTxs.slice(i, i+this.Chain.config.maxGetLogKeys!);
            const lows: string[] = [];
            const highs: string[] = [];
            let startHeight: number | null | undefined = undefined;
            checkWithdrawalTxs.forEach(withdrawalTx => {
                const txHash = Buffer.from(withdrawalTx.withdrawal.getTxId(), "hex").reverse();
                const txHashU256 = cairo.uint256("0x"+txHash.toString("hex"));
                lows.push(toHex(txHashU256.low));
                highs.push(toHex(txHashU256.high));
                if(startHeight!==null) {
                    if(withdrawalTx.scStartBlockheight==null) {
                        startHeight = null;
                    } else {
                        startHeight = Math.min(startHeight ?? Infinity, withdrawalTx.scStartBlockheight);
                    }
                }
            });

            await this._Events.findInContractEventsForward(
                events,[lows, highs],
                async (event) => {
                    const txId = bigNumberishToBuffer(event.params.btc_tx_hash, 32).reverse().toString("hex");
                    if(result[txId]==null) {
                        this.logger.warn(`getWithdrawalStates(): findInContractEvents-callback: loaded event for ${txId}, but transaction not found in input params!`)
                        return;
                    }
                    const eventResult = this.parseWithdrawalEvent(event);
                    if(eventResult!=null) result[txId] = eventResult;
                },
                startHeight
            );
        }

        return result;
    }

    /**
     * @inheritDoc
     */
    async getWithdrawalState(withdrawalTx: StarknetSpvWithdrawalData, scStartBlockheight?: number): Promise<SpvWithdrawalState> {
        const txHash = Buffer.from(withdrawalTx.getTxId(), "hex").reverse();
        const txHashU256 = cairo.uint256("0x"+txHash.toString("hex"));
        let result: SpvWithdrawalState = {
            type: SpvWithdrawalStateType.NOT_FOUND
        };
        const events: ["spv_swap_vault::events::Fronted", "spv_swap_vault::events::Claimed", "spv_swap_vault::events::Closed"] =
            ["spv_swap_vault::events::Fronted", "spv_swap_vault::events::Claimed", "spv_swap_vault::events::Closed"];
        const keys = [toHex(txHashU256.low), toHex(txHashU256.high)];

        await this._Events.findInContractEventsForward(
            events, keys,
            async (event) => {
                const eventResult = this.parseWithdrawalEvent(event);
                if(eventResult!=null) result = eventResult;
            },
            scStartBlockheight
        );
        return result;
    }

    async getHistoricalWithdrawalStates(recipient: string, startBlockheight?: number): Promise<{
        withdrawals: { [btcTxId: string]: SpvWithdrawalClaimedState | SpvWithdrawalFrontedState };
        latestBlockheight?: number
    }> {
        const {height: latestBlockheight} = await this.Chain.getFinalizedBlock();
        const withdrawals: { [btcTxId: string]: SpvWithdrawalClaimedState | SpvWithdrawalFrontedState } = {};

        const eventTypes: ["spv_swap_vault::events::Fronted", "spv_swap_vault::events::Claimed"] =
            ["spv_swap_vault::events::Fronted", "spv_swap_vault::events::Claimed"];

        await this._Events.findInContractEventsForward(
            eventTypes,
            [null, null, null, null, recipient],
            async (_event) => {
                const eventResult = this.parseWithdrawalEvent(_event);
                if(eventResult==null || eventResult.type===SpvWithdrawalStateType.CLOSED) return null;
                withdrawals[eventResult.btcTxId] = eventResult;
            },
            startBlockheight
        );

        return {
            withdrawals,
            latestBlockheight
        }
    }

    /**
     * @inheritDoc
     */
    getWithdrawalData(btcTx: BtcTx): Promise<StarknetSpvWithdrawalData> {
        return Promise.resolve(new StarknetSpvWithdrawalData(btcTx));
    }

    //OP_RETURN data encoding/decoding
    /**
     * @inheritDoc
     */
    fromOpReturnData(data: Buffer): { recipient: string; rawAmounts: bigint[]; executionHash?: string } {
        return StarknetSpvVaultContract.fromOpReturnData(data);
    }

    /**
     * Parses withdrawal params from OP_RETURN data
     *
     * @param data data as specified in the OP_RETURN output of the transaction
     */
    static fromOpReturnData(data: Buffer): { recipient: string; rawAmounts: bigint[]; executionHash?: string } {
        let rawAmount0: bigint = 0n;
        let rawAmount1: bigint = 0n;
        let executionHash: string | undefined = undefined;
        if(data.length===40) {
            rawAmount0 = data.readBigInt64LE(32).valueOf();
        } else if(data.length===48) {
            rawAmount0 = data.readBigInt64LE(32).valueOf();
            rawAmount1 = data.readBigInt64LE(40).valueOf();
        } else if(data.length===72) {
            rawAmount0 = data.readBigInt64LE(32).valueOf();
            executionHash = data.slice(40, 72).toString("hex");
        } else if(data.length===80) {
            rawAmount0 = data.readBigInt64LE(32).valueOf();
            rawAmount1 = data.readBigInt64LE(40).valueOf();
            executionHash = data.slice(48, 80).toString("hex");
        } else {
            throw new Error("Invalid OP_RETURN data length!");
        }

        if(executionHash!=undefined) {
            const executionHashValue = BigInt("0x"+executionHash);
            if(executionHashValue >= STARK_PRIME_MOD) throw new Error("Execution hash not in range of starknet prime");
        }

        const recipient = "0x"+data.slice(0, 32).toString("hex");
        if(!StarknetAddresses.isValidAddress(recipient)) throw new Error("Invalid recipient specified");

        return {executionHash, rawAmounts: [rawAmount0, rawAmount1], recipient};
    }

    /**
     * @inheritDoc
     */
    toOpReturnData(recipient: string, rawAmounts: bigint[], executionHash?: string): Buffer {
        return StarknetSpvVaultContract.toOpReturnData(recipient, rawAmounts, executionHash);
    }

    /**
     * Serializes the withdrawal params to the OP_RETURN data
     *
     * @param recipient Recipient of the withdrawn tokens
     * @param rawAmounts Raw amount of tokens to withdraw
     * @param executionHash Optional execution hash of the actions to execute
     */
    static toOpReturnData(recipient: string, rawAmounts: bigint[], executionHash?: string): Buffer {
        if(!StarknetAddresses.isValidAddress(recipient)) throw new Error("Invalid recipient specified");
        if(rawAmounts.length < 1) throw new Error("At least 1 amount needs to be specified");
        if(rawAmounts.length > 2) throw new Error("At most 2 amounts need to be specified");
        rawAmounts.forEach(val => {
            if(val < 0n) throw new Error("Negative raw amount specified");
            if(val >= 2n**64n) throw new Error("Raw amount overflow");
        });
        if(executionHash!=null) {
            const executionHashValue = toBigInt(executionHash);
            if(executionHashValue < 0n) throw new Error("Execution hash negative");
            if(executionHashValue >= STARK_PRIME_MOD) throw new Error("Execution hash not in range of starknet prime");
        }
        const recipientBuffer = Buffer.from(recipient.substring(2).padStart(64, "0"), "hex");
        const amount0Buffer = Buffer.from(rawAmounts[0].toString(16).padStart(16, "0"), "hex");
        const amount1Buffer = rawAmounts[1]==null || rawAmounts[1]===0n ? Buffer.alloc(0) : Buffer.from(rawAmounts[1].toString(16).padStart(16, "0"), "hex");
        const executionHashBuffer = executionHash==null ? Buffer.alloc(0) : Buffer.from(executionHash.substring(2).padStart(64, "0"), "hex");

        return Buffer.concat([
            recipientBuffer,
            amount0Buffer.reverse(),
            amount1Buffer.reverse(),
            executionHashBuffer
        ]);
    }

    //Actions
    /**
     * @inheritDoc
     */
    async claim(signer: StarknetSigner, vault: StarknetSpvVaultData, txs: {tx: StarknetSpvWithdrawalData, storedHeader?: StarknetBtcStoredHeader}[], synchronizer?: RelaySynchronizer<any, any, any>, initAta?: boolean, txOptions?: TransactionConfirmationOptions): Promise<string> {
        const result = await this.txsClaim(signer.getAddress(), vault, txs, synchronizer, initAta, txOptions?.feeRate);
        const [signature] = await this.Chain.sendAndConfirm(signer, result, txOptions?.waitForConfirmation, txOptions?.abortSignal);
        return signature;
    }

    /**
     * @inheritDoc
     */
    async deposit(signer: StarknetSigner, vault: StarknetSpvVaultData, rawAmounts: bigint[], txOptions?: TransactionConfirmationOptions): Promise<string> {
        const result = await this.txsDeposit(signer.getAddress(), vault, rawAmounts, txOptions?.feeRate);
        const [signature] = await this.Chain.sendAndConfirm(signer, result, txOptions?.waitForConfirmation, txOptions?.abortSignal);
        return signature;
    }

    /**
     * @inheritDoc
     */
    async frontLiquidity(signer: StarknetSigner, vault: StarknetSpvVaultData, realWithdrawalTx: StarknetSpvWithdrawalData, withdrawSequence: number, txOptions?: TransactionConfirmationOptions): Promise<string> {
        const result = await this.txsFrontLiquidity(signer.getAddress(), vault, realWithdrawalTx, withdrawSequence, txOptions?.feeRate);
        const [signature] = await this.Chain.sendAndConfirm(signer, result, txOptions?.waitForConfirmation, txOptions?.abortSignal);
        return signature;
    }

    /**
     * @inheritDoc
     */
    async open(signer: StarknetSigner, vault: StarknetSpvVaultData, txOptions?: TransactionConfirmationOptions): Promise<string> {
        const result = await this.txsOpen(signer.getAddress(), vault, txOptions?.feeRate);
        const [signature] = await this.Chain.sendAndConfirm(signer, result, txOptions?.waitForConfirmation, txOptions?.abortSignal);
        return signature;
    }

    //Transactions
    /**
     * @inheritDoc
     */
    async txsClaim(
        signer: string,
        vault: StarknetSpvVaultData,
        txs: {
            tx: StarknetSpvWithdrawalData,
            storedHeader?: StarknetBtcStoredHeader
        }[],
        synchronizer?: RelaySynchronizer<any, any, any>,
        initAta?: boolean,
        feeRate?: string
    ): Promise<StarknetTx[]> {
        if(!vault.isOpened()) throw new Error("Cannot claim from a closed vault!");
        feeRate ??= await this.Chain.Fees.getFeeRate();

        const txsWithMerkleProofs: {
            tx: StarknetSpvWithdrawalData,
            reversedTxId: Buffer,
            pos: number,
            blockheight: number,
            merkle: Buffer[],
            storedHeader?: StarknetBtcStoredHeader
        }[] = [];
        for(let tx of txs) {
            if(tx.tx.btcTx.blockhash==null) throw new Error(`Transaction ${tx.tx.btcTx.txid} doesn't have any blockhash, unconfirmed?`);
            const merkleProof = await this.bitcoinRpc.getMerkleProof(tx.tx.btcTx.txid, tx.tx.btcTx.blockhash);
            if(merkleProof==null) throw new Error(`Failed to get merkle proof for tx: ${tx.tx.btcTx.txid}!`);
            this.logger.debug("txsClaim(): merkle proof computed: ", merkleProof);
            txsWithMerkleProofs.push({
                ...merkleProof,
                ...tx
            });
        }

        const starknetTxs: StarknetTx[] = [];
        const storedHeaders = await StarknetBtcRelay.getCommitedHeadersAndSynchronize(
            signer, this.btcRelay, txsWithMerkleProofs.filter(tx => tx.storedHeader==null).map(tx => {
                return {
                    blockhash: tx.tx.btcTx.blockhash!,
                    blockheight: tx.blockheight,
                    requiredConfirmations: vault.getConfirmations()
                }
            }), starknetTxs, synchronizer, feeRate
        );
        if(storedHeaders==null) throw new Error("Cannot fetch committed header!");

        const actions = txsWithMerkleProofs.map(tx => {
            return this.Claim(signer, vault, tx.tx, tx.storedHeader ?? storedHeaders[tx.tx.btcTx.blockhash!], tx.merkle, tx.pos);
        });

        let starknetAction = new StarknetAction(signer, this.Chain);
        for(let action of actions) {
            starknetAction.add(action);
            if(starknetAction.ixsLength() >= this.maxClaimsPerTx) {
                await starknetAction.addToTxs(starknetTxs, feeRate);
                starknetAction = new StarknetAction(signer, this.Chain);
            }
        }
        if(starknetAction.ixsLength() > 0) {
            await starknetAction.addToTxs(starknetTxs, feeRate);
        }

        this.logger.debug("txsClaim(): "+starknetTxs.length+" claim TXs created claiming "+txs.length+" txs, owner: "+vault.getOwner()+
            " vaultId: "+vault.getVaultId().toString(10));

        return starknetTxs;
    }

    /**
     * @inheritDoc
     */
    async txsDeposit(signer: string, vault: StarknetSpvVaultData, rawAmounts: bigint[], feeRate?: string): Promise<StarknetTx[]> {
        if(!vault.isOpened()) throw new Error("Cannot deposit to a closed vault!");
        //Approve first
        const vaultTokens = vault.getTokenData();
        const action = new StarknetAction(signer, this.Chain);
        let realAmount0 = 0n;
        let realAmount1 = 0n;
        if(rawAmounts[0]!=null && rawAmounts[0]!==0n) {
            realAmount0 = rawAmounts[0] * vaultTokens[0].multiplier;
            action.add(this.Chain.Tokens.Approve(signer, this.contract.address, vaultTokens[0].token, realAmount0));
        }
        if(rawAmounts[1]!=null && rawAmounts[1]!==0n) {
            realAmount1 = rawAmounts[1] * vaultTokens[1].multiplier;
            action.add(this.Chain.Tokens.Approve(signer, this.contract.address, vaultTokens[1].token, realAmount1));
        }
        action.add(this.Deposit(signer, vault, rawAmounts));

        feeRate ??= await this.Chain.Fees.getFeeRate();

        this.logger.debug("txsDeposit(): deposit TX created,"+
            " token0: "+vaultTokens[0].token+" rawAmount0: "+rawAmounts[0].toString(10)+" amount0: "+realAmount0.toString(10)+
            " token1: "+vaultTokens[1].token+" rawAmount1: "+(rawAmounts[1] ?? 0n).toString(10)+" amount1: "+realAmount1.toString(10));

        return [await action.tx(feeRate)];
    }

    /**
     * @inheritDoc
     */
    async txsFrontLiquidity(signer: string, vault: StarknetSpvVaultData, realWithdrawalTx: StarknetSpvWithdrawalData, withdrawSequence: number, feeRate?: string): Promise<StarknetTx[]> {
        if(!vault.isOpened()) throw new Error("Cannot front on a closed vault!");

        //Approve first
        const vaultTokens = vault.getTokenData();
        const action = new StarknetAction(signer, this.Chain);
        const rawAmounts = realWithdrawalTx.getFrontingAmount();
        let realAmount0 = 0n;
        let realAmount1 = 0n;
        if(rawAmounts[0]!=null && rawAmounts[0]!==0n) {
            realAmount0 = rawAmounts[0] * vaultTokens[0].multiplier;
            action.add(this.Chain.Tokens.Approve(signer, this.contract.address, vaultTokens[0].token, realAmount0));
        }
        if(rawAmounts[1]!=null && rawAmounts[1]!==0n) {
            realAmount1 = rawAmounts[1] * vaultTokens[1].multiplier;
            action.add(this.Chain.Tokens.Approve(signer, this.contract.address, vaultTokens[1].token, realAmount1));
        }
        action.add(this.Front(signer, vault, realWithdrawalTx, withdrawSequence));

        feeRate ??= await this.Chain.Fees.getFeeRate();

        this.logger.debug("txsFrontLiquidity(): front TX created,"+
            " token0: "+vaultTokens[0].token+" rawAmount0: "+rawAmounts[0].toString(10)+" amount0: "+realAmount0.toString(10)+
            " token1: "+vaultTokens[1].token+" rawAmount1: "+(rawAmounts[1] ?? 0n).toString(10)+" amount1: "+realAmount1.toString(10));

        return [await action.tx(feeRate)];
    }

    /**
     * @inheritDoc
     */
    async txsOpen(signer: string, vault: StarknetSpvVaultData, feeRate?: string): Promise<StarknetTx[]> {
        if(vault.isOpened()) throw new Error("Cannot open an already opened vault!");

        const action = this.Open(signer, vault);

        feeRate ??= await this.Chain.Fees.getFeeRate();

        this.logger.debug("txsOpen(): open TX created, owner: "+vault.getOwner()+
            " vaultId: "+vault.getVaultId().toString(10));

        return [await action.tx(feeRate)];
    }

    /**
     * @inheritDoc
     */
    async getClaimFee(signer: string, vault: StarknetSpvVaultData, withdrawalData: StarknetSpvWithdrawalData, feeRate?: string): Promise<bigint> {
        feeRate ??= await this.Chain.Fees.getFeeRate();
        return StarknetFees.getGasFee(
            withdrawalData==null ? StarknetSpvVaultContract.GasCosts.CLAIM_OPTIMISTIC_ESTIMATE : StarknetSpvVaultContract.GasCosts.CLAIM,
            feeRate
        );
    }

    /**
     * @inheritDoc
     */
    async getFrontFee(signer: string, vault: StarknetSpvVaultData, withdrawalData: StarknetSpvWithdrawalData, feeRate?: string): Promise<bigint> {
        feeRate ??= await this.Chain.Fees.getFeeRate();
        return StarknetFees.getGasFee(StarknetSpvVaultContract.GasCosts.FRONT, feeRate);
    }

}
