import assert from "assert";
import _ from "lodash";
import { DEFAULT_CACHING_SAFE_LAG, DEFAULT_CACHING_TTL, TOKEN_SYMBOLS_MAP } from "../constants";
import * as lpFeeCalculator from "../lpFeeCalculator";
import { EVMBlockFinder } from "../arch/evm";
import { BigNumber, bnZero, dedupArray, assign, fetchTokenInfo, getCachedBlockForTimestamp, getCurrentTime, isDefined, mapAsync, paginatedEventQuery, shouldCache, sortEventsDescending, spreadEventWithBlockNumber, toBN, getTokenInfo, getUsdcSymbol, chainIsSvm, getDeployedAddress, SvmAddress, EvmAddress, } from "../utils";
import { BaseAbstractClient, isUpdateFailureReason, UpdateFailureReason } from "./BaseAbstractClient";
export class HubPoolClient extends BaseAbstractClient {
    logger;
    hubPool;
    configStoreClient;
    deploymentBlock;
    chainId;
    configOverride;
    l1Tokens = []; // L1Tokens and their associated info.
    // @dev `token` here is a 20-byte hex sting
    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 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 = 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) {
        // Return first time that this spoke pool was registered in the HubPool as a cross chain contract. We can use
        // this block as the oldest block that we should query for SpokePoolClient purposes.
        const mostRecentSpokePoolUpdateBeforeBlock = this.crossChainContracts[chain].find((crossChainContract) => crossChainContract.spokePool.eq(spokePool));
        return mostRecentSpokePoolUpdateBeforeBlock?.blockNumber;
    }
    // Returns the latest L2 token to use for an L1 token as of the input hub block.
    getL2TokenForL1TokenAtBlock(l1Token, destinationChainId, latestHubBlock = Number.MAX_SAFE_INTEGER) {
        if (!this.l1TokensToDestinationTokensWithBlock?.[l1Token.toNative()]?.[destinationChainId]) {
            return undefined;
        }
        // Find the last mapping published before the target block.
        const l2Token = this.l1TokensToDestinationTokensWithBlock[l1Token.toNative()][destinationChainId].find((mapping) => mapping.blockNumber <= latestHubBlock);
        return !isDefined(l2Token) || l2Token.l2Token.isZeroAddress() ? undefined : l2Token.l2Token;
    }
    // Returns the latest L1 token to use for an L2 token as of the input hub block.
    getL1TokenForL2TokenAtBlock(l2Token, destinationChainId, latestHubBlock = Number.MAX_SAFE_INTEGER) {
        const l2Tokens = Object.keys(this.l1TokensToDestinationTokensWithBlock).flatMap((l1Token) => {
            // Get the latest L2 token mapping for the given L1 token.
            // @dev Since tokens on L2s (like Solana) can have 32 byte addresses, filter on the lower 20 bytes of the token only.
            const sortedL2Tokens = sortEventsDescending((this.l1TokensToDestinationTokensWithBlock[l1Token][destinationChainId] ?? []).filter((dstTokenWithBlock) => dstTokenWithBlock.blockNumber <= latestHubBlock));
            // If the latest L2 token mapping is equal to the target L2 token, return it.
            return sortedL2Tokens.length > 0 && sortedL2Tokens[0].l2Token.truncateToBytes20() === l2Token.truncateToBytes20()
                ? sortedL2Tokens[0]
                : [];
        });
        return l2Tokens.length === 0 ? undefined : sortEventsDescending(l2Tokens)[0].l1Token;
    }
    getL1TokenForDeposit(deposit) {
        // L1-->L2 token mappings are set via PoolRebalanceRoutes which occur on mainnet,
        // so we use the latest token mapping. This way if a very old deposit is filled, the relayer can use the
        // latest L2 token mapping to find the L1 token counterpart.
        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) {
        // Find the last mapping published before the target block.
        const l2Token = sortEventsDescending(this.l1TokensToDestinationTokensWithBlock?.[l1Token.toNative()]?.[destinationChainId] ?? []).find((mapping) => mapping.blockNumber <= hubBlockNumber);
        return isDefined(l2Token) && !l2Token.l2Token.isZeroAddress();
    }
    l2TokenHasPoolRebalanceRoute(l2Token, l2ChainId, hubPoolBlock = this.latestHeightSearched) {
        const l1Token = this.getL1TokenForL2TokenAtBlock(l2Token, l2ChainId, hubPoolBlock);
        return isDefined(l1Token);
    }
    /**
     * @dev If tokenAddress + chain do not exist in TOKEN_SYMBOLS_MAP then this will throw.
     * @param tokenAddress Token address on `chain`
     * @param chainId Chain where the `tokenAddress` exists in TOKEN_SYMBOLS_MAP.
     * @returns Token info for the given token address on the L2 chain including symbol and decimal.
     */
    getTokenInfoForAddress(address, chainId) {
        const tokenInfo = getTokenInfo(address, chainId);
        // @dev Temporarily handle case where an L2 token for chain ID can map to more than one TOKEN_SYMBOLS_MAP
        // entry. For example, L2 Bridged USDC maps to both the USDC and USDC.e/USDbC entries in TOKEN_SYMBOLS_MAP.
        if (tokenInfo.symbol.toLowerCase() === "usdc" && chainId !== this.chainId) {
            tokenInfo.symbol = getUsdcSymbol(address, chainId) ?? "UNKNOWN USDC";
        }
        return tokenInfo;
    }
    /**
     * Resolve a given timestamp to a block number on the HubPool chain.
     * @param timestamp A single timestamp to be resolved to a block number on the HubPool chain.
     * @returns The block number corresponding to the supplied timestamp.
     */
    getBlockNumber(timestamp) {
        const hints = { lowBlock: this.deploymentBlock };
        return getCachedBlockForTimestamp(this.chainId, timestamp, this.blockFinder, this.cachingMechanism, hints);
    }
    /**
     * For an array of timestamps, resolve each unique timestamp to a block number on the HubPool chain.
     * @dev Inputs are filtered for uniqueness and sorted to improve BlockFinder efficiency.
     * @dev Querying block numbers sequentially also improves BlockFinder efficiency.
     * @param timestamps Array of timestamps to be resolved to a block number on the HubPool chain.
     * @returns A mapping of quoteTimestamp -> HubPool block number.
     */
    async getBlockNumbers(timestamps) {
        const sortedTimestamps = 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, bnZero, getCurrentTime(), 0);
    }
    /**
     * For a HubPool token at a specific block number, compute the relevant utilization.
     * @param hubPoolToken HubPool token to query utilization for.
     * @param blocknumber Block number to query utilization at.
     * @param amount Amount to query. If set to 0, the closing utilization at blockNumber is returned.
     * @param amount timestamp Associated quoteTimestamp for query, used for caching evaluation.
     * @param timeToCache Age at which the response is able to be cached.
     * @returns HubPool utilization at `blockNumber` after optional `amount` increase in utilization.
     */
    async getUtilization(hubPoolToken, blockNumber, depositAmount, timestamp, timeToCache) {
        // Resolve this function call as an async anonymous function
        const resolver = () => {
            const overrides = { blockTag: blockNumber };
            const token = hubPoolToken.toNative();
            // For zero amount, just get the utilisation at `blockNumber`.
            return depositAmount.eq(bnZero)
                ? this.hubPool.callStatic.liquidityUtilizationCurrent(token, overrides)
                : this.hubPool.callStatic.liquidityUtilizationPostRelay(token, depositAmount, overrides);
        };
        // Resolve the cache locally so that we can appease typescript
        const cache = this.cachingMechanism;
        // If there is no cache or the timestamp is not old enough to be cached, just resolve the function.
        if (!cache || !shouldCache(getCurrentTime(), timestamp, timeToCache)) {
            return resolver();
        }
        // Otherwise, let's resolve the key
        const key = depositAmount.eq(bnZero)
            ? `utilization_${hubPoolToken.toNative()}_${blockNumber}`
            : `utilization_${hubPoolToken.toNative()}_${blockNumber}_${depositAmount.toString()}`;
        const result = await cache.get(key);
        if (isDefined(result)) {
            return BigNumber.from(result);
        }
        // We were not able to find a valid result, so let's resolve the function.
        const utilization = await resolver();
        if (cache && shouldCache(getCurrentTime(), timestamp, timeToCache)) {
            // If we should cache the result, store it for up to DEFAULT_CACHING_TTL.
            await cache.set(key, `${utilization.toString()}`, DEFAULT_CACHING_TTL);
        }
        return utilization;
    }
    async computeRealizedLpFeePct(deposit) {
        const [lpFee] = await this.batchComputeRealizedLpFeePct([deposit]);
        return lpFee;
    }
    async batchComputeRealizedLpFeePct(deposits) {
        assert(deposits.length > 0, "No deposits supplied to batchComputeRealizedLpFeePct");
        if (!isDefined(this.currentTime)) {
            throw new Error("HubPoolClient has not set a currentTime");
        }
        // Map each HubPool token to an array of unique quoteTimestamps.
        const utilizationTimestamps = {};
        // Map each HubPool token to utilization at a particular block number.
        let utilization = {};
        let quoteBlocks = {};
        // Map SpokePool token addresses to HubPool token addresses.
        // Note: Should only be accessed via `getHubPoolToken()` or `getHubPoolTokens()`.
        const hubPoolTokens = {};
        const getHubPoolToken = (deposit, quoteBlockNumber) => {
            const tokenKey = `${deposit.originChainId}-${deposit.inputToken}`;
            const l1Token = this.getL1TokenForDeposit({ ...deposit, quoteBlockNumber });
            if (!isDefined(l1Token)) {
                return undefined;
            }
            return (hubPoolTokens[tokenKey] ??= l1Token);
        };
        // Filter hubPoolTokens for duplicates by reverting to their native string
        // representation. This is required for deduplication to work reliably.
        const getHubPoolTokens = () => dedupArray(Object.values(hubPoolTokens)
            .filter(isDefined)
            .map((token) => token.toNative())).map((token) => EvmAddress.from(token));
        // Helper to resolve the unique hubPoolToken & quoteTimestamp mappings.
        const resolveUniqueQuoteTimestamps = (deposit) => {
            const { quoteTimestamp } = deposit;
            // Resolve the HubPool token address for this origin chainId/token pair, if it isn't already known.
            const quoteBlockNumber = quoteBlocks[quoteTimestamp];
            const hubPoolToken = getHubPoolToken(deposit, quoteBlockNumber);
            if (!hubPoolToken) {
                return;
            }
            // Append the quoteTimestamp for this HubPool token, if it isn't already enqueued.
            const token = hubPoolToken.toNative();
            utilizationTimestamps[token] ??= [];
            if (!utilizationTimestamps[token].includes(quoteTimestamp)) {
                utilizationTimestamps[token].push(quoteTimestamp);
            }
        };
        // Helper to resolve existing HubPool token utilisation for an array of unique block numbers.
        // Produces a mapping of blockNumber -> utilization for a specific token.
        const resolveUtilization = async (hubPoolToken) => {
            return Object.fromEntries(await mapAsync(utilizationTimestamps[hubPoolToken.toNative()], async (quoteTimestamp) => {
                const blockNumber = quoteBlocks[quoteTimestamp];
                const utilization = await this.getUtilization(hubPoolToken, blockNumber, bnZero, // amount
                quoteTimestamp, timeToCache);
                return [blockNumber, utilization];
            }));
        };
        // Helper compute the realizedLpFeePct of an individual deposit based on pre-retrieved batch data.
        const computeRealizedLpFeePct = async (deposit) => {
            const { originChainId, paymentChainId, inputAmount, quoteTimestamp } = deposit;
            const quoteBlock = quoteBlocks[quoteTimestamp];
            if (paymentChainId === undefined || paymentChainId === originChainId) {
                return { quoteBlock, realizedLpFeePct: 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 };
        };
        /**
         * Execution flow starts here.
         */
        const timeToCache = this.configOverride.timeToCache ?? DEFAULT_CACHING_SAFE_LAG;
        // Filter all deposits for unique quoteTimestamps, to be resolved to a blockNumber in parallel.
        const quoteTimestamps = dedupArray(deposits.map(({ quoteTimestamp }) => quoteTimestamp));
        quoteBlocks = await this.getBlockNumbers(quoteTimestamps);
        // Identify the unique hubPoolToken & quoteTimestamp mappings. This is used to optimise subsequent HubPool queries.
        deposits.forEach((deposit) => resolveUniqueQuoteTimestamps(deposit));
        // For each token / quoteBlock pair, resolve the utilisation for each quoted block.
        // This can be reused for each deposit with the same HubPool token and quoteTimestamp pair.
        utilization = Object.fromEntries(await mapAsync(getHubPoolTokens(), async (hubPoolToken) => [
            hubPoolToken.toNative(),
            await resolveUtilization(hubPoolToken),
        ]));
        // For each deposit, compute the post-relay HubPool utilisation independently.
        // @dev The caller expects to receive an array in the same length and ordering as the input `deposits`.
        return await 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: 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;
        }
        // Resolve both SpokePool tokens back to their respective HubPool tokens and verify that they match.
        const l1TokenA = this.getL1TokenForL2TokenAtBlock(tokenA, chainIdA, hubPoolBlock);
        const l1TokenB = this.getL1TokenForL2TokenAtBlock(tokenB, chainIdB, hubPoolBlock);
        if (!isDefined(l1TokenA) || !isDefined(l1TokenB) || !l1TokenA.eq(l1TokenB)) {
            return false;
        }
        // Resolve both HubPool tokens back to a current SpokePool token and verify that they match.
        const _tokenA = this.getL2TokenForL1TokenAtBlock(l1TokenA, chainIdA, hubPoolBlock);
        const _tokenB = this.getL2TokenForL1TokenAtBlock(l1TokenB, chainIdB, hubPoolBlock);
        return isDefined(_tokenA) && isDefined(_tokenB) && tokenA.eq(_tokenA) && tokenB.eq(_tokenB);
    }
    getSpokeActivationBlockForChain(chainId) {
        return this.getSpokePoolActivationBlock(chainId, this.getSpokePoolForBlock(chainId)) ?? 0;
    }
    // Root bundles are valid if all of their pool rebalance leaves have been executed before the next bundle, or the
    // latest mainnet block to search. Whichever comes first.
    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;
    }
    // This should find the ProposeRootBundle event whose bundle block number for `chain` is closest to the `block`
    // without being smaller. It returns the bundle block number for the chain or undefined if not matched.
    getRootBundleEvalBlockNumberContainingBlock(latestMainnetBlock, block, chain, chainIdListOverride) {
        const chainIdList = chainIdListOverride ?? this.configStoreClient.getChainIdIndicesForBlock(latestMainnetBlock);
        let endingBlockNumber;
        // Search proposed root bundles in reverse chronological order.
        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;
            }
            // 0 is the default value bundleEvalBlockNumber.
            const bundleEvalBlockNumber = this.getBundleEndBlockForChain(rootBundle, chain, chainIdList);
            // Since we're iterating from newest to oldest, bundleEvalBlockNumber is only decreasing, and if the
            // bundleEvalBlockNumber is smaller than the target block, then we should return the last set `endingBlockNumber`.
            if (bundleEvalBlockNumber <= block) {
                if (bundleEvalBlockNumber === block) {
                    endingBlockNumber = bundleEvalBlockNumber;
                }
                break;
            }
            endingBlockNumber = bundleEvalBlockNumber;
        }
        return endingBlockNumber;
    }
    // TODO: This might not be necessary since the cumulative root bundle count doesn't grow fast enough, but consider
    // using _.findLast/_.find instead of resorting the arrays if these functions begin to take a lot time.
    getProposedRootBundlesInBlockRange(startingBlock, endingBlock) {
        return this.proposedRootBundles.filter((bundle) => bundle.blockNumber >= startingBlock && bundle.blockNumber <= endingBlock);
    }
    getCancelledRootBundlesInBlockRange(startingBlock, endingBlock) {
        return sortEventsDescending(this.canceledRootBundles).filter((bundle) => bundle.blockNumber >= startingBlock && bundle.blockNumber <= endingBlock);
    }
    getDisputedRootBundlesInBlockRange(startingBlock, endingBlock) {
        return sortEventsDescending(this.disputedRootBundles).filter((bundle) => bundle.blockNumber >= startingBlock && bundle.blockNumber <= endingBlock);
    }
    /**
     * Retrieves token mappings that were modified within a specified block range.
     * @param startingBlock - The starting block of the range (inclusive).
     * @param endingBlock - The ending block of the range (inclusive).
     * @returns An array of destination tokens, each containing the `l2ChainId`, that
     *          were modified within the given block range.
     */
    getTokenMappingsModifiedInBlockRange(startingBlock, endingBlock) {
        // This function iterates over `l1TokensToDestinationTokensWithBlock`, a nested
        // structure of L1 tokens mapped to destination chain IDs, each containing lists
        // of destination tokens with associated block numbers.
        return (Object.values(this.l1TokensToDestinationTokensWithBlock)
            .flatMap((destinationTokens) => 
        // Map through destination chain IDs and their associated tokens
        Object.entries(destinationTokens).flatMap(([destinationChainId, tokensWithBlock]) => 
        // Map the tokens to add the l2ChainId field for each token
        tokensWithBlock.map((token) => ({
            ...token,
            l2ChainId: Number(destinationChainId),
        }))))
            // Filter out tokens whose blockNumber is outside the block range
            .filter((token) => token.blockNumber >= startingBlock && token.blockNumber <= endingBlock));
    }
    getLatestProposedRootBundle() {
        return this.proposedRootBundles[this.proposedRootBundles.length - 1];
    }
    getFollowingRootBundle(currentRootBundle) {
        const index = _.findLastIndex(this.proposedRootBundles, (bundle) => bundle.blockNumber === currentRootBundle.blockNumber);
        // If index of current root bundle is not found or is the last bundle, return undefined.
        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 &&
            // Note: We can use > instead of >= here because a leaf can never be executed in same block as its root
            // proposal due to bundle liveness enforced by HubPool. This importantly avoids the edge case
            // where the execution all leaves occurs in the same block as the next proposal, leading us to think
            // that the next proposal is fully executed when its not.
            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) {
        // Search for latest ProposeRootBundleExecuted event followed by all of its RootBundleExecuted event suggesting
        // that all pool rebalance leaves were executed. This ignores any proposed bundles that were partially executed.
        return _.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);
        });
    }
    // If n is negative, then return the Nth latest executed bundle, otherwise return the Nth earliest
    // executed bundle. Latest means most recent, earliest means oldest. N cannot be 0.
    // `startBlock` can be used to set the starting point from which we look forwards or backwards, depending
    // on whether n is positive or negative.
    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 is negative, then return the Nth latest executed bundle, otherwise return the Nth earliest
        // executed bundle.
        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;
                // Subtract 1 so that next `getLatestFullyExecutedRootBundle` call filters out the root bundle we just found
                // because its block number is > nextLatestMainnetBlock.
                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;
                // Add 1 so that next `getEarliestFullyExecutedRootBundle` call filters out the root bundle we just found
                // because its block number is < nextStartBlock.
                nextStartBlock = Math.min(bundleBlockNumber + 1, this.latestHeightSearched);
            }
        }
        return bundleToReturn;
    }
    getLatestBundleEndBlockForChain(chainIdList, latestMainnetBlock, chainId) {
        const latestFullyExecutedPoolRebalanceRoot = this.getLatestFullyExecutedRootBundle(latestMainnetBlock);
        // If no event, then we can return a conservative default starting block like 0,
        // or we could throw an Error.
        if (!latestFullyExecutedPoolRebalanceRoot) {
            return 0;
        }
        // Once this proposal event is found, determine its mapping of indices to chainId in its
        // bundleEvaluationBlockNumbers array using CHAIN_ID_LIST. For each chainId, their starting block number is that
        // chain's bundleEvaluationBlockNumber + 1 in this past proposal event.
        return this.getBundleEndBlockForChain(latestFullyExecutedPoolRebalanceRoot, chainId, chainIdList);
    }
    getNextBundleStartBlockNumber(chainIdList, latestMainnetBlock, chainId) {
        const endBlock = this.getLatestBundleEndBlockForChain(chainIdList, latestMainnetBlock, chainId);
        // This assumes that chain ID's are only added to the chain ID list over time, and that chains are never
        // deleted.
        return endBlock > 0 ? endBlock + 1 : 0;
    }
    // @dev Returns the start block of the next bundle assuming that if there is a currently outstanding root bundle proposal, it will pass liveness.
    getOptimisticBundleStartBlockNumber(chainIdList, latestMainnetBlock, chainId) {
        // If there is no pending root bundle, then return block ranges based on the latest fully executed root bundle.
        if (!this.hasPendingProposal()) {
            return this.getNextBundleStartBlockNumber(chainIdList, latestMainnetBlock, chainId);
        }
        // We cannot normally index `this.proposedRootBundles` since a bundle there may have been previously disputed, so only index `this.proposedRootBundles`
        // if we have a pending proposal, since this must mean that the pending root bundle is the most recent proposed root bundle.
        const latestProposedBundle = this.proposedRootBundles[this.proposedRootBundles.length - 1];
        // If there is no previous root bundle, then return 0.
        if (!isDefined(latestProposedBundle)) {
            return 0;
        }
        // Otherwise, get the bundle end block for the optimistic bundle.
        const optimisticEndBlock = this.getBundleEndBlockForChain(latestProposedBundle, chainId, chainIdList);
        // As above, this assumes that chain ID's are only added to the chain ID list over time, and that chains are never
        // deleted.
        return optimisticEndBlock > 0 ? optimisticEndBlock + 1 : 0;
    }
    getLatestExecutedRootBundleContainingL1Token(block, chain, l1Token) {
        // Search ExecutedRootBundles in descending block order to find the most recent event before the target block.
        return 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 = 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 (isUpdateFailureReason(searchConfig)) {
            return { success: false, reason: searchConfig };
        }
        const supportedEvents = Object.keys(hubPoolEvents);
        if (eventNames.some((eventName) => !supportedEvents.includes(eventName))) {
            return { success: false, reason: UpdateFailureReason.BadRequest };
        }
        const eventSearchConfigs = eventNames.map((eventName) => {
            const _searchConfig = { ...searchConfig }; // shallow copy
            // By default, an event's query range is controlled by the `searchConfig` passed in during
            // instantiation. However, certain events generally must be queried back to HubPool genesis.
            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) => 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: 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 !== UpdateFailureReason.AlreadyUpdated) {
                throw new Error(`Unable to update HubPoolClient: ${update.reason}`);
            }
            // No need to touch `this.isUpdated` because it should already be set from a previous update.
            return;
        }
        const { events, currentTime, pendingRootBundleProposal, searchEndBlock } = update;
        if (eventsToQuery.includes("CrossChainContractsSet")) {
            for (const event of events["CrossChainContractsSet"]) {
                const args = spreadEventWithBlockNumber(event);
                const dataToAdd = {
                    spokePool: EvmAddress.from(args.spokePool),
                    blockNumber: args.blockNumber,
                    txnRef: args.txnRef,
                    logIndex: args.logIndex,
                    txnIndex: args.txnIndex,
                    l2ChainId: args.l2ChainId,
                };
                // If the chain is SVM then the resulting SpokePool address will be inferred based on the lower 20
                // bytes of the address. Matching addresses will be promoted to a full 32-byte SvmAddress type.
                if (chainIsSvm(args.l2ChainId)) {
                    const solanaSpokePool = getDeployedAddress("SvmSpoke", args.l2ChainId);
                    if (!solanaSpokePool) {
                        throw new Error(`SVM spoke pool not found for chain ${args.l2ChainId}`);
                    }
                    const svmSpoke = SvmAddress.from(solanaSpokePool);
                    const truncatedAddress = svmSpoke.truncateToBytes20();
                    // Verify the event address matches our expected truncated address
                    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;
                }
                assign(this.crossChainContracts, [args.l2ChainId], [dataToAdd]);
            }
        }
        if (eventsToQuery.includes("SetPoolRebalanceRoute")) {
            for (const event of events["SetPoolRebalanceRoute"]) {
                const args = spreadEventWithBlockNumber(event);
                // If the destination chain is SVM, then we need to convert the destination token to the Solana address.
                // This is because the HubPool contract only holds a truncated address for the USDC token and currently
                // only supports USDC as a destination token for Solana.
                let destinationToken = EvmAddress.from(args.destinationToken);
                if (chainIsSvm(args.destinationChainId)) {
                    const usdcTokenSol = TOKEN_SYMBOLS_MAP.USDC.addresses[args.destinationChainId];
                    const svmUsdc = 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: 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]) {
                    // Events are most likely coming in descending orders already but just in case we sort them again.
                    this.l1TokensToDestinationTokensWithBlock[args.l1Token][args.destinationChainId] = sortEventsDescending([
                        ...this.l1TokensToDestinationTokensWithBlock[args.l1Token][args.destinationChainId],
                        newRoute,
                    ]);
                }
                else {
                    assign(this.l1TokensToDestinationTokensWithBlock, [args.l1Token, args.destinationChainId], [newRoute]);
                }
            }
        }
        // For each enabled Lp token fetch the token symbol and decimals from the token contract. Note this logic will
        // only run iff a new token has been enabled. Will only append iff the info is not there already.
        // Filter out any duplicate addresses. This might happen due to enabling, disabling and re-enabling a token.
        if (eventsToQuery.includes("L1TokenEnabledForLiquidityProvision")) {
            const uniqueL1Tokens = dedupArray(events["L1TokenEnabledForLiquidityProvision"].map((event) => String(event.args["l1Token"])));
            const [tokenInfo, lpTokenInfo] = await Promise.all([
                Promise.all(uniqueL1Tokens.map(async (l1Token) => {
                    const tokenInfo = await fetchTokenInfo(l1Token, this.hubPool.provider);
                    return {
                        ...tokenInfo,
                        address: 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 = spreadEventWithBlockNumber(_event);
                return {
                    ...args,
                    proposer: EvmAddress.from(args.proposer),
                };
            }));
        }
        if (eventsToQuery.includes("RootBundleCanceled")) {
            this.canceledRootBundles.push(...events["RootBundleCanceled"].map((event) => spreadEventWithBlockNumber(event)));
        }
        if (eventsToQuery.includes("RootBundleDisputed")) {
            this.disputedRootBundles.push(...events["RootBundleDisputed"].map((event) => spreadEventWithBlockNumber(event)));
        }
        if (eventsToQuery.includes("RootBundleExecuted")) {
            for (const event of events["RootBundleExecuted"]) {
                if (this.configOverride.ignoredHubExecutedBundles.includes(event.blockNumber)) {
                    continue;
                }
                // Set running balances and incentive balances for this bundle.
                const executedRootBundle = spreadEventWithBlockNumber(event);
                const { l1Tokens, runningBalances } = executedRootBundle;
                const nTokens = l1Tokens.length;
                // Safeguard
                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) => EvmAddress.from(l1Token)),
                });
            }
        }
        // If the contract's current rootBundleProposal() value has an unclaimedPoolRebalanceLeafCount > 0, then
        // it means that either the root bundle proposal is in the challenge period and can be disputed, or it has
        // passed the challenge period and pool rebalance leaves can be executed. Once all leaves are executed, the
        // unclaimed count will drop to 0 and at that point there is nothing more that we can do with this root bundle
        // besides proposing another one.
        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) => {
                        // Ideally, the HubPool.sol contract should limit the size of the elements within the
                        // bundleEvaluationBlockNumbers array. But because it doesn't, we wrap the cast of BN --> Number
                        // in a try/catch statement and return some value that would always be disputable.
                        // This catches the denial of service attack vector where a malicious proposer proposes with bundle block
                        // evaluation block numbers larger than what BigNumber::toNumber() can handle.
                        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; // Next iteration should start off from where this one ended.
        this.eventSearchConfig.to = undefined; // Caller can re-set on subsequent updates if necessary.
        this.isUpdated = true;
        this.logger.debug({ at: "HubPoolClient::update", message: "HubPool client updated!", searchEndBlock, currentTime });
    }
    // Returns end block for `chainId` in ProposedRootBundle.bundleBlockEvalNumbers. Looks up chainId
    // in chainId list, gets the index where its located, and returns the value of the index in
    // bundleBlockEvalNumbers. Returns 0 if `chainId` can't be found in `chainIdList` and if index doesn't
    // exist in bundleBlockEvalNumbers.
    getBundleEndBlockForChain(proposeRootBundleEvent, chainId, chainIdList) {
        const bundleEvaluationBlockNumbers = proposeRootBundleEvent.bundleEvaluationBlockNumbers;
        const chainIdIndex = chainIdList.indexOf(chainId);
        if (chainIdIndex === -1) {
            return 0;
        }
        // Sometimes, the root bundle event's chain ID list will update from bundle to bundle, so we need to check that
        // the bundle evaluation block number list is long enough to contain this index. We assume that chain ID's
        // are only added to the bundle block list, never deleted.
        if (chainIdIndex >= bundleEvaluationBlockNumbers.length) {
            return 0;
        }
        return bundleEvaluationBlockNumbers[chainIdIndex].toNumber();
    }
}
//# sourceMappingURL=HubPoolClient.js.map