"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SpokePoolClient = exports.knownEventNames = void 0;
const ethers_1 = require("ethers");
const utils_1 = require("../../utils");
const EventUtils_1 = require("../../utils/EventUtils");
const constants_1 = require("../../constants");
const BaseAbstractClient_1 = require("../BaseAbstractClient");
const BundleDataClient_1 = require("../BundleDataClient");
exports.knownEventNames = [
    "EnabledDepositRoute",
    "TokensBridged",
    "RelayedRootBundle",
    "ExecutedRelayerRefundRoot",
    "V3FundsDeposited",
    "FundsDeposited",
    "RequestedSpeedUpV3Deposit",
    "RequestedSpeedUpDeposit",
    "RequestedV3SlowFill",
    "RequestedSlowFill",
    "FilledV3Relay",
    "FilledRelay",
    "BridgedToHubPool",
    "ClaimedRelayerRefund",
];
class SpokePoolClient extends BaseAbstractClient_1.BaseAbstractClient {
    logger;
    hubPoolClient;
    chainId;
    deploymentBlock;
    currentTime = 0;
    duplicateDepositHashes = {};
    depositHashesToFills = {};
    speedUps = {};
    slowFillRequests = {};
    depositRoutes = {};
    tokensBridged = [];
    rootBundleRelays = [];
    relayerRefundExecutions = [];
    claimedRelayerRefunds = [];
    bridgedToHubPool = [];
    configStoreClient;
    invalidFills = new Set();
    depositHashes = {};
    spokePoolAddress;
    fills = {};
    constructor(logger, hubPoolClient, chainId, deploymentBlock, eventSearchConfig = { from: 0, maxLookBack: 0 }) {
        super(eventSearchConfig);
        this.logger = logger;
        this.hubPoolClient = hubPoolClient;
        this.chainId = chainId;
        this.deploymentBlock = deploymentBlock;
        this.firstHeightToSearch = eventSearchConfig.from;
        this.latestHeightSearched = 0;
        this.configStoreClient = hubPoolClient?.configStoreClient;
    }
    getDepositsForDestinationChain(destinationChainId) {
        return Object.values(this.depositHashes).filter((deposit) => deposit.destinationChainId === destinationChainId);
    }
    _getDuplicateDeposits(deposit) {
        const depositHash = (0, utils_1.getRelayEventKey)(deposit);
        return this.duplicateDepositHashes[depositHash] ?? [];
    }
    getDepositsForDestinationChainWithDuplicates(destinationChainId) {
        const deposits = this.getDepositsForDestinationChain(destinationChainId);
        const duplicateDeposits = deposits.reduce((acc, deposit) => {
            const duplicates = this._getDuplicateDeposits(deposit);
            return acc.concat(duplicates);
        }, []);
        return (0, EventUtils_1.sortEventsAscendingInPlace)(deposits.concat(duplicateDeposits.flat()));
    }
    getDeposits(filter) {
        let deposits = Object.values(this.depositHashes);
        if ((0, utils_1.isDefined)(filter)) {
            const { fromBlock, toBlock } = filter;
            deposits = deposits.filter(({ blockNumber }) => blockNumber >= fromBlock && toBlock >= blockNumber);
        }
        return (0, EventUtils_1.sortEventsAscendingInPlace)(deposits);
    }
    getTokensBridged() {
        return this.tokensBridged;
    }
    getDepositRoutes() {
        return this.depositRoutes;
    }
    getFills() {
        return (0, EventUtils_1.sortEventsAscendingInPlace)(Object.values(this.fills).flat());
    }
    getFillsForOriginChain(originChainId) {
        return this.fills[originChainId] || [];
    }
    getFillsForRelayer(relayer) {
        return this.getFills().filter((fill) => fill.relayer.eq(relayer));
    }
    getFillsWithBlockInRange(startingBlock, endingBlock) {
        return this.getFills().filter((fill) => fill.blockNumber >= startingBlock && fill.blockNumber <= endingBlock);
    }
    getRootBundleRelays() {
        return this.rootBundleRelays;
    }
    getLatestRootBundleId() {
        return this.rootBundleRelays.length > 0
            ? this.rootBundleRelays[this.rootBundleRelays.length - 1]?.rootBundleId + 1
            : 0;
    }
    getRelayerRefundExecutions() {
        return this.relayerRefundExecutions;
    }
    getClaimedRelayerRefunds() {
        return this.claimedRelayerRefunds;
    }
    getBridgedToHubPoolEvents() {
        return this.bridgedToHubPool;
    }
    appendMaxSpeedUpSignatureToDeposit(deposit) {
        const { depositId, depositor } = deposit;
        const speedups = this.speedUps[depositor.toEvmAddress()]?.[depositId.toString()];
        if (!(0, utils_1.isDefined)(speedups) || speedups.length === 0) {
            return deposit;
        }
        const maxSpeedUp = speedups.reduce((prev, current) => prev.updatedOutputAmount.lt(current.updatedOutputAmount) ? prev : current);
        if (maxSpeedUp.updatedOutputAmount.gte(deposit.outputAmount)) {
            return deposit;
        }
        const updatedDeposit = {
            ...deposit,
            speedUpSignature: maxSpeedUp.depositorSignature,
            updatedOutputAmount: maxSpeedUp.updatedOutputAmount,
            updatedRecipient: maxSpeedUp.updatedRecipient,
            updatedMessage: maxSpeedUp.updatedMessage,
        };
        return updatedDeposit;
    }
    getDeposit(depositId) {
        return Object.values(this.depositHashes).find(({ depositId: _depositId }) => _depositId.eq(depositId));
    }
    getSlowFillRequests() {
        return (0, EventUtils_1.sortEventsAscendingInPlace)(Object.values(this.slowFillRequests));
    }
    getSlowFillRequest(relayData) {
        const hash = (0, utils_1.getRelayEventKey)({ ...relayData, destinationChainId: this.chainId });
        return this.slowFillRequests[hash];
    }
    getSlowFillRequestsForOriginChain(originChainId) {
        return Object.values(this.slowFillRequests).filter((e) => e.originChainId === originChainId);
    }
    getSpeedUps() {
        return this.speedUps;
    }
    getDepositForFill(fill) {
        const deposit = this.depositHashes[(0, utils_1.getRelayEventKey)(fill)];
        const match = (0, utils_1.validateFillForDeposit)(fill, deposit);
        return match.valid ? deposit : undefined;
    }
    getFillsForDeposit(deposit) {
        return this.depositHashesToFills[this.getDepositHash(deposit)] ?? [];
    }
    isDepositFilled(deposit) {
        const depositHash = this.getDepositHash(deposit);
        const fills = this.depositHashesToFills[depositHash] ?? [];
        return fills.some((fill) => (0, utils_1.validateFillForDeposit)(fill, deposit).valid);
    }
    getValidUnfilledAmountForDeposit(deposit) {
        const { outputAmount, originChainId } = deposit;
        const fillsForDeposit = this.depositHashesToFills[this.getDepositHash(deposit)];
        if (fillsForDeposit === undefined || fillsForDeposit.length === 0) {
            return { unfilledAmount: outputAmount, fillCount: 0, invalidFills: [] };
        }
        const { validFills, invalidFills, unrepayableFills } = fillsForDeposit.reduce((groupedFills, fill) => {
            if ((0, utils_1.validateFillForDeposit)(fill, deposit).valid) {
                const fillRepaymentData = {
                    ...fill,
                    fromLiteChain: deposit.fromLiteChain,
                };
                const { chainToSendRefundTo: repaymentChainId } = (0, BundleDataClient_1.getRefundInformationFromFill)(fillRepaymentData, this.hubPoolClient, this.hubPoolClient.latestHeightSearched);
                if (this.hubPoolClient && !(0, utils_1.isSlowFill)(fill) && !fill.relayer.isValidOn(repaymentChainId)) {
                    groupedFills.unrepayableFills.push(fill);
                }
                groupedFills.validFills.push(fill);
            }
            else {
                groupedFills.invalidFills.push(fill);
            }
            return groupedFills;
        }, { validFills: [], invalidFills: [], unrepayableFills: [] });
        const logLevel = (0, utils_1.chainIsProd)(originChainId) ? "warn" : "debug";
        const unrepayableFillsForDeposit = unrepayableFills.filter((x) => x.depositId.eq(deposit.depositId));
        if (unrepayableFillsForDeposit.length > 0) {
            this.logger[logLevel]({
                at: "SpokePoolClient",
                chainId: this.chainId,
                message: "Unrepayable fills found where we need to switch repayment address and or chain",
                deposit,
                unrepayableFills: Object.fromEntries(unrepayableFillsForDeposit.map((x) => [x.relayer.toNative(), x])),
                notificationPath: "across-unrepayable-fills",
            });
        }
        if (validFills.length === 0) {
            return { unfilledAmount: outputAmount, fillCount: 0, invalidFills };
        }
        return {
            unfilledAmount: utils_1.bnZero,
            fillCount: validFills.length,
            invalidFills,
        };
    }
    getDepositHash(event) {
        return `${event.depositId.toString()}-${event.originChainId}`;
    }
    canResolveZeroAddressOutputToken(deposit) {
        if (!this.hubPoolClient?.l2TokenHasPoolRebalanceRoute(deposit.inputToken, deposit.originChainId, deposit.quoteBlockNumber)) {
            return false;
        }
        const l1Token = this.hubPoolClient?.getL1TokenForL2TokenAtBlock(deposit.inputToken, deposit.originChainId, deposit.quoteBlockNumber);
        if (!l1Token) {
            return false;
        }
        return this.hubPoolClient.l2TokenEnabledForL1TokenAtBlock(l1Token, deposit.destinationChainId, deposit.quoteBlockNumber);
    }
    async update(eventsToQuery = this._queryableEventNames()) {
        const duplicateEvents = [];
        if (this.hubPoolClient !== null && !this.hubPoolClient.isUpdated) {
            throw new Error("HubPoolClient not updated");
        }
        const update = await this._update(eventsToQuery);
        if (!update.success) {
            return;
        }
        const { events: queryResults, currentTime, searchEndBlock } = update;
        if (eventsToQuery.includes("TokensBridged")) {
            for (const _event of queryResults[eventsToQuery.indexOf("TokensBridged")]) {
                const event = _event;
                this.tokensBridged.push({
                    ...event,
                    l2TokenAddress: (0, utils_1.toAddressType)(event.l2TokenAddress, this.chainId),
                });
            }
        }
        const queryDepositEvents = async (eventName) => {
            const depositEvents = (queryResults[eventsToQuery.indexOf(eventName)] ?? []).map((event) => (0, utils_1.unpackDepositEvent)(event, this.chainId));
            const quoteBlockNumbers = await this.getBlockNumbers(depositEvents.map((e) => e.quoteTimestamp));
            for (const event of depositEvents) {
                const quoteBlockNumber = quoteBlockNumbers[Number(event.quoteTimestamp)];
                const outputToken = event.outputToken.isZeroAddress()
                    ? this.getDestinationTokenForDeposit({ ...event, quoteBlockNumber })
                    : event.outputToken;
                const deposit = {
                    ...event,
                    outputToken,
                    quoteBlockNumber,
                    fromLiteChain: this.isOriginLiteChain(event),
                    toLiteChain: this.isDestinationLiteChain(event),
                };
                if (this.depositHashes[(0, utils_1.getRelayEventKey)(deposit)] !== undefined) {
                    const allDeposits = this._getDuplicateDeposits(deposit).concat(this.depositHashes[(0, utils_1.getRelayEventKey)(deposit)]);
                    if (allDeposits.some((e) => (0, EventUtils_1.duplicateEvent)(deposit, e))) {
                        duplicateEvents.push(event);
                        continue;
                    }
                    (0, utils_1.assign)(this.duplicateDepositHashes, [(0, utils_1.getRelayEventKey)(deposit)], [deposit]);
                    continue;
                }
                (0, utils_1.assign)(this.depositHashes, [(0, utils_1.getRelayEventKey)(deposit)], deposit);
            }
        };
        for (const event of ["FundsDeposited"]) {
            if (eventsToQuery.includes(event)) {
                await queryDepositEvents(event);
            }
        }
        const querySpeedUpDepositEvents = (eventName) => {
            const speedUpEvents = (queryResults[eventsToQuery.indexOf(eventName)] ?? [])
                .map((_event) => {
                const event = _event;
                const invalid = [event.depositor, event.updatedRecipient].some((addr) => !utils_1.EvmAddress.validate(ethers_1.utils.arrayify(addr)));
                if (invalid) {
                    return;
                }
                return {
                    ...event,
                    depositor: utils_1.EvmAddress.from(event.depositor),
                    updatedRecipient: utils_1.EvmAddress.from(event.updatedRecipient),
                };
            })
                .filter(utils_1.isDefined);
            for (const event of speedUpEvents) {
                const speedUp = {
                    ...event,
                    originChainId: this.chainId,
                };
                (0, utils_1.assign)(this.speedUps, [speedUp.depositor.toEvmAddress(), speedUp.depositId.toString()], [speedUp]);
                const deposit = this.getDeposit(speedUp.depositId);
                if ((0, utils_1.isDefined)(deposit) && (0, utils_1.chainIsEvm)(deposit.destinationChainId) && deposit.depositor.eq(speedUp.depositor)) {
                    const eventKey = (0, utils_1.getRelayEventKey)(deposit);
                    this.depositHashes[eventKey] = this.appendMaxSpeedUpSignatureToDeposit(deposit);
                }
            }
        };
        ["RequestedSpeedUpDeposit"].forEach((event) => {
            if (eventsToQuery.includes(event)) {
                querySpeedUpDepositEvents(event);
            }
        });
        const queryRequestedSlowFillEvents = (eventName) => {
            const destinationChainId = this.chainId;
            const slowFillRequests = (queryResults[eventsToQuery.indexOf(eventName)] ?? []).map((_event) => {
                const event = _event;
                return {
                    ...event,
                    destinationChainId,
                    depositor: (0, utils_1.toAddressType)(event.depositor, event.originChainId),
                    recipient: (0, utils_1.toAddressType)(event.recipient, destinationChainId),
                    inputToken: (0, utils_1.toAddressType)(event.inputToken, event.originChainId),
                    outputToken: (0, utils_1.toAddressType)(event.outputToken, destinationChainId),
                    exclusiveRelayer: (0, utils_1.toAddressType)(event.exclusiveRelayer, destinationChainId),
                };
            });
            for (const slowFillRequest of slowFillRequests) {
                const depositHash = (0, utils_1.getRelayEventKey)(slowFillRequest);
                if (this.slowFillRequests[depositHash] !== undefined) {
                    duplicateEvents.push(slowFillRequest);
                    continue;
                }
                this.slowFillRequests[depositHash] ??= slowFillRequest;
            }
        };
        ["RequestedSlowFill"].forEach((event) => {
            if (eventsToQuery.includes(event)) {
                queryRequestedSlowFillEvents(event);
            }
        });
        const queryFilledRelayEvents = (eventName) => {
            const fillEvents = (queryResults[eventsToQuery.indexOf(eventName)] ?? []).map((event) => (0, utils_1.unpackFillEvent)(event, this.chainId));
            if (fillEvents.length > 0) {
                this.log("debug", `Using ${fillEvents.length} newly queried ${eventName} events for chain ${this.chainId}`, {
                    earliestEvent: fillEvents[0].blockNumber,
                });
            }
            for (const fill of fillEvents) {
                const duplicateFill = this.fills[fill.originChainId]?.find((f) => (0, EventUtils_1.duplicateEvent)(fill, f));
                if (duplicateFill) {
                    duplicateEvents.push(fill);
                    continue;
                }
                (0, utils_1.assign)(this.fills, [fill.originChainId], [fill]);
                (0, utils_1.assign)(this.depositHashesToFills, [this.getDepositHash(fill)], [fill]);
            }
        };
        ["FilledRelay"].forEach((event) => {
            if (eventsToQuery.includes(event)) {
                queryFilledRelayEvents(event);
            }
        });
        if (eventsToQuery.includes("EnabledDepositRoute")) {
            const enableDepositsEvents = queryResults[eventsToQuery.indexOf("EnabledDepositRoute")].map((_event) => {
                const event = _event;
                return {
                    ...event,
                    originToken: (0, utils_1.toAddressType)(event.originToken, constants_1.CHAIN_IDs.MAINNET),
                };
            });
            for (const event of enableDepositsEvents) {
                (0, utils_1.assign)(this.depositRoutes, [event.originToken.toBytes32(), event.destinationChainId], event.enabled);
            }
        }
        if (eventsToQuery.includes("RelayedRootBundle")) {
            const relayedRootBundleEvents = queryResults[eventsToQuery.indexOf("RelayedRootBundle")];
            for (const event of relayedRootBundleEvents) {
                this.rootBundleRelays.push(event);
            }
        }
        if (eventsToQuery.includes("ExecutedRelayerRefundRoot")) {
            const refundEvents = queryResults[eventsToQuery.indexOf("ExecutedRelayerRefundRoot")];
            for (const _event of refundEvents) {
                const event = _event;
                this.relayerRefundExecutions.push({
                    ...event,
                    l2TokenAddress: (0, utils_1.toAddressType)(event.l2TokenAddress, this.chainId),
                    refundAddresses: event.refundAddresses.map((addr) => (0, utils_1.toAddressType)(addr, this.chainId)),
                });
            }
        }
        if (eventsToQuery.includes("ClaimedRelayerRefund")) {
            const claimedRelayerRefundEvents = (queryResults[eventsToQuery.indexOf("ClaimedRelayerRefund")] ??
                []);
            for (const event of claimedRelayerRefundEvents) {
                this.claimedRelayerRefunds.push({
                    ...event,
                    amount: event.amount || event.claimAmount,
                });
            }
        }
        if (eventsToQuery.includes("BridgedToHubPool")) {
            const bridgedToHubPoolEvents = (queryResults[eventsToQuery.indexOf("BridgedToHubPool")] ??
                []);
            for (const event of bridgedToHubPoolEvents) {
                this.bridgedToHubPool.push(event);
            }
        }
        if (duplicateEvents.length > 0) {
            this.log("debug", "Duplicate events listed", {
                duplicateEvents,
            });
            this.log("error", "Duplicate events detected, check debug logs");
        }
        this.currentTime = currentTime;
        this.latestHeightSearched = searchEndBlock;
        this.firstHeightToSearch = searchEndBlock + 1;
        this.eventSearchConfig.to = undefined;
        this.isUpdated = true;
        this.log("debug", `SpokePool client for chain ${this.chainId} updated!`, {
            nextFirstHeightToSearch: this.firstHeightToSearch,
        });
    }
    getBlockNumber(timestamp) {
        return this.hubPoolClient?.getBlockNumber(timestamp) ?? Promise.resolve(utils_1.MAX_BIG_INT.toNumber());
    }
    getBlockNumbers(timestamps) {
        return (this.hubPoolClient?.getBlockNumbers(timestamps) ??
            Promise.resolve(Object.fromEntries(timestamps.map((timestamp) => [timestamp, utils_1.MAX_BIG_INT.toNumber()]))));
    }
    getDestinationTokenForDeposit(deposit) {
        if (!this.canResolveZeroAddressOutputToken(deposit)) {
            return (0, utils_1.toAddressType)(constants_1.ZERO_ADDRESS, constants_1.CHAIN_IDs.MAINNET);
        }
        const l1Token = this.hubPoolClient.getL1TokenForL2TokenAtBlock(deposit.inputToken, deposit.originChainId, deposit.quoteBlockNumber);
        const counterpartToken = this.hubPoolClient.getL2TokenForL1TokenAtBlock(l1Token, deposit.destinationChainId, deposit.quoteBlockNumber);
        return counterpartToken ?? (0, utils_1.toAddressType)(constants_1.ZERO_ADDRESS, constants_1.CHAIN_IDs.MAINNET);
    }
    log(level, message, data) {
        this.logger[level]({ at: "SpokePoolClient", chainId: this.chainId, message, ...data });
    }
    getCurrentTime() {
        return this.currentTime;
    }
    isOriginLiteChain(deposit) {
        return this.configStoreClient?.isChainLiteChainAtTimestamp(deposit.originChainId, deposit.quoteTimestamp) ?? false;
    }
    isDestinationLiteChain(deposit) {
        return (this.configStoreClient?.isChainLiteChainAtTimestamp(deposit.destinationChainId, deposit.quoteTimestamp) ?? false);
    }
}
exports.SpokePoolClient = SpokePoolClient;
//# sourceMappingURL=SpokePoolClient.js.map