import { fillStatusArray, findDepositBlock, getMaxFillDeadlineInRange as getMaxFillDeadline, getTimeAt as _getTimeAt, relayFillStatus, getTimestampForBlock as _getTimestampForBlock, } from "../../arch/evm";
import { relayFillStatus as relayFillStatusTvm, getMaxFillDeadlineInRange as getMaxFillDeadlineTvm, getTimeAt as _getTimeAtTvm, findDepositBlock as findDepositBlockTvm, } from "../../arch/tvm";
import { getNetworkName, InvalidFill, EvmAddress, unpackDepositEvent, chainIsTvm, } from "../../utils";
import { logToSortableEvent, paginatedEventQuery, sortEventsAscendingInPlace, spreadEventWithBlockNumber, } from "../../utils/EventUtils";
import { isUpdateFailureReason } from "../BaseAbstractClient";
import { knownEventNames, SpokePoolClient } from "./SpokePoolClient";
import { EVM_SPOKE_POOL_CLIENT_TYPE } from "./types";
/**
 * An EVM-specific SpokePoolClient.
 */
export class EVMSpokePoolClient extends SpokePoolClient {
    spokePool;
    type = EVM_SPOKE_POOL_CLIENT_TYPE;
    tvm;
    constructor(logger, spokePool, hubPoolClient, chainId, deploymentBlock, eventSearchConfig = { from: 0, maxLookBack: 0 }) {
        super(logger, hubPoolClient, chainId, deploymentBlock, eventSearchConfig);
        this.spokePool = spokePool;
        this.spokePoolAddress = EvmAddress.from(spokePool.address);
        this.tvm = chainIsTvm(this.chainId);
    }
    relayFillStatus(relayData, atHeight) {
        const fillStatusHandler = this.tvm ? relayFillStatusTvm : relayFillStatus;
        return fillStatusHandler(this.spokePool, relayData, atHeight, this.chainId);
    }
    fillStatusArray(relayData, atHeight) {
        return fillStatusArray(this.spokePool, relayData, atHeight);
    }
    getMaxFillDeadlineInRange(startBlock, endBlock) {
        const maxFillDeadlineInRangeHandler = this.tvm ? getMaxFillDeadlineTvm : getMaxFillDeadline;
        return maxFillDeadlineInRangeHandler(this.spokePool, startBlock, endBlock);
    }
    _availableEventsOnSpoke(eventNames = knownEventNames) {
        return Object.fromEntries(this.spokePool.interface.fragments
            .filter(({ name, type }) => type === "event" && eventNames.includes(name))
            .map(({ name }) => [name, this.spokePool.filters[name]()]));
    }
    _queryableEventNames() {
        return Object.keys(this._availableEventsOnSpoke(knownEventNames));
    }
    /**
     * Retrieve the on-chain time at a specific block.
     * Reads the block timestamp, which is well-supported for historical queries on both EVM and TVM.
     * @param blockNumber The block number to query.
     * @returns The on-chain time as a number.
     */
    async _getCurrentTime(blockNumber) {
        const block = await this.spokePool.provider.getBlock(blockNumber);
        const currentTime = block.timestamp;
        if (currentTime < this.currentTime) {
            throw new Error(`EVMSpokePoolClient::_getCurrentTime: currentTime: ${currentTime} < ${this.currentTime}`);
        }
        return currentTime;
    }
    async _update(eventsToQuery) {
        const searchConfig = await this.updateSearchConfig(this.spokePool.provider);
        if (isUpdateFailureReason(searchConfig)) {
            const reason = searchConfig;
            return { success: false, reason };
        }
        const eventSearchConfigs = eventsToQuery.map((eventName) => {
            if (!this._queryableEventNames().includes(eventName)) {
                throw new Error(`SpokePoolClient: Cannot query unrecognised SpokePool event name: ${eventName}`);
            }
            const _searchConfig = { ...searchConfig }; // shallow copy
            // By default, an event's query range is controlled by the `eventSearchConfig` passed in during instantiation.
            // However, certain events have special overriding requirements to their search ranges:
            // - EnabledDepositRoute: The full history is always required, so override the requested fromBlock.
            if (eventName === "EnabledDepositRoute" && !this.isUpdated) {
                _searchConfig.from = this.deploymentBlock;
            }
            return {
                filter: this._availableEventsOnSpoke()[eventName],
                searchConfig: _searchConfig,
            };
        });
        const { spokePool } = this;
        this.log("debug", `Updating SpokePool client for chain ${this.chainId}`, {
            eventsToQuery,
            searchConfig,
            spokePool: spokePool.address,
        });
        const timerStart = Date.now();
        const [currentTime, ...events] = await Promise.all([
            this._getCurrentTime(searchConfig.to),
            ...eventSearchConfigs.map((config) => paginatedEventQuery(this.spokePool, config.filter, config.searchConfig)),
        ]);
        this.log("debug", `Time to query new events from RPC for ${this.chainId}: ${Date.now() - timerStart} ms`);
        if (currentTime < this.currentTime) {
            throw new Error(`EVMSpokePoolClient::update: currentTime: ${currentTime} < ${this.currentTime}`);
        }
        // Sort all events to ensure they are stored in a consistent order.
        events.forEach((events) => sortEventsAscendingInPlace(events.map(logToSortableEvent)));
        // Map events to SortableEvent
        const eventsWithBlockNumber = events.map((eventList) => eventList.map((event) => spreadEventWithBlockNumber(event)));
        return {
            success: true,
            currentTime,
            searchEndBlock: searchConfig.to,
            events: eventsWithBlockNumber,
        };
    }
    getTimeAt(blockNumber) {
        const getTimeAtHandler = this.tvm ? _getTimeAtTvm : _getTimeAt;
        return getTimeAtHandler(this.spokePool, blockNumber);
    }
    async findDeposit(depositId) {
        let deposit = this.getDeposit(depositId);
        if (deposit) {
            return { found: true, deposit };
        }
        // No deposit found; revert to searching for it.
        const result = await this.queryDepositEvents(depositId);
        if ("reason" in result) {
            return { found: false, code: InvalidFill.DepositIdNotFound, reason: result.reason };
        }
        const { event, elapsedMs } = result;
        const partialDeposit = unpackDepositEvent(spreadEventWithBlockNumber(event), this.chainId);
        const quoteBlockNumber = await this.getBlockNumber(partialDeposit.quoteTimestamp);
        const outputToken = partialDeposit.outputToken.isZeroAddress()
            ? this.getDestinationTokenForDeposit({ ...partialDeposit, quoteBlockNumber })
            : partialDeposit.outputToken;
        deposit = {
            ...partialDeposit,
            outputToken,
            quoteBlockNumber,
            fromLiteChain: this.isOriginLiteChain(partialDeposit),
            toLiteChain: this.isDestinationLiteChain(partialDeposit),
        };
        this.logger.debug({
            at: "SpokePoolClient#findDeposit",
            message: "Located deposit outside of SpokePoolClient's search range",
            deposit,
            elapsedMs,
        });
        return { found: true, deposit };
    }
    getTimestampForBlock(blockNumber) {
        return _getTimestampForBlock(this.spokePool.provider, blockNumber);
    }
    /**
     * Find the block at which a deposit was created.
     * EVM uses a binary-search over historical numberOfDeposits().
     * TVM overrides this with an event-based lookup.
     */
    _findDepositBlock(depositId, lowBlock, highBlock) {
        const findDepositBlockHandler = this.tvm ? findDepositBlockTvm : findDepositBlock;
        return findDepositBlockHandler(this.spokePool, depositId, lowBlock, highBlock);
    }
    async queryDepositEvents(depositId) {
        const tStart = Date.now();
        const upperBound = this.latestHeightSearched || undefined;
        const from = await this._findDepositBlock(depositId, this.deploymentBlock, upperBound);
        const chain = getNetworkName(this.chainId);
        if (!from) {
            return {
                reason: `Unable to find ${chain} depositId ${depositId} within blocks [${this.deploymentBlock}, ${upperBound ?? "latest"}].`,
            };
        }
        const to = from;
        const { maxLookBack } = this.eventSearchConfig;
        const events = (await Promise.all([
            paginatedEventQuery(this.spokePool, this.spokePool.filters.V3FundsDeposited(null, null, null, null, null, depositId), { from, to, maxLookBack }),
            paginatedEventQuery(this.spokePool, this.spokePool.filters.FundsDeposited(null, null, null, null, null, depositId), { from, to, maxLookBack }),
        ]))
            .flat()
            .filter(({ args }) => args["depositId"].eq(depositId));
        const tStop = Date.now();
        const [event] = events;
        if (!event) {
            return {
                reason: `Unable to find ${chain} depositId ${depositId} within blocks [${from}, ${upperBound ?? "latest"}].`,
            };
        }
        return { event, elapsedMs: tStop - tStart };
    }
}
//# sourceMappingURL=EVMSpokePoolClient.js.map