"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HubPoolClient = void 0;
const tslib_1 = require("tslib");
const assert_1 = tslib_1.__importDefault(require("assert"));
const lodash_1 = tslib_1.__importDefault(require("lodash"));
const constants_1 = require("../constants");
const lpFeeCalculator = tslib_1.__importStar(require("../lpFeeCalculator"));
const evm_1 = require("../arch/evm");
const utils_1 = require("../utils");
const BaseAbstractClient_1 = require("./BaseAbstractClient");
class HubPoolClient extends BaseAbstractClient_1.BaseAbstractClient {
    logger;
    hubPool;
    configStoreClient;
    deploymentBlock;
    chainId;
    configOverride;
    l1Tokens = [];
    lpTokens = {};
    proposedRootBundles = [];
    canceledRootBundles = [];
    disputedRootBundles = [];
    executedRootBundles = [];
    crossChainContracts = {};
    l1TokensToDestinationTokensWithBlock = {};
    pendingRootBundle;
    currentTime;
    blockFinder;
    constructor(logger, hubPool, configStoreClient, deploymentBlock = 0, chainId = 1, eventSearchConfig = { from: 0, maxLookBack: 0 }, configOverride = {
        ignoredHubExecutedBundles: [],
        ignoredHubProposedBundles: [],
    }, cachingMechanism) {
        super(eventSearchConfig, cachingMechanism);
        this.logger = logger;
        this.hubPool = hubPool;
        this.configStoreClient = configStoreClient;
        this.deploymentBlock = deploymentBlock;
        this.chainId = chainId;
        this.configOverride = configOverride;
        this.latestHeightSearched = Math.min(deploymentBlock - 1, 0);
        this.firstHeightToSearch = eventSearchConfig.from;
        const provider = this.hubPool.provider;
        this.blockFinder = new evm_1.EVMBlockFinder(provider);
    }
    hubPoolEventFilters() {
        return {
            SetPoolRebalanceRoute: this.hubPool.filters.SetPoolRebalanceRoute(),
            L1TokenEnabledForLiquidityProvision: this.hubPool.filters.L1TokenEnabledForLiquidityProvision(),
            ProposeRootBundle: this.hubPool.filters.ProposeRootBundle(),
            RootBundleCanceled: this.hubPool.filters.RootBundleCanceled(),
            RootBundleDisputed: this.hubPool.filters.RootBundleDisputed(),
            RootBundleExecuted: this.hubPool.filters.RootBundleExecuted(),
            CrossChainContractsSet: this.hubPool.filters.CrossChainContractsSet(),
        };
    }
    hasPendingProposal() {
        return this.pendingRootBundle !== undefined;
    }
    getPendingRootBundle() {
        return this.pendingRootBundle;
    }
    getProposedRootBundles() {
        return this.proposedRootBundles;
    }
    getCancelledRootBundles() {
        return this.canceledRootBundles;
    }
    getDisputedRootBundles() {
        return this.disputedRootBundles;
    }
    getExecutedRootBundles() {
        return this.executedRootBundles;
    }
    getSpokePoolForBlock(chain, block = Number.MAX_SAFE_INTEGER) {
        if (!this.crossChainContracts[chain]) {
            throw new Error(`No cross chain contracts set for ${chain}`);
        }
        const mostRecentSpokePoolUpdateBeforeBlock = (0, utils_1.sortEventsDescending)(this.crossChainContracts[chain]).find((crossChainContract) => crossChainContract.blockNumber <= block);
        if (!mostRecentSpokePoolUpdateBeforeBlock) {
            throw new Error(`No cross chain contract found before block ${block} for chain ${chain}`);
        }
        else {
            return mostRecentSpokePoolUpdateBeforeBlock.spokePool;
        }
    }
    getSpokePoolActivationBlock(chain, spokePool) {
        const mostRecentSpokePoolUpdateBeforeBlock = this.crossChainContracts[chain].find((crossChainContract) => crossChainContract.spokePool.eq(spokePool));
        return mostRecentSpokePoolUpdateBeforeBlock?.blockNumber;
    }
    getL2TokenForL1TokenAtBlock(l1Token, destinationChainId, latestHubBlock = Number.MAX_SAFE_INTEGER) {
        if (!this.l1TokensToDestinationTokensWithBlock?.[l1Token.toNative()]?.[destinationChainId]) {
            return undefined;
        }
        const l2Token = this.l1TokensToDestinationTokensWithBlock[l1Token.toNative()][destinationChainId].find((mapping) => mapping.blockNumber <= latestHubBlock);
        return !(0, utils_1.isDefined)(l2Token) || l2Token.l2Token.isZeroAddress() ? undefined : l2Token.l2Token;
    }
    getL1TokenForL2TokenAtBlock(l2Token, destinationChainId, latestHubBlock = Number.MAX_SAFE_INTEGER) {
        const l2Tokens = Object.keys(this.l1TokensToDestinationTokensWithBlock).flatMap((l1Token) => {
            const sortedL2Tokens = (0, utils_1.sortEventsDescending)((this.l1TokensToDestinationTokensWithBlock[l1Token][destinationChainId] ?? []).filter((dstTokenWithBlock) => dstTokenWithBlock.blockNumber <= latestHubBlock));
            return sortedL2Tokens.length > 0 && sortedL2Tokens[0].l2Token.truncateToBytes20() === l2Token.truncateToBytes20()
                ? sortedL2Tokens[0]
                : [];
        });
        return l2Tokens.length === 0 ? undefined : (0, utils_1.sortEventsDescending)(l2Tokens)[0].l1Token;
    }
    getL1TokenForDeposit(deposit) {
        return this.getL1TokenForL2TokenAtBlock(deposit.inputToken, deposit.originChainId, deposit.quoteBlockNumber);
    }
    l2TokenEnabledForL1Token(l1Token, destinationChainId) {
        return this.l2TokenEnabledForL1TokenAtBlock(l1Token, destinationChainId, Number.MAX_SAFE_INTEGER);
    }
    l2TokenEnabledForL1TokenAtBlock(l1Token, destinationChainId, hubBlockNumber) {
        const l2Token = (0, utils_1.sortEventsDescending)(this.l1TokensToDestinationTokensWithBlock?.[l1Token.toNative()]?.[destinationChainId] ?? []).find((mapping) => mapping.blockNumber <= hubBlockNumber);
        return (0, utils_1.isDefined)(l2Token) && !l2Token.l2Token.isZeroAddress();
    }
    l2TokenHasPoolRebalanceRoute(l2Token, l2ChainId, hubPoolBlock = this.latestHeightSearched) {
        const l1Token = this.getL1TokenForL2TokenAtBlock(l2Token, l2ChainId, hubPoolBlock);
        return (0, utils_1.isDefined)(l1Token);
    }
    getTokenInfoForAddress(address, chainId) {
        const tokenInfo = (0, utils_1.getTokenInfo)(address, chainId);
        if (tokenInfo.symbol.toLowerCase() === "usdc" && chainId !== this.chainId) {
            tokenInfo.symbol = (0, utils_1.getUsdcSymbol)(address, chainId) ?? "UNKNOWN USDC";
        }
        return tokenInfo;
    }
    getBlockNumber(timestamp) {
        const hints = { lowBlock: this.deploymentBlock };
        return (0, utils_1.getCachedBlockForTimestamp)(this.chainId, timestamp, this.blockFinder, this.cachingMechanism, hints);
    }
    async getBlockNumbers(timestamps) {
        const sortedTimestamps = (0, utils_1.dedupArray)(timestamps).sort((x, y) => x - y);
        const blockNumbers = {};
        for (const timestamp of sortedTimestamps) {
            blockNumbers[timestamp] = await this.getBlockNumber(timestamp);
        }
        return blockNumbers;
    }
    async getCurrentPoolUtilization(l1Token) {
        const blockNumber = this.latestHeightSearched ?? (await this.hubPool.provider.getBlockNumber());
        return await this.getUtilization(l1Token, blockNumber, utils_1.bnZero, (0, utils_1.getCurrentTime)(), 0);
    }
    async getUtilization(hubPoolToken, blockNumber, depositAmount, timestamp, timeToCache) {
        const resolver = () => {
            const overrides = { blockTag: blockNumber };
            const token = hubPoolToken.toNative();
            return depositAmount.eq(utils_1.bnZero)
                ? this.hubPool.callStatic.liquidityUtilizationCurrent(token, overrides)
                : this.hubPool.callStatic.liquidityUtilizationPostRelay(token, depositAmount, overrides);
        };
        const cache = this.cachingMechanism;
        if (!cache || !(0, utils_1.shouldCache)((0, utils_1.getCurrentTime)(), timestamp, timeToCache)) {
            return resolver();
        }
        const key = depositAmount.eq(utils_1.bnZero)
            ? `utilization_${hubPoolToken.toNative()}_${blockNumber}`
            : `utilization_${hubPoolToken.toNative()}_${blockNumber}_${depositAmount.toString()}`;
        const result = await cache.get(key);
        if ((0, utils_1.isDefined)(result)) {
            return utils_1.BigNumber.from(result);
        }
        const utilization = await resolver();
        if (cache && (0, utils_1.shouldCache)((0, utils_1.getCurrentTime)(), timestamp, timeToCache)) {
            await cache.set(key, `${utilization.toString()}`, constants_1.DEFAULT_CACHING_TTL);
        }
        return utilization;
    }
    async computeRealizedLpFeePct(deposit) {
        const [lpFee] = await this.batchComputeRealizedLpFeePct([deposit]);
        return lpFee;
    }
    async batchComputeRealizedLpFeePct(deposits) {
        (0, assert_1.default)(deposits.length > 0, "No deposits supplied to batchComputeRealizedLpFeePct");
        if (!(0, utils_1.isDefined)(this.currentTime)) {
            throw new Error("HubPoolClient has not set a currentTime");
        }
        const utilizationTimestamps = {};
        let utilization = {};
        let quoteBlocks = {};
        const hubPoolTokens = {};
        const getHubPoolToken = (deposit, quoteBlockNumber) => {
            const tokenKey = `${deposit.originChainId}-${deposit.inputToken}`;
            const l1Token = this.getL1TokenForDeposit({ ...deposit, quoteBlockNumber });
            if (!(0, utils_1.isDefined)(l1Token)) {
                return undefined;
            }
            return (hubPoolTokens[tokenKey] ??= l1Token);
        };
        const getHubPoolTokens = () => (0, utils_1.dedupArray)(Object.values(hubPoolTokens)
            .filter(utils_1.isDefined)
            .map((token) => token.toNative())).map((token) => utils_1.EvmAddress.from(token));
        const resolveUniqueQuoteTimestamps = (deposit) => {
            const { quoteTimestamp } = deposit;
            const quoteBlockNumber = quoteBlocks[quoteTimestamp];
            const hubPoolToken = getHubPoolToken(deposit, quoteBlockNumber);
            if (!hubPoolToken) {
                return;
            }
            const token = hubPoolToken.toNative();
            utilizationTimestamps[token] ??= [];
            if (!utilizationTimestamps[token].includes(quoteTimestamp)) {
                utilizationTimestamps[token].push(quoteTimestamp);
            }
        };
        const resolveUtilization = async (hubPoolToken) => {
            return Object.fromEntries(await (0, utils_1.mapAsync)(utilizationTimestamps[hubPoolToken.toNative()], async (quoteTimestamp) => {
                const blockNumber = quoteBlocks[quoteTimestamp];
                const utilization = await this.getUtilization(hubPoolToken, blockNumber, utils_1.bnZero, quoteTimestamp, timeToCache);
                return [blockNumber, utilization];
            }));
        };
        const computeRealizedLpFeePct = async (deposit) => {
            const { originChainId, paymentChainId, inputAmount, quoteTimestamp } = deposit;
            const quoteBlock = quoteBlocks[quoteTimestamp];
            if (paymentChainId === undefined || paymentChainId === originChainId) {
                return { quoteBlock, realizedLpFeePct: utils_1.bnZero };
            }
            const hubPoolToken = getHubPoolToken(deposit, quoteBlock);
            if (hubPoolToken === undefined) {
                throw new Error(`Cannot computeRealizedLpFeePct for deposit with no pool rebalance route for input token ${deposit.inputToken} on ${originChainId}`);
            }
            const rateModel = this.configStoreClient.getRateModelForBlockNumber(hubPoolToken, originChainId, paymentChainId, quoteBlock);
            const preUtilization = utilization[hubPoolToken.toNative()][quoteBlock];
            const postUtilization = await this.getUtilization(hubPoolToken, quoteBlock, inputAmount, quoteTimestamp, timeToCache);
            const realizedLpFeePct = lpFeeCalculator.calculateRealizedLpFeePct(rateModel, preUtilization, postUtilization);
            return { quoteBlock, realizedLpFeePct };
        };
        const timeToCache = this.configOverride.timeToCache ?? constants_1.DEFAULT_CACHING_SAFE_LAG;
        const quoteTimestamps = (0, utils_1.dedupArray)(deposits.map(({ quoteTimestamp }) => quoteTimestamp));
        quoteBlocks = await this.getBlockNumbers(quoteTimestamps);
        deposits.forEach((deposit) => resolveUniqueQuoteTimestamps(deposit));
        utilization = Object.fromEntries(await (0, utils_1.mapAsync)(getHubPoolTokens(), async (hubPoolToken) => [
            hubPoolToken.toNative(),
            await resolveUtilization(hubPoolToken),
        ]));
        return await (0, utils_1.mapAsync)(deposits, async (deposit) => {
            const quoteBlock = quoteBlocks[deposit.quoteTimestamp];
            if (this.l2TokenHasPoolRebalanceRoute(deposit.inputToken, deposit.originChainId, quoteBlock)) {
                return await computeRealizedLpFeePct(deposit);
            }
            else {
                return {
                    quoteBlock,
                    realizedLpFeePct: utils_1.bnZero,
                };
            }
        });
    }
    getL1Tokens() {
        return this.l1Tokens;
    }
    getTokenInfoForL1Token(l1Token) {
        return this.l1Tokens.find((token) => token.address.eq(l1Token));
    }
    getLpTokenInfoForL1Token(l1Token) {
        return this.lpTokens[l1Token.toNative()];
    }
    areTokensEquivalent(tokenA, chainIdA, tokenB, chainIdB, hubPoolBlock = this.latestHeightSearched) {
        if (!this.l2TokenHasPoolRebalanceRoute(tokenA, chainIdA, hubPoolBlock) ||
            !this.l2TokenHasPoolRebalanceRoute(tokenB, chainIdB, hubPoolBlock)) {
            return false;
        }
        const l1TokenA = this.getL1TokenForL2TokenAtBlock(tokenA, chainIdA, hubPoolBlock);
        const l1TokenB = this.getL1TokenForL2TokenAtBlock(tokenB, chainIdB, hubPoolBlock);
        if (!(0, utils_1.isDefined)(l1TokenA) || !(0, utils_1.isDefined)(l1TokenB) || !l1TokenA.eq(l1TokenB)) {
            return false;
        }
        const _tokenA = this.getL2TokenForL1TokenAtBlock(l1TokenA, chainIdA, hubPoolBlock);
        const _tokenB = this.getL2TokenForL1TokenAtBlock(l1TokenB, chainIdB, hubPoolBlock);
        return (0, utils_1.isDefined)(_tokenA) && (0, utils_1.isDefined)(_tokenB) && tokenA.eq(_tokenA) && tokenB.eq(_tokenB);
    }
    getSpokeActivationBlockForChain(chainId) {
        return this.getSpokePoolActivationBlock(chainId, this.getSpokePoolForBlock(chainId)) ?? 0;
    }
    isRootBundleValid(rootBundle, latestMainnetBlock) {
        const nextRootBundle = this.getFollowingRootBundle(rootBundle);
        const executedLeafCount = this.getExecutedLeavesForRootBundle(rootBundle, nextRootBundle ? Math.min(nextRootBundle.blockNumber, latestMainnetBlock) : latestMainnetBlock);
        return executedLeafCount.length === rootBundle.poolRebalanceLeafCount;
    }
    getRootBundleEvalBlockNumberContainingBlock(latestMainnetBlock, block, chain, chainIdListOverride) {
        const chainIdList = chainIdListOverride ?? this.configStoreClient.getChainIdIndicesForBlock(latestMainnetBlock);
        let endingBlockNumber;
        for (let i = this.proposedRootBundles.length - 1; i >= 0; i--) {
            const rootBundle = this.proposedRootBundles[i];
            const nextRootBundle = this.getFollowingRootBundle(rootBundle);
            if (!this.isRootBundleValid(rootBundle, nextRootBundle ? nextRootBundle.blockNumber : latestMainnetBlock)) {
                continue;
            }
            const bundleEvalBlockNumber = this.getBundleEndBlockForChain(rootBundle, chain, chainIdList);
            if (bundleEvalBlockNumber <= block) {
                if (bundleEvalBlockNumber === block) {
                    endingBlockNumber = bundleEvalBlockNumber;
                }
                break;
            }
            endingBlockNumber = bundleEvalBlockNumber;
        }
        return endingBlockNumber;
    }
    getProposedRootBundlesInBlockRange(startingBlock, endingBlock) {
        return this.proposedRootBundles.filter((bundle) => bundle.blockNumber >= startingBlock && bundle.blockNumber <= endingBlock);
    }
    getCancelledRootBundlesInBlockRange(startingBlock, endingBlock) {
        return (0, utils_1.sortEventsDescending)(this.canceledRootBundles).filter((bundle) => bundle.blockNumber >= startingBlock && bundle.blockNumber <= endingBlock);
    }
    getDisputedRootBundlesInBlockRange(startingBlock, endingBlock) {
        return (0, utils_1.sortEventsDescending)(this.disputedRootBundles).filter((bundle) => bundle.blockNumber >= startingBlock && bundle.blockNumber <= endingBlock);
    }
    getTokenMappingsModifiedInBlockRange(startingBlock, endingBlock) {
        return (Object.values(this.l1TokensToDestinationTokensWithBlock)
            .flatMap((destinationTokens) => Object.entries(destinationTokens).flatMap(([destinationChainId, tokensWithBlock]) => tokensWithBlock.map((token) => ({
            ...token,
            l2ChainId: Number(destinationChainId),
        }))))
            .filter((token) => token.blockNumber >= startingBlock && token.blockNumber <= endingBlock));
    }
    getLatestProposedRootBundle() {
        return this.proposedRootBundles[this.proposedRootBundles.length - 1];
    }
    getFollowingRootBundle(currentRootBundle) {
        const index = lodash_1.default.findLastIndex(this.proposedRootBundles, (bundle) => bundle.blockNumber === currentRootBundle.blockNumber);
        if (index === -1 || index === this.proposedRootBundles.length - 1) {
            return undefined;
        }
        return this.proposedRootBundles[index + 1];
    }
    getExecutedLeavesForRootBundle(rootBundle, latestMainnetBlockToSearch) {
        return this.executedRootBundles.filter((executedLeaf) => executedLeaf.blockNumber <= latestMainnetBlockToSearch &&
            executedLeaf.blockNumber > rootBundle.blockNumber);
    }
    getValidatedRootBundles(latestMainnetBlock = Number.MAX_SAFE_INTEGER) {
        return this.proposedRootBundles.filter((rootBundle) => {
            if (rootBundle.blockNumber > latestMainnetBlock) {
                return false;
            }
            return this.isRootBundleValid(rootBundle, latestMainnetBlock);
        });
    }
    getLatestFullyExecutedRootBundle(latestMainnetBlock) {
        return lodash_1.default.findLast(this.proposedRootBundles, (rootBundle) => {
            if (rootBundle.blockNumber > latestMainnetBlock) {
                return false;
            }
            return this.isRootBundleValid(rootBundle, latestMainnetBlock);
        });
    }
    getEarliestFullyExecutedRootBundle(latestMainnetBlock, startBlock = 0) {
        return this.proposedRootBundles.find((rootBundle) => {
            if (rootBundle.blockNumber > latestMainnetBlock) {
                return false;
            }
            if (rootBundle.blockNumber < startBlock) {
                return false;
            }
            return this.isRootBundleValid(rootBundle, latestMainnetBlock);
        });
    }
    getNthFullyExecutedRootBundle(n, startBlock) {
        if (n === 0) {
            throw new Error("n cannot be 0");
        }
        if (!this.latestHeightSearched) {
            throw new Error("HubPoolClient::getNthFullyExecutedRootBundle client not updated");
        }
        let bundleToReturn;
        if (n < 0) {
            let nextLatestMainnetBlock = startBlock ?? this.latestHeightSearched;
            for (let i = 0; i < Math.abs(n); i++) {
                bundleToReturn = this.getLatestFullyExecutedRootBundle(nextLatestMainnetBlock);
                const bundleBlockNumber = bundleToReturn ? bundleToReturn.blockNumber : 0;
                nextLatestMainnetBlock = Math.max(0, bundleBlockNumber - 1);
            }
        }
        else {
            let nextStartBlock = startBlock ?? 0;
            for (let i = 0; i < n; i++) {
                bundleToReturn = this.getEarliestFullyExecutedRootBundle(this.latestHeightSearched, nextStartBlock);
                const bundleBlockNumber = bundleToReturn ? bundleToReturn.blockNumber : 0;
                nextStartBlock = Math.min(bundleBlockNumber + 1, this.latestHeightSearched);
            }
        }
        return bundleToReturn;
    }
    getLatestBundleEndBlockForChain(chainIdList, latestMainnetBlock, chainId) {
        const latestFullyExecutedPoolRebalanceRoot = this.getLatestFullyExecutedRootBundle(latestMainnetBlock);
        if (!latestFullyExecutedPoolRebalanceRoot) {
            return 0;
        }
        return this.getBundleEndBlockForChain(latestFullyExecutedPoolRebalanceRoot, chainId, chainIdList);
    }
    getNextBundleStartBlockNumber(chainIdList, latestMainnetBlock, chainId) {
        const endBlock = this.getLatestBundleEndBlockForChain(chainIdList, latestMainnetBlock, chainId);
        return endBlock > 0 ? endBlock + 1 : 0;
    }
    getOptimisticBundleStartBlockNumber(chainIdList, latestMainnetBlock, chainId) {
        if (!this.hasPendingProposal()) {
            return this.getNextBundleStartBlockNumber(chainIdList, latestMainnetBlock, chainId);
        }
        const latestProposedBundle = this.proposedRootBundles[this.proposedRootBundles.length - 1];
        if (!(0, utils_1.isDefined)(latestProposedBundle)) {
            return 0;
        }
        const optimisticEndBlock = this.getBundleEndBlockForChain(latestProposedBundle, chainId, chainIdList);
        return optimisticEndBlock > 0 ? optimisticEndBlock + 1 : 0;
    }
    getLatestExecutedRootBundleContainingL1Token(block, chain, l1Token) {
        return (0, utils_1.sortEventsDescending)(this.executedRootBundles).find((executedLeaf) => {
            return (executedLeaf.blockNumber <= block &&
                executedLeaf.chainId === chain &&
                executedLeaf.l1Tokens.some((token) => token.eq(l1Token)));
        });
    }
    getRunningBalanceBeforeBlockForChain(block, chain, l1Token) {
        const executedRootBundle = this.getLatestExecutedRootBundleContainingL1Token(block, chain, l1Token);
        return this.getRunningBalanceForToken(l1Token, executedRootBundle);
    }
    getRunningBalanceForToken(l1Token, executedRootBundle) {
        let runningBalance = (0, utils_1.toBN)(0);
        if (executedRootBundle) {
            const indexOfL1Token = executedRootBundle.l1Tokens.findIndex((tokenInBundle) => tokenInBundle.eq(l1Token));
            if (indexOfL1Token !== -1) {
                runningBalance = executedRootBundle.runningBalances[indexOfL1Token];
            }
        }
        return { runningBalance };
    }
    async _update(eventNames) {
        const hubPoolEvents = this.hubPoolEventFilters();
        const searchConfig = await this.updateSearchConfig(this.hubPool.provider);
        if ((0, BaseAbstractClient_1.isUpdateFailureReason)(searchConfig)) {
            return { success: false, reason: searchConfig };
        }
        const supportedEvents = Object.keys(hubPoolEvents);
        if (eventNames.some((eventName) => !supportedEvents.includes(eventName))) {
            return { success: false, reason: BaseAbstractClient_1.UpdateFailureReason.BadRequest };
        }
        const eventSearchConfigs = eventNames.map((eventName) => {
            const _searchConfig = { ...searchConfig };
            const overrideEvents = ["CrossChainContractsSet", "L1TokenEnabledForLiquidityProvision", "SetPoolRebalanceRoute"];
            if (overrideEvents.includes(eventName) && !this.isUpdated) {
                _searchConfig.from = this.deploymentBlock;
            }
            return {
                eventName,
                filter: hubPoolEvents[eventName],
                searchConfig: _searchConfig,
            };
        });
        this.logger.debug({
            at: "HubPoolClient",
            message: "Updating HubPool client",
            searchConfig: eventSearchConfigs.map(({ eventName, searchConfig }) => ({ eventName, searchConfig })),
        });
        const timerStart = Date.now();
        const { hubPool } = this;
        const multicallFunctions = ["getCurrentTime", "rootBundleProposal"];
        const [multicallOutput, ...events] = await Promise.all([
            hubPool.callStatic.multicall(multicallFunctions.map((f) => hubPool.interface.encodeFunctionData(f)), { blockTag: searchConfig.to }),
            ...eventSearchConfigs.map((config) => (0, utils_1.paginatedEventQuery)(hubPool, config.filter, config.searchConfig)),
        ]);
        const [currentTime, pendingRootBundleProposal] = multicallFunctions.map((fn, idx) => {
            const output = hubPool.interface.decodeFunctionResult(fn, multicallOutput[idx]);
            return output.length > 1 ? output : output[0];
        });
        this.logger.debug({
            at: "HubPoolClient#_update",
            message: `Time to query new events from RPC for ${this.chainId}: ${Date.now() - timerStart} ms`,
        });
        const _events = Object.fromEntries(eventNames.map((eventName, idx) => [eventName, events[idx]]));
        return {
            success: true,
            currentTime,
            pendingRootBundleProposal: {
                ...pendingRootBundleProposal,
                proposer: utils_1.EvmAddress.from(pendingRootBundleProposal.proposer),
            },
            searchEndBlock: searchConfig.to,
            events: _events,
        };
    }
    async update(eventsToQuery = Object.keys(this.hubPoolEventFilters())) {
        if (!this.configStoreClient.isUpdated) {
            throw new Error("ConfigStoreClient not updated");
        }
        const update = await this._update(eventsToQuery);
        if (!update.success) {
            if (update.reason !== BaseAbstractClient_1.UpdateFailureReason.AlreadyUpdated) {
                throw new Error(`Unable to update HubPoolClient: ${update.reason}`);
            }
            return;
        }
        const { events, currentTime, pendingRootBundleProposal, searchEndBlock } = update;
        if (eventsToQuery.includes("CrossChainContractsSet")) {
            for (const event of events["CrossChainContractsSet"]) {
                const args = (0, utils_1.spreadEventWithBlockNumber)(event);
                const dataToAdd = {
                    spokePool: utils_1.EvmAddress.from(args.spokePool),
                    blockNumber: args.blockNumber,
                    txnRef: args.txnRef,
                    logIndex: args.logIndex,
                    txnIndex: args.txnIndex,
                    l2ChainId: args.l2ChainId,
                };
                if ((0, utils_1.chainIsSvm)(args.l2ChainId)) {
                    const solanaSpokePool = (0, utils_1.getDeployedAddress)("SvmSpoke", args.l2ChainId);
                    if (!solanaSpokePool) {
                        throw new Error(`SVM spoke pool not found for chain ${args.l2ChainId}`);
                    }
                    const svmSpoke = utils_1.SvmAddress.from(solanaSpokePool);
                    const truncatedAddress = svmSpoke.truncateToBytes20();
                    if (args.spokePool.toLowerCase() !== truncatedAddress.toLowerCase()) {
                        throw new Error(`SVM spoke pool address mismatch for chain ${args.l2ChainId}. ` +
                            `Expected ${truncatedAddress}, got ${args.spokePool}`);
                    }
                    dataToAdd.spokePool = svmSpoke;
                }
                (0, utils_1.assign)(this.crossChainContracts, [args.l2ChainId], [dataToAdd]);
            }
        }
        if (eventsToQuery.includes("SetPoolRebalanceRoute")) {
            for (const event of events["SetPoolRebalanceRoute"]) {
                const args = (0, utils_1.spreadEventWithBlockNumber)(event);
                let destinationToken = utils_1.EvmAddress.from(args.destinationToken);
                if ((0, utils_1.chainIsSvm)(args.destinationChainId)) {
                    const usdcTokenSol = constants_1.TOKEN_SYMBOLS_MAP.USDC.addresses[args.destinationChainId];
                    const svmUsdc = utils_1.SvmAddress.from(usdcTokenSol);
                    if (destinationToken.truncateToBytes20() !== svmUsdc.truncateToBytes20()) {
                        throw new Error(`SVM USDC address mismatch for chain ${args.destinationChainId}. ` +
                            `Expected ${svmUsdc.truncateToBytes20()}, got ${destinationToken}`);
                    }
                    destinationToken = svmUsdc;
                }
                const newRoute = {
                    l1Token: utils_1.EvmAddress.from(args.l1Token),
                    l2Token: destinationToken,
                    blockNumber: args.blockNumber,
                    txnIndex: args.txnIndex,
                    logIndex: args.logIndex,
                    txnRef: args.txnRef,
                };
                if (this.l1TokensToDestinationTokensWithBlock[args.l1Token]?.[args.destinationChainId]) {
                    this.l1TokensToDestinationTokensWithBlock[args.l1Token][args.destinationChainId] = (0, utils_1.sortEventsDescending)([
                        ...this.l1TokensToDestinationTokensWithBlock[args.l1Token][args.destinationChainId],
                        newRoute,
                    ]);
                }
                else {
                    (0, utils_1.assign)(this.l1TokensToDestinationTokensWithBlock, [args.l1Token, args.destinationChainId], [newRoute]);
                }
            }
        }
        if (eventsToQuery.includes("L1TokenEnabledForLiquidityProvision")) {
            const uniqueL1Tokens = (0, utils_1.dedupArray)(events["L1TokenEnabledForLiquidityProvision"].map((event) => String(event.args["l1Token"])));
            const [tokenInfo, lpTokenInfo] = await Promise.all([
                Promise.all(uniqueL1Tokens.map(async (l1Token) => {
                    const tokenInfo = await (0, utils_1.fetchTokenInfo)(l1Token, this.hubPool.provider);
                    return {
                        ...tokenInfo,
                        address: utils_1.EvmAddress.from(l1Token),
                    };
                })),
                Promise.all(uniqueL1Tokens.map(async (l1Token) => await this.hubPool.pooledTokens(l1Token, { blockTag: update.searchEndBlock }))),
            ]);
            for (const info of tokenInfo) {
                if (!this.l1Tokens.find((token) => token.address.eq(info.address))) {
                    if (info.decimals > 0 && info.decimals <= 18) {
                        this.l1Tokens.push(info);
                    }
                    else {
                        throw new Error(`Unsupported HubPool token: ${JSON.stringify(info)}`);
                    }
                }
            }
            uniqueL1Tokens.forEach((token, i) => {
                this.lpTokens[token] = {
                    lastLpFeeUpdate: lpTokenInfo[i].lastLpFeeUpdate,
                    liquidReserves: lpTokenInfo[i].liquidReserves,
                };
            });
        }
        if (eventsToQuery.includes("ProposeRootBundle")) {
            this.proposedRootBundles.push(...events["ProposeRootBundle"]
                .filter((event) => !this.configOverride.ignoredHubProposedBundles.includes(event.blockNumber))
                .map((_event) => {
                const args = (0, utils_1.spreadEventWithBlockNumber)(_event);
                return {
                    ...args,
                    proposer: utils_1.EvmAddress.from(args.proposer),
                };
            }));
        }
        if (eventsToQuery.includes("RootBundleCanceled")) {
            this.canceledRootBundles.push(...events["RootBundleCanceled"].map((event) => (0, utils_1.spreadEventWithBlockNumber)(event)));
        }
        if (eventsToQuery.includes("RootBundleDisputed")) {
            this.disputedRootBundles.push(...events["RootBundleDisputed"].map((event) => (0, utils_1.spreadEventWithBlockNumber)(event)));
        }
        if (eventsToQuery.includes("RootBundleExecuted")) {
            for (const event of events["RootBundleExecuted"]) {
                if (this.configOverride.ignoredHubExecutedBundles.includes(event.blockNumber)) {
                    continue;
                }
                const executedRootBundle = (0, utils_1.spreadEventWithBlockNumber)(event);
                const { l1Tokens, runningBalances } = executedRootBundle;
                const nTokens = l1Tokens.length;
                if (![nTokens, nTokens * 2].includes(runningBalances.length)) {
                    throw new Error(`Invalid runningBalances length: ${runningBalances.length}.` +
                        ` Expected ${nTokens} or ${nTokens * 2} for chain ${this.chainId} transaction ${event.transactionHash}`);
                }
                executedRootBundle.runningBalances = runningBalances.slice(0, nTokens);
                this.executedRootBundles.push({
                    ...executedRootBundle,
                    l1Tokens: l1Tokens.map((l1Token) => utils_1.EvmAddress.from(l1Token)),
                });
            }
        }
        if (eventsToQuery.includes("ProposeRootBundle")) {
            if (pendingRootBundleProposal.unclaimedPoolRebalanceLeafCount > 0) {
                const mostRecentProposedRootBundle = this.proposedRootBundles[this.proposedRootBundles.length - 1];
                this.pendingRootBundle = {
                    poolRebalanceRoot: pendingRootBundleProposal.poolRebalanceRoot,
                    relayerRefundRoot: pendingRootBundleProposal.relayerRefundRoot,
                    slowRelayRoot: pendingRootBundleProposal.slowRelayRoot,
                    proposer: pendingRootBundleProposal.proposer,
                    unclaimedPoolRebalanceLeafCount: pendingRootBundleProposal.unclaimedPoolRebalanceLeafCount,
                    challengePeriodEndTimestamp: pendingRootBundleProposal.challengePeriodEndTimestamp,
                    bundleEvaluationBlockNumbers: mostRecentProposedRootBundle.bundleEvaluationBlockNumbers.map((block) => {
                        try {
                            return block.toNumber();
                        }
                        catch {
                            return 0;
                        }
                    }),
                    proposalBlockNumber: mostRecentProposedRootBundle.blockNumber,
                };
            }
            else {
                this.pendingRootBundle = undefined;
            }
        }
        this.currentTime = currentTime;
        this.latestHeightSearched = searchEndBlock;
        this.firstHeightToSearch = update.searchEndBlock + 1;
        this.eventSearchConfig.to = undefined;
        this.isUpdated = true;
        this.logger.debug({ at: "HubPoolClient::update", message: "HubPool client updated!", searchEndBlock, currentTime });
    }
    getBundleEndBlockForChain(proposeRootBundleEvent, chainId, chainIdList) {
        const bundleEvaluationBlockNumbers = proposeRootBundleEvent.bundleEvaluationBlockNumbers;
        const chainIdIndex = chainIdList.indexOf(chainId);
        if (chainIdIndex === -1) {
            return 0;
        }
        if (chainIdIndex >= bundleEvaluationBlockNumbers.length) {
            return 0;
        }
        return bundleEvaluationBlockNumbers[chainIdIndex].toNumber();
    }
}
exports.HubPoolClient = HubPoolClient;
//# sourceMappingURL=HubPoolClient.js.map