"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BundleDataClient = void 0;
const tslib_1 = require("tslib");
const assert_1 = tslib_1.__importDefault(require("assert"));
const lodash_1 = tslib_1.__importDefault(require("lodash"));
const superstruct_1 = require("superstruct");
const interfaces_1 = require("../../interfaces");
const evm_1 = require("../../arch/evm");
const svm_1 = require("../../arch/svm");
const tvm_1 = require("../../arch/tvm");
const utils_1 = require("../../utils");
const utils_2 = require("./utils");
const SpokePoolClient_1 = require("../SpokePoolClient");
const SpokePoolClientManager_1 = require("../SpokePoolClient/SpokePoolClientManager");
const constants_1 = require("../../constants");
function updateExpiredDepositsV3(dict, deposit) {
    if (!deposit.depositor.isValidOn(deposit.originChainId)) {
        return;
    }
    const { originChainId, inputToken } = deposit;
    dict[originChainId] ??= {};
    dict[originChainId][inputToken.toBytes32()] ??= [];
    dict[originChainId][inputToken.toBytes32()].push(deposit);
}
function updateBundleDepositsV3(dict, deposit) {
    const { originChainId, inputToken } = deposit;
    dict[originChainId] ??= {};
    dict[originChainId][inputToken.toBytes32()] ??= [];
    dict[originChainId][inputToken.toBytes32()].push(deposit);
}
function updateBundleFillsV3(dict, fill, lpFeePct, repaymentChainId, repaymentToken, repaymentAddress) {
    if (!fill.relayer.isValidOn(repaymentChainId)) {
        return;
    }
    dict[repaymentChainId] ??= {};
    dict[repaymentChainId][repaymentToken.toBytes32()] ??= {
        fills: [],
        totalRefundAmount: utils_1.bnZero,
        realizedLpFees: utils_1.bnZero,
        refunds: {},
    };
    const bundleFill = { ...fill, lpFeePct, relayer: repaymentAddress };
    (0, utils_1.assign)(dict, [repaymentChainId, repaymentToken.toBytes32(), "fills"], [bundleFill]);
    const refundObj = dict[repaymentChainId][repaymentToken.toBytes32()];
    const realizedLpFee = bundleFill.inputAmount.mul(bundleFill.lpFeePct).div(utils_1.fixedPointAdjustment);
    refundObj.realizedLpFees = refundObj.realizedLpFees ? refundObj.realizedLpFees.add(realizedLpFee) : realizedLpFee;
    if (!(0, utils_1.isSlowFill)(bundleFill)) {
        const refundAmount = bundleFill.inputAmount.mul(utils_1.fixedPointAdjustment.sub(lpFeePct)).div(utils_1.fixedPointAdjustment);
        refundObj.totalRefundAmount = refundObj.totalRefundAmount
            ? refundObj.totalRefundAmount.add(refundAmount)
            : refundAmount;
        refundObj.refunds ??= {};
        if (refundObj.refunds[bundleFill.relayer.toBytes32()]) {
            refundObj.refunds[bundleFill.relayer.toBytes32()] =
                refundObj.refunds[bundleFill.relayer.toBytes32()].add(refundAmount);
        }
        else {
            refundObj.refunds[bundleFill.relayer.toBytes32()] = refundAmount;
        }
    }
}
function updateBundleExcessSlowFills(dict, deposit) {
    const { destinationChainId, outputToken } = deposit;
    dict[destinationChainId] ??= {};
    dict[destinationChainId][outputToken.toBytes32()] ??= [];
    dict[destinationChainId][outputToken.toBytes32()].push(deposit);
}
function updateBundleSlowFills(dict, deposit) {
    if (!deposit.recipient.isValidOn(deposit.destinationChainId)) {
        return;
    }
    const { destinationChainId, outputToken } = deposit;
    dict[destinationChainId] ??= {};
    dict[destinationChainId][outputToken.toBytes32()] ??= [];
    dict[destinationChainId][outputToken.toBytes32()].push(deposit);
}
class BundleDataClient {
    logger;
    clients;
    chainIdListForBundleEvaluationBlockNumbers;
    blockRangeEndBlockBuffer;
    arweaveTopicCache;
    loadDataCache = {};
    arweaveDataCache = {};
    bundleTimestampCache = {};
    spokePoolClientManager;
    constructor(logger, clients, spokePoolClients, chainIdListForBundleEvaluationBlockNumbers, blockRangeEndBlockBuffer = {}, arweaveTopicCache) {
        this.logger = logger;
        this.clients = clients;
        this.chainIdListForBundleEvaluationBlockNumbers = chainIdListForBundleEvaluationBlockNumbers;
        this.blockRangeEndBlockBuffer = blockRangeEndBlockBuffer;
        this.arweaveTopicCache = arweaveTopicCache;
        this.spokePoolClientManager = new SpokePoolClientManager_1.SpokePoolManager(logger, spokePoolClients);
    }
    clearCache() {
        this.loadDataCache = {};
    }
    async loadDataFromCache(key) {
        return lodash_1.default.cloneDeep(await this.loadDataCache[key]);
    }
    getBundleTimestampsFromCache(key) {
        if (this.bundleTimestampCache[key]) {
            return lodash_1.default.cloneDeep(this.bundleTimestampCache[key]);
        }
        return undefined;
    }
    setBundleTimestampsInCache(key, timestamps) {
        this.bundleTimestampCache[key] = timestamps;
    }
    static getArweaveClientKey(blockRangesForChains) {
        return blockRangesForChains[0][1].toString();
    }
    getArweaveBundleDataClientKey(blockRangesForChains) {
        return `bundles-${BundleDataClient.getArweaveClientKey(blockRangesForChains)}`;
    }
    async getPersistedArweaveTopicFromCache(tag) {
        if (!(0, utils_1.isDefined)(this.arweaveTopicCache)) {
            return undefined;
        }
        try {
            const cachedPayload = await this.arweaveTopicCache.get((0, utils_1.getArweaveTopicCacheKey)(tag));
            if (!(0, utils_1.isDefined)(cachedPayload)) {
                return undefined;
            }
            return (0, superstruct_1.create)(JSON.parse(cachedPayload), utils_2.BundleDataSS);
        }
        catch (error) {
            this.logger.debug({
                at: "BundleDataClient#getPersistedArweaveTopicFromCache",
                message: "Failed to restore persisted bundle data from topic cache, falling back to Arweave",
                tag,
                error: String(error),
            });
            return undefined;
        }
    }
    async backfillPersistedArweaveTopicCache(tag, data) {
        if (!(0, utils_1.isDefined)(this.arweaveTopicCache)) {
            return;
        }
        try {
            await this.arweaveTopicCache.set((0, utils_1.getArweaveTopicCacheKey)(tag), JSON.stringify(data, utils_1.jsonReplacerWithBigNumbers), constants_1.DEFAULT_CACHING_TTL);
        }
        catch (error) {
            this.logger.debug({
                at: "BundleDataClient#backfillPersistedArweaveTopicCache",
                message: "Failed to backfill persisted bundle data into topic cache",
                tag,
                error: String(error),
            });
        }
    }
    async loadPersistedDataFromArweave(blockRangesForChains) {
        if (!(0, utils_1.isDefined)(this.clients?.arweaveClient)) {
            return undefined;
        }
        const start = performance.now();
        const tag = this.getArweaveBundleDataClientKey(blockRangesForChains);
        let data = await this.getPersistedArweaveTopicFromCache(tag);
        if (!(0, utils_1.isDefined)(data)) {
            const persistedData = await this.clients.arweaveClient.getByTopic(tag, utils_2.BundleDataSS);
            if (!(0, utils_1.isDefined)(persistedData) || persistedData.length < 1) {
                return undefined;
            }
            data = persistedData[0].data;
            await this.backfillPersistedArweaveTopicCache(tag, data);
        }
        else {
            this.logger.debug({
                at: "BundleDataClient#loadPersistedDataFromArweave",
                message: "Loaded persisted bundle data from topic cache",
                blockRanges: JSON.stringify(blockRangesForChains),
                tag,
            });
        }
        const convertTypedStringRecordIntoNumericRecord = (data) => Object.keys(data).reduce((acc, chainId) => {
            acc[Number(chainId)] = data[chainId];
            return acc;
        }, {});
        const convertSortableEventFieldsIntoRequiredFields = (data) => {
            return data.map((item) => {
                if ((item.txnIndex !== undefined && item.transactionIndex !== undefined) ||
                    (item.txnIndex === undefined && item.transactionIndex === undefined)) {
                    throw new Error("Either txnIndex or transactionIndex must be defined, but not both.");
                }
                if ((item.txnRef !== undefined && item.transactionHash !== undefined) ||
                    (item.txnRef === undefined && item.transactionHash === undefined)) {
                    throw new Error("Either txnRef or transactionHash must be defined, but not both.");
                }
                const { txnIndex, transactionIndex, txnRef, transactionHash, ...rest } = item;
                return {
                    ...rest,
                    txnIndex: txnIndex ?? transactionIndex,
                    txnRef: txnRef ?? transactionHash,
                };
            });
        };
        const convertEmbeddedSortableEventFieldsIntoRequiredFields = (data) => {
            return Object.fromEntries(Object.entries(data).map(([chainId, tokenData]) => [
                chainId,
                Object.fromEntries(Object.entries(tokenData).map(([token, data]) => [
                    token,
                    convertSortableEventFieldsIntoRequiredFields(data),
                ])),
            ]));
        };
        const bundleData = {
            bundleFillsV3: convertTypedStringRecordIntoNumericRecord(Object.fromEntries(Object.entries(data.bundleFillsV3).map(([chainId, tokenData]) => [
                chainId,
                Object.fromEntries(Object.entries(tokenData).map(([token, data]) => [
                    token,
                    {
                        refunds: Object.fromEntries(Object.entries(data.refunds).map(([refundAddress, refund]) => [(0, utils_1.toBytes32)(refundAddress), refund])),
                        totalRefundAmount: data.totalRefundAmount,
                        realizedLpFees: data.realizedLpFees,
                        fills: convertSortableEventFieldsIntoRequiredFields(data.fills),
                    },
                ])),
            ]))),
            expiredDepositsToRefundV3: convertTypedStringRecordIntoNumericRecord(convertEmbeddedSortableEventFieldsIntoRequiredFields(data.expiredDepositsToRefundV3)),
            bundleDepositsV3: convertTypedStringRecordIntoNumericRecord(convertEmbeddedSortableEventFieldsIntoRequiredFields(data.bundleDepositsV3)),
            unexecutableSlowFills: convertTypedStringRecordIntoNumericRecord(convertEmbeddedSortableEventFieldsIntoRequiredFields(data.unexecutableSlowFills)),
            bundleSlowFillsV3: convertTypedStringRecordIntoNumericRecord(convertEmbeddedSortableEventFieldsIntoRequiredFields(data.bundleSlowFillsV3)),
        };
        this.logger.debug({
            at: "BundleDataClient#loadPersistedDataFromArweave",
            message: `Loaded persisted data from Arweave in ${Math.round(performance.now() - start) / 1000}s.`,
            blockRanges: JSON.stringify(blockRangesForChains),
            bundleData: (0, utils_2.prettyPrintV3SpokePoolEvents)(bundleData.bundleDepositsV3, bundleData.bundleFillsV3, bundleData.bundleSlowFillsV3, bundleData.expiredDepositsToRefundV3, bundleData.unexecutableSlowFills),
        });
        return bundleData;
    }
    async loadArweaveData(blockRangesForChains) {
        const arweaveKey = this.getArweaveBundleDataClientKey(blockRangesForChains);
        if (!this.arweaveDataCache[arweaveKey]) {
            this.arweaveDataCache[arweaveKey] = this.loadPersistedDataFromArweave(blockRangesForChains).catch((error) => {
                delete this.arweaveDataCache[arweaveKey];
                throw error;
            });
        }
        const arweaveData = lodash_1.default.cloneDeep(await this.arweaveDataCache[arweaveKey]);
        return arweaveData;
    }
    async loadData(blockRangesForChains, spokePoolClients, attemptArweaveLoad = false) {
        const key = JSON.stringify(blockRangesForChains);
        if (!this.loadDataCache[key]) {
            let arweaveData;
            if (attemptArweaveLoad) {
                try {
                    arweaveData = await this.loadArweaveData(blockRangesForChains);
                }
                catch (error) {
                    this.logger.warn({
                        at: "BundleDataClient#loadData",
                        message: "Failed to load bundle data from Arweave, falling back to on-chain reconstruction",
                        blockRanges: JSON.stringify(blockRangesForChains),
                        error: String(error),
                    });
                }
            }
            const data = (0, utils_1.isDefined)(arweaveData)
                ?
                    Promise.resolve(arweaveData)
                : this.loadDataFromScratch(blockRangesForChains, spokePoolClients);
            this.loadDataCache[key] = data;
        }
        return this.loadDataFromCache(key);
    }
    async loadDataFromScratch(blockRangesForChains, spokePoolClients) {
        let start = performance.now();
        const key = JSON.stringify(blockRangesForChains);
        if (!this.clients.configStoreClient.isUpdated) {
            throw new Error("ConfigStoreClient not updated");
        }
        else if (!this.clients.hubPoolClient.isUpdated) {
            throw new Error("HubPoolClient not updated");
        }
        const [bundleStartBlockForMainnet, bundleEndBlockForMainnet] = blockRangesForChains[0];
        const chainIds = this.clients.configStoreClient.getChainIdIndicesForBlock(bundleStartBlockForMainnet);
        if (blockRangesForChains.length > chainIds.length) {
            throw new Error(`Unexpected block range list length of ${blockRangesForChains.length}, should be <= ${chainIds.length}`);
        }
        const bundleDepositsV3 = {};
        const bundleFillsV3 = {};
        const bundleInvalidFillsV3 = [];
        const bundleUnrepayableFillsV3 = [];
        const bundleInvalidSlowFillRequests = [];
        const bundleSlowFillsV3 = {};
        const expiredDepositsToRefundV3 = {};
        const unexecutableSlowFills = {};
        const _isChainDisabled = (chainId) => {
            return (0, utils_2.isChainDisabledAtBlock)(chainId, bundleStartBlockForMainnet, this.clients.configStoreClient);
        };
        const _isChainPaused = (chainId) => {
            const blockRangeForChain = (0, utils_1.getBlockRangeForChain)(blockRangesForChains, chainId, chainIds);
            return (0, utils_2.isChainPaused)(blockRangeForChain);
        };
        const _canCreateSlowFillLeaf = (deposit) => {
            return (this.clients.hubPoolClient.areTokensEquivalent(deposit.inputToken, deposit.originChainId, deposit.outputToken, deposit.destinationChainId, bundleEndBlockForMainnet) &&
                !deposit.fromLiteChain &&
                !deposit.toLiteChain);
        };
        const _depositIsExpired = (deposit) => {
            return deposit.fillDeadline <= bundleBlockTimestamps[deposit.destinationChainId][1];
        };
        const _getFillStatusForDeposit = (deposit, queryBlock) => {
            return spokePoolClients[deposit.destinationChainId].relayFillStatus(deposit, Math.min(queryBlock, spokePoolClients[deposit.destinationChainId].latestHeightSearched));
        };
        const allChainIds = blockRangesForChains
            .map((_blockRange, index) => chainIds[index])
            .filter((chainId) => !_isChainDisabled(chainId) && spokePoolClients[chainId] !== undefined);
        allChainIds.forEach((chainId) => {
            const spokePoolClient = spokePoolClients[chainId];
            if (!spokePoolClient.isUpdated) {
                throw new Error(`SpokePoolClient for chain ${chainId} not updated.`);
            }
        });
        const _cachedBundleTimestamps = this.getBundleTimestampsFromCache(key);
        let bundleBlockTimestamps = {};
        if (!_cachedBundleTimestamps) {
            bundleBlockTimestamps = await this.getBundleBlockTimestamps(chainIds, blockRangesForChains, spokePoolClients);
            this.setBundleTimestampsInCache(key, bundleBlockTimestamps);
            this.logger.debug({
                at: "BundleDataClient#loadData",
                message: "Bundle block timestamps",
                bundleBlockTimestamps,
                blockRangesForChains: JSON.stringify(blockRangesForChains),
            });
        }
        else {
            bundleBlockTimestamps = _cachedBundleTimestamps;
        }
        const v3RelayHashes = {};
        const bundleDepositHashes = [];
        const olderDepositHashes = [];
        const decodeBundleDepositHash = (depositHash) => {
            const [relayDataHash, i] = depositHash.split("@");
            return { relayDataHash, index: Number(i) };
        };
        for (const originChainId of allChainIds) {
            const originClient = spokePoolClients[originChainId];
            const originChainBlockRange = (0, utils_1.getBlockRangeForChain)(blockRangesForChains, originChainId, chainIds);
            for (const destinationChainId of allChainIds) {
                originClient.getDepositsForDestinationChainWithDuplicates(destinationChainId).forEach((deposit) => {
                    if (deposit.blockNumber > originChainBlockRange[1] || (0, utils_1.isZeroValueDeposit)(deposit)) {
                        return;
                    }
                    const relayDataHash = (0, utils_1.getRelayEventKey)(deposit);
                    if (!v3RelayHashes[relayDataHash]) {
                        v3RelayHashes[relayDataHash] = {
                            deposits: [deposit],
                            fill: undefined,
                            slowFillRequest: undefined,
                        };
                    }
                    else {
                        const { deposits } = v3RelayHashes[relayDataHash];
                        (0, assert_1.default)((0, utils_1.isDefined)(deposits) && deposits.length > 0, "Deposit should exist in relay hash dictionary.");
                        deposits.push(deposit);
                    }
                    const { deposits } = v3RelayHashes[relayDataHash];
                    (0, assert_1.default)((0, utils_1.isDefined)(deposits) && deposits.length > 0, "Deposit should exist in relay hash dictionary.");
                    const newBundleDepositHash = `${relayDataHash}@${deposits.length - 1}`;
                    const decodedBundleDepositHash = decodeBundleDepositHash(newBundleDepositHash);
                    (0, assert_1.default)(decodedBundleDepositHash.relayDataHash === relayDataHash &&
                        decodedBundleDepositHash.index === deposits.length - 1, "Not using correct bundle deposit hash key");
                    if (deposit.blockNumber >= originChainBlockRange[0]) {
                        if (bundleDepositsV3?.[originChainId]?.[deposit.inputToken.toBytes32()]?.find((d) => (0, utils_1.duplicateEvent)(deposit, d))) {
                            this.logger.debug({
                                at: "BundleDataClient#loadData",
                                message: "Duplicate deposit detected",
                                deposit: (0, utils_1.convertRelayDataParamsToBytes32)(deposit),
                            });
                            throw new Error("Duplicate deposit detected");
                        }
                        if (!_isChainPaused(originChainId)) {
                            bundleDepositHashes.push(newBundleDepositHash);
                            updateBundleDepositsV3(bundleDepositsV3, deposit);
                        }
                    }
                    else if (deposit.blockNumber < originChainBlockRange[0]) {
                        olderDepositHashes.push(newBundleDepositHash);
                    }
                });
            }
        }
        this.logger.debug({
            at: "BundleDataClient#loadData",
            message: `Processed ${bundleDepositHashes.length + olderDepositHashes.length} deposits in ${performance.now() - start}ms.`,
        });
        start = performance.now();
        const validatedBundleV3Fills = [];
        const validatedBundleSlowFills = [];
        const validatedBundleUnexecutableSlowFills = [];
        let fillCounter = 0;
        const allFillChainIds = allChainIds.filter((chainId) => !_isChainPaused(chainId));
        for (const originChainId of allFillChainIds) {
            const originClient = spokePoolClients[originChainId];
            for (const destinationChainId of allFillChainIds) {
                const destinationClient = spokePoolClients[destinationChainId];
                const destinationChainBlockRange = (0, utils_1.getBlockRangeForChain)(blockRangesForChains, destinationChainId, chainIds);
                const originChainBlockRange = (0, utils_1.getBlockRangeForChain)(blockRangesForChains, originChainId, chainIds);
                const fastFillsReplacingSlowFills = [];
                await (0, utils_1.forEachAsync)(destinationClient
                    .getFillsForOriginChain(originChainId)
                    .filter((fill) => fill.blockNumber <= destinationChainBlockRange[1] &&
                    !(0, utils_1.isZeroValueFillOrSlowFillRequest)(fill) &&
                    !(0, utils_1.invalidOutputToken)(fill)), async (fill) => {
                    fillCounter++;
                    const relayDataHash = (0, utils_1.getRelayEventKey)(fill);
                    if (v3RelayHashes[relayDataHash]) {
                        if (!v3RelayHashes[relayDataHash].fill) {
                            const { deposits } = v3RelayHashes[relayDataHash];
                            (0, assert_1.default)((0, utils_1.isDefined)(deposits) && deposits.length > 0, "Deposit should exist in relay hash dictionary.");
                            v3RelayHashes[relayDataHash].fill = fill;
                            if (fill.blockNumber >= destinationChainBlockRange[0]) {
                                let provider;
                                if ((0, SpokePoolClient_1.isEVMSpokePoolClient)(destinationClient)) {
                                    provider = destinationClient.spokePool.provider;
                                }
                                else if ((0, SpokePoolClient_1.isSVMSpokePoolClient)(destinationClient)) {
                                    provider = destinationClient.svmEventsClient.getRpc();
                                }
                                const fillToRefund = await (0, utils_2.verifyFillRepayment)(fill, provider, deposits[0], this.clients.hubPoolClient, bundleEndBlockForMainnet);
                                if (!(0, utils_1.isDefined)(fillToRefund)) {
                                    bundleUnrepayableFillsV3.push(fill);
                                }
                                else {
                                    v3RelayHashes[relayDataHash].fill = fillToRefund;
                                    validatedBundleV3Fills.push({
                                        ...fillToRefund,
                                        quoteTimestamp: deposits[0].quoteTimestamp,
                                    });
                                    const duplicateDeposits = deposits.slice(1);
                                    duplicateDeposits.forEach((duplicateDeposit) => {
                                        if ((0, utils_1.isSlowFill)(fill)) {
                                            updateExpiredDepositsV3(expiredDepositsToRefundV3, duplicateDeposit);
                                        }
                                        else {
                                            validatedBundleV3Fills.push({
                                                ...fillToRefund,
                                                quoteTimestamp: duplicateDeposit.quoteTimestamp,
                                            });
                                        }
                                    });
                                }
                                if (fill.relayExecutionInfo.fillType === interfaces_1.FillType.ReplacedSlowFill &&
                                    _canCreateSlowFillLeaf(deposits[0])) {
                                    fastFillsReplacingSlowFills.push(relayDataHash);
                                }
                            }
                        }
                        else {
                            this.logger.debug({
                                at: "BundleDataClient#loadData",
                                message: "Duplicate fill detected",
                                fill: (0, utils_1.convertFillParamsToBytes32)(fill),
                            });
                            throw new Error("Duplicate fill detected");
                        }
                        return;
                    }
                    v3RelayHashes[relayDataHash] = {
                        deposits: undefined,
                        fill,
                        slowFillRequest: undefined,
                    };
                    if (fill.blockNumber < destinationChainBlockRange[0]) {
                        return;
                    }
                    const historicalDeposit = await (0, utils_1.queryHistoricalDepositForFill)(originClient, fill);
                    if (!historicalDeposit.found) {
                        bundleInvalidFillsV3.push(fill);
                        return;
                    }
                    const matchedDeposit = historicalDeposit.deposit;
                    if (matchedDeposit.blockNumber > originChainBlockRange[1]) {
                        bundleInvalidFillsV3.push(fill);
                        return;
                    }
                    v3RelayHashes[relayDataHash].deposits = [matchedDeposit];
                    let provider;
                    if ((0, SpokePoolClient_1.isEVMSpokePoolClient)(destinationClient)) {
                        provider = destinationClient.spokePool.provider;
                    }
                    else if ((0, SpokePoolClient_1.isSVMSpokePoolClient)(destinationClient)) {
                        provider = destinationClient.svmEventsClient.getRpc();
                    }
                    const fillToRefund = await (0, utils_2.verifyFillRepayment)(fill, provider, matchedDeposit, this.clients.hubPoolClient, bundleEndBlockForMainnet);
                    if (!(0, utils_1.isDefined)(fillToRefund)) {
                        bundleUnrepayableFillsV3.push(fill);
                    }
                    else {
                        (0, assert_1.default)((0, utils_1.getRelayEventKey)(matchedDeposit) === relayDataHash, "Relay hashes should match.");
                        validatedBundleV3Fills.push({
                            ...fillToRefund,
                            quoteTimestamp: matchedDeposit.quoteTimestamp,
                        });
                        v3RelayHashes[relayDataHash].fill = fillToRefund;
                    }
                    if (fill.relayExecutionInfo.fillType === interfaces_1.FillType.ReplacedSlowFill &&
                        _canCreateSlowFillLeaf(matchedDeposit)) {
                        fastFillsReplacingSlowFills.push(relayDataHash);
                    }
                });
                await (0, utils_1.forEachAsync)(destinationClient
                    .getSlowFillRequestsForOriginChain(originChainId)
                    .filter((request) => request.blockNumber <= destinationChainBlockRange[1] &&
                    !(0, utils_1.isZeroValueFillOrSlowFillRequest)(request) &&
                    !(0, utils_1.invalidOutputToken)(request)), async (slowFillRequest) => {
                    const relayDataHash = (0, utils_1.getRelayEventKey)(slowFillRequest);
                    if (v3RelayHashes[relayDataHash]) {
                        if (!v3RelayHashes[relayDataHash].slowFillRequest) {
                            v3RelayHashes[relayDataHash].slowFillRequest = slowFillRequest;
                            const { deposits, fill } = v3RelayHashes[relayDataHash];
                            if (fill) {
                                return;
                            }
                            (0, assert_1.default)((0, utils_1.isDefined)(deposits) && deposits.length > 0, "Deposit should exist in relay hash dictionary.");
                            const matchedDeposit = deposits[0];
                            if (slowFillRequest.blockNumber >= destinationChainBlockRange[0] &&
                                _canCreateSlowFillLeaf(matchedDeposit) &&
                                !_depositIsExpired(matchedDeposit)) {
                                validatedBundleSlowFills.push(matchedDeposit);
                            }
                        }
                        else {
                            this.logger.debug({
                                at: "BundleDataClient#loadData",
                                message: "Duplicate slow fill request detected",
                                slowFillRequest,
                            });
                            throw new Error("Duplicate slow fill request detected.");
                        }
                        return;
                    }
                    v3RelayHashes[relayDataHash] = {
                        deposits: undefined,
                        fill: undefined,
                        slowFillRequest: slowFillRequest,
                    };
                    if (slowFillRequest.blockNumber < destinationChainBlockRange[0]) {
                        return;
                    }
                    const historicalDeposit = await (0, utils_1.queryHistoricalDepositForFill)(originClient, slowFillRequest);
                    if (!historicalDeposit.found) {
                        bundleInvalidSlowFillRequests.push(slowFillRequest);
                        return;
                    }
                    const matchedDeposit = historicalDeposit.deposit;
                    if (matchedDeposit.blockNumber > originChainBlockRange[1]) {
                        bundleInvalidSlowFillRequests.push(slowFillRequest);
                        return;
                    }
                    (0, assert_1.default)((0, utils_1.getRelayEventKey)(matchedDeposit) === relayDataHash, "Deposit relay hashes should match.");
                    v3RelayHashes[relayDataHash].deposits = [matchedDeposit];
                    if (!_canCreateSlowFillLeaf(matchedDeposit) || _depositIsExpired(matchedDeposit)) {
                        return;
                    }
                    validatedBundleSlowFills.push(matchedDeposit);
                });
                await (0, utils_1.mapAsync)(bundleDepositHashes, async (depositHash) => {
                    const { relayDataHash, index } = decodeBundleDepositHash(depositHash);
                    const { deposits, fill, slowFillRequest } = v3RelayHashes[relayDataHash];
                    if (!deposits || deposits.length === 0) {
                        throw new Error("Deposits should exist in relay hash dictionary.");
                    }
                    const deposit = deposits[index];
                    if (!deposit)
                        throw new Error("Deposit should exist in relay hash dictionary.");
                    if (deposit.originChainId !== originChainId || deposit.destinationChainId !== destinationChainId) {
                        return;
                    }
                    if (fill) {
                        if (fill.blockNumber < destinationChainBlockRange[0]) {
                            let provider;
                            if ((0, SpokePoolClient_1.isEVMSpokePoolClient)(destinationClient)) {
                                provider = destinationClient.spokePool.provider;
                            }
                            else if ((0, SpokePoolClient_1.isSVMSpokePoolClient)(destinationClient)) {
                                provider = destinationClient.svmEventsClient.getRpc();
                            }
                            const fillToRefund = await (0, utils_2.verifyFillRepayment)(fill, provider, deposits[0], this.clients.hubPoolClient, bundleEndBlockForMainnet);
                            if (!(0, utils_1.isDefined)(fillToRefund)) {
                                bundleUnrepayableFillsV3.push(fill);
                            }
                            else if (!(0, utils_1.isSlowFill)(fill)) {
                                v3RelayHashes[relayDataHash].fill = fillToRefund;
                                validatedBundleV3Fills.push({
                                    ...fillToRefund,
                                    quoteTimestamp: deposit.quoteTimestamp,
                                });
                            }
                            else {
                                updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit);
                            }
                        }
                        return;
                    }
                    if (slowFillRequest) {
                        if (_depositIsExpired(deposit)) {
                            updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit);
                        }
                        else if (slowFillRequest.blockNumber < destinationChainBlockRange[0] &&
                            _canCreateSlowFillLeaf(deposit) &&
                            validatedBundleSlowFills.every((d) => (0, utils_1.getRelayEventKey)(d) !== relayDataHash)) {
                            validatedBundleSlowFills.push(deposit);
                        }
                        return;
                    }
                    const fillStatus = await _getFillStatusForDeposit(deposit, destinationChainBlockRange[1]);
                    if (fillStatus === interfaces_1.FillStatus.Filled) {
                        const prefill = await this.findMatchingFillEvent(deposit, destinationClient);
                        (0, assert_1.default)((0, utils_1.isDefined)(prefill), `findFillEvent# Cannot find prefill: ${relayDataHash}`);
                        (0, assert_1.default)((0, utils_1.getRelayEventKey)(prefill) === relayDataHash, "Relay hashes should match.");
                        let provider;
                        if ((0, SpokePoolClient_1.isEVMSpokePoolClient)(destinationClient)) {
                            provider = destinationClient.spokePool.provider;
                        }
                        else if ((0, SpokePoolClient_1.isSVMSpokePoolClient)(destinationClient)) {
                            provider = destinationClient.svmEventsClient.getRpc();
                        }
                        const verifiedFill = await (0, utils_2.verifyFillRepayment)(prefill, provider, deposit, this.clients.hubPoolClient, bundleEndBlockForMainnet);
                        if (!(0, utils_1.isDefined)(verifiedFill)) {
                            bundleUnrepayableFillsV3.push(prefill);
                        }
                        else if (!(0, utils_1.isSlowFill)(verifiedFill)) {
                            validatedBundleV3Fills.push({
                                ...verifiedFill,
                                quoteTimestamp: deposit.quoteTimestamp,
                            });
                        }
                        else {
                            updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit);
                        }
                    }
                    else if (_depositIsExpired(deposit)) {
                        updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit);
                    }
                    else if (fillStatus === interfaces_1.FillStatus.RequestedSlowFill &&
                        validatedBundleSlowFills.every((d) => (0, utils_1.getRelayEventKey)(d) !== relayDataHash)) {
                        if (_canCreateSlowFillLeaf(deposit)) {
                            validatedBundleSlowFills.push(deposit);
                        }
                    }
                });
                fastFillsReplacingSlowFills.forEach((relayDataHash) => {
                    const { deposits, slowFillRequest, fill } = v3RelayHashes[relayDataHash];
                    (0, assert_1.default)(fill?.relayExecutionInfo.fillType === interfaces_1.FillType.ReplacedSlowFill, "Fill type should be ReplacedSlowFill.");
                    if (!deposits || deposits.length < 1) {
                        throw new Error("Deposit should exist in relay hash dictionary.");
                    }
                    (0, assert_1.default)(_canCreateSlowFillLeaf(deposits[0]), "fastFillsReplacingSlowFills should contain only deposits that can be slow filled");
                    const destinationBlockRange = (0, utils_1.getBlockRangeForChain)(blockRangesForChains, destinationChainId, chainIds);
                    const originBlockRange = (0, utils_1.getBlockRangeForChain)(blockRangesForChains, originChainId, chainIds);
                    const matchedDeposit = deposits[0];
                    if (matchedDeposit.blockNumber < originBlockRange[0] &&
                        (!slowFillRequest || slowFillRequest.blockNumber < destinationBlockRange[0])) {
                        validatedBundleUnexecutableSlowFills.push(deposits[0]);
                    }
                });
            }
        }
        this.logger.debug({
            at: "BundleDataClient#loadData",
            message: `Processed ${fillCounter} fills in ${performance.now() - start}ms.`,
        });
        start = performance.now();
        await (0, utils_1.forEachAsync)(olderDepositHashes, async (depositHash) => {
            const { relayDataHash, index } = decodeBundleDepositHash(depositHash);
            const { deposits, slowFillRequest, fill } = v3RelayHashes[relayDataHash];
            if (!deposits || deposits.length < 1) {
                throw new Error("Deposit should exist in relay hash dictionary.");
            }
            const deposit = deposits[index];
            const { destinationChainId } = deposit;
            if (_isChainPaused(destinationChainId)) {
                return;
            }
            const destinationBlockRange = (0, utils_1.getBlockRangeForChain)(blockRangesForChains, destinationChainId, chainIds);
            if (!fill &&
                (0, utils_1.isDefined)(deposit) &&
                _depositIsExpired(deposit) &&
                deposit.fillDeadline >= bundleBlockTimestamps[destinationChainId][0] &&
                spokePoolClients[destinationChainId] !== undefined) {
                const fillStatus = await _getFillStatusForDeposit(deposit, destinationBlockRange[1]);
                if (fillStatus !== interfaces_1.FillStatus.Filled) {
                    updateExpiredDepositsV3(expiredDepositsToRefundV3, deposit);
                }
                if (fillStatus !== interfaces_1.FillStatus.RequestedSlowFill || !_canCreateSlowFillLeaf(deposit)) {
                    return;
                }
                if (!slowFillRequest || slowFillRequest.blockNumber < destinationBlockRange[0]) {
                    validatedBundleUnexecutableSlowFills.push(deposit);
                }
            }
        });
        start = performance.now();
        const promises = [
            validatedBundleV3Fills.length > 0
                ? this.clients.hubPoolClient.batchComputeRealizedLpFeePct(validatedBundleV3Fills.map((fill) => {
                    const { deposits } = v3RelayHashes[(0, utils_1.getRelayEventKey)(fill)];
                    (0, assert_1.default)((0, utils_1.isDefined)(deposits) && deposits.length > 0, "Deposit should exist in relay hash dictionary.");
                    const matchedDeposit = deposits[0];
                    (0, assert_1.default)((0, utils_1.isDefined)(matchedDeposit), "Deposit should exist in relay hash dictionary.");
                    const { chainToSendRefundTo: paymentChainId } = (0, utils_2.getRefundInformationFromFill)({
                        ...fill,
                        fromLiteChain: matchedDeposit.fromLiteChain,
                    }, this.clients.hubPoolClient, bundleEndBlockForMainnet);
                    return {
                        ...fill,
                        paymentChainId,
                    };
                }))
                : [],
            validatedBundleSlowFills.length > 0
                ? this.clients.hubPoolClient.batchComputeRealizedLpFeePct(validatedBundleSlowFills.map((deposit) => {
                    return {
                        ...deposit,
                        paymentChainId: deposit.destinationChainId,
                    };
                }))
                : [],
            validatedBundleUnexecutableSlowFills.length > 0
                ? this.clients.hubPoolClient.batchComputeRealizedLpFeePct(validatedBundleUnexecutableSlowFills.map((deposit) => {
                    return {
                        ...deposit,
                        paymentChainId: deposit.destinationChainId,
                    };
                }))
                : [],
        ];
        const [v3FillLpFees, v3SlowFillLpFees, v3UnexecutableSlowFillLpFees] = await Promise.all(promises);
        this.logger.debug({
            at: "BundleDataClient#loadData",
            message: `Computed batch async LP fees in ${performance.now() - start}ms.`,
        });
        v3FillLpFees.forEach(({ realizedLpFeePct }, idx) => {
            const fill = validatedBundleV3Fills[idx];
            const { deposits } = v3RelayHashes[(0, utils_1.getRelayEventKey)(fill)];
            (0, assert_1.default)((0, utils_1.isDefined)(deposits) && deposits.length > 0, "Deposit should exist in relay hash dictionary.");
            const associatedDeposit = deposits[0];
            (0, assert_1.default)((0, utils_1.isDefined)(associatedDeposit), "Deposit should exist in relay hash dictionary.");
            const { chainToSendRefundTo, repaymentToken } = (0, utils_2.getRefundInformationFromFill)({
                ...fill,
                fromLiteChain: associatedDeposit.fromLiteChain,
            }, this.clients.hubPoolClient, bundleEndBlockForMainnet);
            updateBundleFillsV3(bundleFillsV3, fill, realizedLpFeePct, chainToSendRefundTo, repaymentToken, fill.relayer);
        });
        v3SlowFillLpFees.forEach(({ realizedLpFeePct: lpFeePct }, idx) => {
            const deposit = validatedBundleSlowFills[idx];
            const relayDataHash = (0, utils_1.getRelayEventKey)(deposit);
            if (validatedBundleSlowFills.slice(0, idx).some((d) => (0, utils_1.getRelayEventKey)(d) === relayDataHash)) {
                return;
            }
            (0, assert_1.default)(!_depositIsExpired(deposit), "Cannot create slow fill leaf for expired deposit.");
            updateBundleSlowFills(bundleSlowFillsV3, { ...deposit, lpFeePct });
        });
        v3UnexecutableSlowFillLpFees.forEach(({ realizedLpFeePct: lpFeePct }, idx) => {
            const deposit = validatedBundleUnexecutableSlowFills[idx];
            updateBundleExcessSlowFills(unexecutableSlowFills, { ...deposit, lpFeePct });
        });
        const v3SpokeEventsReadable = (0, utils_2.prettyPrintV3SpokePoolEvents)(bundleDepositsV3, bundleFillsV3, bundleSlowFillsV3, expiredDepositsToRefundV3, unexecutableSlowFills);
        if (bundleInvalidFillsV3.length > 0) {
            const invalidFillsWithPartialMatchedDeposits = [];
            const preFillsForNextBundle = [];
            const unknownReasonInvalidFills = [];
            bundleInvalidFillsV3.forEach((fill) => {
                const originClient = spokePoolClients[fill.originChainId];
                const fullyMatchedDeposit = originClient.getDepositForFill(fill);
                if (!(0, utils_1.isDefined)(fullyMatchedDeposit)) {
                    const partiallyMatchedDeposit = originClient.getDeposit(fill.depositId);
                    if ((0, utils_1.isDefined)(partiallyMatchedDeposit)) {
                        invalidFillsWithPartialMatchedDeposits.push(fill);
                    }
                    else {
                        unknownReasonInvalidFills.push(fill);
                    }
                }
                else {
                    const originBundleBlockRange = (0, utils_1.getBlockRangeForChain)(blockRangesForChains, fill.originChainId, chainIds);
                    if (fullyMatchedDeposit.blockNumber <= originBundleBlockRange[1]) {
                        throw new Error("Detected invalid fill that matches fully with a deposit in current or prior bundle");
                    }
                    preFillsForNextBundle.push(fill);
                }
            });
            this.logger.debug({
                at: "BundleDataClient#loadData",
                message: "Finished loading V3 spoke pool data and found some invalid fills in range",
                blockRangesForChains,
                invalidFillsWithPartialMatchedDeposits: invalidFillsWithPartialMatchedDeposits.map((invalidFill) => (0, utils_1.convertFillParamsToBytes32)(invalidFill)),
                preFillsForNextBundle: preFillsForNextBundle.map((preFill) => (0, utils_1.convertFillParamsToBytes32)(preFill)),
                unknownReasonInvalidFills: unknownReasonInvalidFills.map((invalidFill) => (0, utils_1.convertFillParamsToBytes32)(invalidFill)),
            });
        }
        if (bundleUnrepayableFillsV3.length > 0) {
            this.logger.debug({
                at: "BundleDataClient#loadData",
                message: "Finished loading V3 spoke pool data and found some unrepayable fills in range",
                blockRangesForChains,
                bundleUnrepayableFillsV3,
            });
        }
        if (bundleInvalidSlowFillRequests.length > 0) {
            this.logger.debug({
                at: "BundleDataClient#loadData",
                message: "Finished loading V3 spoke pool data and found some invalid slow fill requests in range",
                blockRangesForChains,
                bundleInvalidSlowFillRequests,
            });
        }
        this.logger.debug({
            at: "BundleDataClient#loadDataFromScratch",
            message: `Computed bundle data in ${Math.round(performance.now() - start) / 1000}s.`,
            blockRangesForChains: JSON.stringify(blockRangesForChains),
            v3SpokeEventsReadable,
        });
        return {
            bundleDepositsV3,
            expiredDepositsToRefundV3,
            bundleFillsV3,
            unexecutableSlowFills,
            bundleSlowFillsV3,
        };
    }
    async findMatchingFillEvent(deposit, spokePoolClient) {
        if ((0, SpokePoolClient_1.isSVMSpokePoolClient)(spokePoolClient)) {
            return await (0, svm_1.findFillEvent)(deposit, spokePoolClient.chainId, spokePoolClient.svmEventsClient, spokePoolClient.deploymentBlock, spokePoolClient.latestHeightSearched, spokePoolClient.logger);
        }
        else if ((0, SpokePoolClient_1.isEVMSpokePoolClient)(spokePoolClient)) {
            const findFillEventHandler = (0, utils_1.chainIsTvm)(spokePoolClient.chainId) ? tvm_1.findFillEvent : evm_1.findFillEvent;
            return await findFillEventHandler(spokePoolClient.spokePool, deposit, spokePoolClient.deploymentBlock, spokePoolClient.latestHeightSearched);
        }
        else {
            throw new Error("Unsupported spoke pool client type");
        }
    }
    async getBundleBlockTimestamps(chainIds, blockRangesForChains, spokePoolClients) {
        return Object.fromEntries((await (0, utils_1.mapAsync)(chainIds, async (chainId, index) => {
            const blockRangeForChain = blockRangesForChains[index];
            if (!(0, utils_1.isDefined)(blockRangeForChain) ||
                (0, utils_2.isChainDisabledAtBlock)(chainId, blockRangesForChains[0][0], this.clients.configStoreClient)) {
                return;
            }
            const [_startBlockForChain, _endBlockForChain] = blockRangeForChain;
            const spokePoolClient = spokePoolClients[chainId];
            if (!(0, utils_1.isDefined)(spokePoolClient)) {
                return;
            }
            const startBlockForChain = Math.min(_startBlockForChain, spokePoolClient.latestHeightSearched);
            const endBlockForChain = Math.min(_endBlockForChain + 1, spokePoolClient.latestHeightSearched);
            const [startTime, _endTime] = [
                await spokePoolClient.getTimestampForBlock(startBlockForChain),
                await spokePoolClient.getTimestampForBlock(endBlockForChain),
            ];
            const endBlockDelta = endBlockForChain > startBlockForChain ? 1 : 0;
            const endTime = Math.max(0, _endTime - endBlockDelta);
            (0, assert_1.default)(endTime >= startTime, `End time for block ${endBlockForChain} should be greater than start time for block ${startBlockForChain}: ${endTime} >= ${startTime}.`);
            (0, assert_1.default)(startBlockForChain === 0 || startTime > 0, "Start timestamp must be greater than 0 if the start block is greater than 0.");
            return [chainId, [startTime, endTime]];
        })).filter(utils_1.isDefined));
    }
}
exports.BundleDataClient = BundleDataClient;
//# sourceMappingURL=BundleDataClient.js.map