"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AcrossConfigStoreClient = exports.GLOBAL_CONFIG_STORE_KEYS = exports.DEFAULT_CONFIG_STORE_VERSION = void 0;
const tslib_1 = require("tslib");
const assert_1 = tslib_1.__importDefault(require("assert"));
const typeguards_1 = require("../../typeguards");
const evm_1 = require("../../arch/evm");
const utils_1 = require("../../utils");
const constants_1 = require("../../constants");
const JSONUtils_1 = require("../../utils/JSONUtils");
const BaseAbstractClient_1 = require("../BaseAbstractClient");
const rateModel_1 = require("../../lpFeeCalculator/rateModel");
exports.DEFAULT_CONFIG_STORE_VERSION = 0;
var GLOBAL_CONFIG_STORE_KEYS;
(function (GLOBAL_CONFIG_STORE_KEYS) {
    GLOBAL_CONFIG_STORE_KEYS["MAX_RELAYER_REPAYMENT_LEAF_SIZE"] = "MAX_RELAYER_REPAYMENT_LEAF_SIZE";
    GLOBAL_CONFIG_STORE_KEYS["MAX_POOL_REBALANCE_LEAF_SIZE"] = "MAX_POOL_REBALANCE_LEAF_SIZE";
    GLOBAL_CONFIG_STORE_KEYS["VERSION"] = "VERSION";
    GLOBAL_CONFIG_STORE_KEYS["DISABLED_CHAINS"] = "DISABLED_CHAINS";
    GLOBAL_CONFIG_STORE_KEYS["CHAIN_ID_INDICES"] = "CHAIN_ID_INDICES";
    GLOBAL_CONFIG_STORE_KEYS["LITE_CHAIN_ID_INDICES"] = "LITE_CHAIN_ID_INDICES";
})(GLOBAL_CONFIG_STORE_KEYS || (exports.GLOBAL_CONFIG_STORE_KEYS = GLOBAL_CONFIG_STORE_KEYS = {}));
const KNOWN_INVALID_TOKEN_CONFIG_UPDATE_HASHES = [
    "0x422abc617c6598e4b91859f99c392939d2034c1a839a342a963a34a2f0390195",
    "0x36c85e388279714b2c98d46e3377dc37a1575665b2cac5e52fe97d8d77efcd2b",
    "0x6f0a93119e538dd84e02adfce821fb4e6dd9baddcceb041977e8ba3c39185ab8",
    "0xc28d8bb445e0b747201e6f98ee62aa03009f4c04b8d6f9fad8f214ec1166463d",
    "0x3b0719ef1e3cae2dc1a854a1012332a288e50ad24adc52861d42bcc30fd3deaf",
    "0xbae5c792f74d9f0b6554acf793df0d6b3610868bd6f6a377371b9dec10038003",
    "0xd983142980ac2451e913b152413e769f7a7007fe7305c2e8a03db432e892f84c",
    "0xf64610347950488503428fd720132f8188aa26dcc48e3fc9a89b7bc24aa7fda2",
    "0x1970fcd1e5d5d6cf3bbb640d30d5e471ce5161d65580cedb388526a32b2f7638",
    "0xf098c547d726be8fda419faaee1850280ded1ea75a1b10f4a1614805fa4207d3",
    "0xbfa181663761a78c66dd2c7012604eb910c4c39bad17089e2cc4a011ccc0e981",
    "0x89830f5e81b9e8b44ac2f8966b2fa4bf8e71d7f546e2bc0e773d8ee8df4bdb36",
    "0xb0ad6270124c925a234d9c4f87b60396f2b52fdc250cd2fc9cac792d0d62e467",
    "0x779bc3bf2dba1128d5dda6be8ae99b503cae23343a7265a86bca3d5572ed4268",
].map((hash) => hash.toLowerCase());
class AcrossConfigStoreClient extends BaseAbstractClient_1.BaseAbstractClient {
    logger;
    configStore;
    configStoreVersion;
    cumulativeRateModelUpdates = [];
    cumulativeRouteRateModelUpdates = [];
    cumulativeMaxRefundCountUpdates = [];
    cumulativeMaxL1TokenCountUpdates = [];
    chainIdIndicesUpdates = [];
    liteChainIndicesUpdates = [];
    cumulativeSpokeTargetBalanceUpdates = [];
    cumulativeConfigStoreVersionUpdates = [];
    cumulativeDisabledChainUpdates = [];
    hasLatestConfigStoreVersion = false;
    chainId;
    constructor(logger, configStore, eventSearchConfig = { from: 0, maxLookBack: 0 }, configStoreVersion) {
        super(eventSearchConfig);
        this.logger = logger;
        this.configStore = configStore;
        this.configStoreVersion = configStoreVersion;
        this.firstHeightToSearch = eventSearchConfig.from;
        this.latestHeightSearched = 0;
    }
    getRateModelForBlockNumber(l1Token, originChainId, destinationChainId, blockNumber = undefined) {
        const route = `${originChainId}-${destinationChainId}`;
        const routeRateModel = this.getRouteRateModelForBlockNumber(l1Token, route, blockNumber);
        if (routeRateModel) {
            return routeRateModel;
        }
        const defaultRateModelUpdate = (0, utils_1.sortEventsDescending)(this.cumulativeRateModelUpdates).find((config) => config.blockNumber <= (blockNumber ?? 0) &&
            config.l1Token === l1Token.toEvmAddress() &&
            config.rateModel !== undefined);
        if (!defaultRateModelUpdate) {
            throw new Error(`Could not find TokenConfig update for ${l1Token} at block ${blockNumber}`);
        }
        return (0, rateModel_1.parseAndReturnRateModelFromString)(defaultRateModelUpdate.rateModel);
    }
    getRouteRateModelForBlockNumber(l1Token, route, blockNumber = undefined) {
        const config = (0, utils_1.sortEventsDescending)(this.cumulativeRouteRateModelUpdates).find((config) => config.blockNumber <= (blockNumber ?? 0) && config.l1Token === l1Token.toEvmAddress());
        if (config?.routeRateModel[route] === undefined) {
            return undefined;
        }
        return (0, rateModel_1.parseAndReturnRateModelFromString)(config.routeRateModel[route]);
    }
    implicitChainIdIndices(chainId) {
        (0, assert_1.default)((0, utils_1.isDefined)(chainId), "ConfigStoreClient used before update");
        return constants_1.PROTOCOL_DEFAULT_CHAIN_ID_INDICES[0] === chainId ? constants_1.PROTOCOL_DEFAULT_CHAIN_ID_INDICES : [chainId];
    }
    getChainIdIndicesForBlock(blockNumber = Number.MAX_SAFE_INTEGER) {
        const chainIdUpdates = (0, utils_1.sortEventsDescending)(this.chainIdIndicesUpdates);
        const chainIdIndices = chainIdUpdates.find((update) => update.blockNumber <= blockNumber)?.value;
        return chainIdIndices ?? this.implicitChainIdIndices(this.chainId);
    }
    getLiteChainIdIndicesForBlock(blockNumber = Number.MAX_SAFE_INTEGER) {
        const liteChainIdList = (0, utils_1.sortEventsDescending)(this.liteChainIndicesUpdates);
        return liteChainIdList.find((update) => update.blockNumber <= blockNumber)?.value ?? [];
    }
    getLiteChainIdIndicesForTimestamp(timestamp = Number.MAX_SAFE_INTEGER) {
        const liteChainIdList = (0, utils_1.sortEventsDescending)(this.liteChainIndicesUpdates);
        return liteChainIdList.find((update) => update.timestamp <= timestamp)?.value ?? [];
    }
    isChainLiteChainAtTimestamp(chainId, timestamp = Number.MAX_SAFE_INTEGER) {
        return this.getLiteChainIdIndicesForTimestamp(timestamp).includes(chainId);
    }
    getSpokeTargetBalancesForBlock(l1Token, chainId, blockNumber = Number.MAX_SAFE_INTEGER) {
        const config = (0, utils_1.sortEventsDescending)(this.cumulativeSpokeTargetBalanceUpdates).find((config) => config.l1Token === l1Token && config.blockNumber <= blockNumber);
        const targetBalance = config?.spokeTargetBalances?.[chainId];
        return targetBalance || { target: (0, utils_1.toBN)(0), threshold: (0, utils_1.toBN)(0) };
    }
    getMaxRefundCountForRelayerRefundLeafForBlock(blockNumber = Number.MAX_SAFE_INTEGER) {
        const config = (0, utils_1.sortEventsDescending)(this.cumulativeMaxRefundCountUpdates).find((config) => config.blockNumber <= blockNumber);
        if (!config) {
            throw new Error(`Could not find MaxRefundCount before block ${blockNumber}`);
        }
        return Number(config.value);
    }
    getMaxL1TokenCountForPoolRebalanceLeafForBlock(blockNumber = Number.MAX_SAFE_INTEGER) {
        const config = (0, utils_1.sortEventsDescending)(this.cumulativeMaxL1TokenCountUpdates).find((config) => config.blockNumber <= blockNumber);
        if (!config) {
            throw new Error(`Could not find MaxL1TokenCount before block ${blockNumber}`);
        }
        return Number(config.value);
    }
    getEnabledChainsInBlockRange(fromBlock, toBlock = Number.MAX_SAFE_INTEGER) {
        if (fromBlock > toBlock) {
            throw new Error(`Invalid block range: fromBlock ${fromBlock} > toBlock ${toBlock}`);
        }
        const disabledChainsAtFromBlock = this.getDisabledChainsForBlock(fromBlock);
        const allPossibleChains = this.getChainIdIndicesForBlock(toBlock);
        const enabledChainsInBlockRange = allPossibleChains.filter((chainId) => !disabledChainsAtFromBlock.includes(chainId));
        this.cumulativeDisabledChainUpdates
            .filter((e) => e.blockNumber <= toBlock && e.blockNumber >= fromBlock)
            .forEach((e) => {
            const newDisabledSet = e.chainIds;
            disabledChainsAtFromBlock.forEach((disabledChain) => {
                if (!newDisabledSet.includes(disabledChain)) {
                    enabledChainsInBlockRange.push(disabledChain);
                }
            });
        });
        return allPossibleChains.filter((chainId) => enabledChainsInBlockRange.includes(chainId));
    }
    getEnabledChains(block = Number.MAX_SAFE_INTEGER) {
        const currentlyDisabledChains = this.getDisabledChainsForBlock(block);
        return this.getChainIdIndicesForBlock(block).filter((chainId) => !currentlyDisabledChains.includes(chainId));
    }
    getDisabledChainsForBlock(blockNumber = Number.MAX_SAFE_INTEGER) {
        return ((0, utils_1.sortEventsDescending)(this.cumulativeDisabledChainUpdates).find((config) => config.blockNumber <= blockNumber)
            ?.chainIds ?? []);
    }
    getConfigStoreVersionForTimestamp(timestamp = Number.MAX_SAFE_INTEGER) {
        const config = this.cumulativeConfigStoreVersionUpdates.find((config) => config.timestamp <= timestamp);
        return (0, utils_1.isDefined)(config) ? Number(config.value) : exports.DEFAULT_CONFIG_STORE_VERSION;
    }
    getConfigStoreVersionForBlock(blockNumber = Number.MAX_SAFE_INTEGER) {
        const config = this.cumulativeConfigStoreVersionUpdates.find((config) => config.blockNumber <= blockNumber);
        return (0, utils_1.isDefined)(config) ? Number(config.value) : exports.DEFAULT_CONFIG_STORE_VERSION;
    }
    hasValidConfigStoreVersionForTimestamp(timestamp = Number.MAX_SAFE_INTEGER) {
        const version = this.getConfigStoreVersionForTimestamp(timestamp);
        return this.configStoreVersion >= version;
    }
    async resolveChainId() {
        return this.chainId ?? (await this.configStore.provider.getNetwork()).chainId;
    }
    async _update() {
        const chainId = await this.resolveChainId();
        const searchConfig = await this.updateSearchConfig(this.configStore.provider);
        if ((0, BaseAbstractClient_1.isUpdateFailureReason)(searchConfig)) {
            const reason = searchConfig;
            return { success: false, reason };
        }
        this.logger.debug({ at: "AcrossConfigStore", message: "Updating ConfigStore client", searchConfig });
        const [updatedTokenConfigEvents, updatedGlobalConfigEvents] = await Promise.all([
            (0, utils_1.paginatedEventQuery)(this.configStore, this.configStore.filters.UpdatedTokenConfig(), searchConfig),
            (0, utils_1.paginatedEventQuery)(this.configStore, this.configStore.filters.UpdatedGlobalConfig(), searchConfig),
        ]);
        const updatedTokenConfigSortableEvents = updatedTokenConfigEvents.map(utils_1.spreadEventWithBlockNumber);
        const updatedGlobalConfigSortableEvents = updatedGlobalConfigEvents.map(utils_1.spreadEventWithBlockNumber);
        [updatedTokenConfigSortableEvents, updatedGlobalConfigSortableEvents].forEach((events) => (0, utils_1.sortEventsAscendingInPlace)(events));
        const globalConfigUpdateTimes = (await Promise.all(updatedGlobalConfigEvents.map((event) => this.configStore.provider.getBlock(event.blockNumber)))).map((block) => block.timestamp);
        return {
            success: true,
            chainId,
            searchEndBlock: searchConfig.to,
            events: {
                updatedTokenConfigEvents: updatedTokenConfigSortableEvents,
                updatedGlobalConfigEvents: updatedGlobalConfigSortableEvents,
                globalConfigUpdateTimes,
            },
        };
    }
    async update() {
        const result = await this._update();
        if (!result.success) {
            if (result.reason !== BaseAbstractClient_1.UpdateFailureReason.AlreadyUpdated) {
                throw new Error(`Unable to update ConfigStoreClient: ${result.reason}`);
            }
            return;
        }
        const { chainId } = result;
        const { updatedTokenConfigEvents, updatedGlobalConfigEvents, globalConfigUpdateTimes } = result.events;
        (0, assert_1.default)(updatedGlobalConfigEvents.length === globalConfigUpdateTimes.length, `GlobalConfigUpdate events mismatch (${updatedGlobalConfigEvents.length} != ${globalConfigUpdateTimes.length})`);
        for (const event of updatedTokenConfigEvents) {
            if (KNOWN_INVALID_TOKEN_CONFIG_UPDATE_HASHES.includes(event.txnRef.toLowerCase())) {
                continue;
            }
            const args = event;
            try {
                const { rateModel, routeRateModel, spokeTargetBalances } = this.validateTokenConfigUpdate(args);
                const { value, key: l1Token, ...eventData } = args;
                if (rateModel !== undefined) {
                    this.cumulativeRateModelUpdates.push({ ...eventData, rateModel, l1Token });
                    this.cumulativeSpokeTargetBalanceUpdates.push({
                        ...eventData,
                        spokeTargetBalances,
                        l1Token,
                    });
                    this.cumulativeRouteRateModelUpdates.push({ ...eventData, routeRateModel, l1Token });
                }
            }
            catch (err) {
                const { average: blockTime } = await (0, evm_1.averageBlockTime)(this.configStore.provider);
                const maxWarnAge = (24 * 60 * 60) / blockTime;
                if (result.searchEndBlock - event.blockNumber < maxWarnAge) {
                    const errMsg = (0, typeguards_1.isError)(err) ? err.message : "unknown error";
                    this.logger.warn({
                        at: "ConfigStore::update",
                        message: `Caught error during ConfigStore update: ${errMsg}`,
                        update: args,
                    });
                }
                else {
                    this.logger.debug({
                        at: "ConfigStoreClient::update",
                        message: `Skipping invalid historical update at block ${event.blockNumber}`,
                        txnRef: event.txnRef,
                    });
                }
                continue;
            }
        }
        for (let i = 0; i < updatedGlobalConfigEvents.length; i++) {
            const args = updatedGlobalConfigEvents[i];
            if (args.key === (0, utils_1.utf8ToHex)(GLOBAL_CONFIG_STORE_KEYS.MAX_RELAYER_REPAYMENT_LEAF_SIZE)) {
                if (!isNaN(args.value)) {
                    this.cumulativeMaxRefundCountUpdates.push(args);
                }
            }
            else if (args.key === (0, utils_1.utf8ToHex)(GLOBAL_CONFIG_STORE_KEYS.LITE_CHAIN_ID_INDICES)) {
                if (!/^\s*["']?\[(\d+(,\d+)*)?\]["']?\s*$/.test(args.value)) {
                    this.logger.warn({ at: "ConfigStore", message: `The lite chain indices array ${args.value} is invalid.` });
                    continue;
                }
                const chainIndices = JSON.parse(args.value.replace(/['"\s]/g, ""));
                if (!(0, utils_1.isArrayOf)(chainIndices, utils_1.isPositiveInteger)) {
                    this.logger.warn({ at: "ConfigStore", message: `The array ${chainIndices} is invalid.` });
                    continue;
                }
                if (new Set(chainIndices).size !== chainIndices.length) {
                    this.logger.warn({
                        at: "ConfigStore",
                        message: `The array ${chainIndices} contains duplicates making it invalid.`,
                    });
                    continue;
                }
                this.liteChainIndicesUpdates.push({ ...args, value: chainIndices, timestamp: globalConfigUpdateTimes[i] });
            }
            else if (args.key === (0, utils_1.utf8ToHex)(GLOBAL_CONFIG_STORE_KEYS.CHAIN_ID_INDICES)) {
                try {
                    const chainIndices = JSON.parse(args.value.replace(/['"\s]/g, ""));
                    if (!(0, utils_1.isArrayOf)(chainIndices, utils_1.isPositiveInteger)) {
                        this.logger.warn({ at: "ConfigStore", message: `The array ${chainIndices} is invalid.` });
                        continue;
                    }
                    if (new Set(chainIndices).size !== chainIndices.length) {
                        this.logger.warn({
                            at: "ConfigStore",
                            message: `The array ${chainIndices} contains duplicates making it invalid.`,
                        });
                        continue;
                    }
                    const previousUpdate = this.chainIdIndicesUpdates.at(-1)?.value ?? this.implicitChainIdIndices(chainId);
                    if (!previousUpdate.every((chainId, idx) => chainIndices[idx] === chainId)) {
                        this.logger.warn({
                            at: "ConfigStoreClient#update",
                            message: `The array ${chainIndices} is invalid. It must be a superset of the previous array ${previousUpdate}`,
                        });
                        continue;
                    }
                    this.chainIdIndicesUpdates.push({ ...args, value: chainIndices });
                }
                catch {
                    this.logger.warn({ at: "ConfigStore::update", message: `Failed to parse chain ID indices: ${args.value}` });
                }
            }
            else if (args.key === (0, utils_1.utf8ToHex)(GLOBAL_CONFIG_STORE_KEYS.MAX_POOL_REBALANCE_LEAF_SIZE)) {
                if (!isNaN(args.value)) {
                    this.cumulativeMaxL1TokenCountUpdates.push(args);
                }
            }
            else if (args.key === (0, utils_1.utf8ToHex)(GLOBAL_CONFIG_STORE_KEYS.VERSION)) {
                if (isNaN(args.value)) {
                    continue;
                }
                const value = Number(args.value);
                if (!Number.isInteger(value)) {
                    continue;
                }
                const lastValue = Number(this.cumulativeConfigStoreVersionUpdates[0]?.value ?? exports.DEFAULT_CONFIG_STORE_VERSION);
                if (value <= lastValue) {
                    continue;
                }
                this.cumulativeConfigStoreVersionUpdates = [
                    { ...args, timestamp: globalConfigUpdateTimes[i] },
                    ...this.cumulativeConfigStoreVersionUpdates,
                ];
            }
            else if (args.key === (0, utils_1.utf8ToHex)(GLOBAL_CONFIG_STORE_KEYS.DISABLED_CHAINS)) {
                try {
                    const chainIds = this.filterDisabledChains(JSON.parse(args.value));
                    this.cumulativeDisabledChainUpdates.push({ ...args, chainIds });
                }
                catch {
                }
            }
            else {
                continue;
            }
        }
        this.hasLatestConfigStoreVersion = this.hasValidConfigStoreVersionForTimestamp();
        this.latestHeightSearched = result.searchEndBlock;
        this.firstHeightToSearch = result.searchEndBlock + 1;
        this.eventSearchConfig.to = undefined;
        this.chainId = this.chainId ?? chainId;
        this.isUpdated = true;
        this.logger.debug({ at: "ConfigStore", message: "ConfigStore client updated!" });
    }
    validateTokenConfigUpdate(args) {
        const { value, key, txnRef } = args;
        const parsedValue = (0, JSONUtils_1.parseJSONWithNumericString)(value);
        const l1Token = key;
        let rateModelForToken = undefined;
        let spokeTargetBalances = {};
        let routeRateModel = {};
        if (parsedValue?.rateModel !== undefined) {
            const rateModel = parsedValue.rateModel;
            (0, assert_1.default)(this.isValidRateModel(rateModel), `Invalid rateModel UBar for ${l1Token} at transaction ${txnRef}, ${JSON.stringify(rateModel)}`);
            rateModelForToken = JSON.stringify(rateModel);
            if (parsedValue?.spokeTargetBalances) {
                spokeTargetBalances = Object.fromEntries(Object.entries(parsedValue.spokeTargetBalances).map(([chainId, targetBalance]) => {
                    const target = (0, utils_1.max)((0, utils_1.toBN)(targetBalance.target), (0, utils_1.toBN)(0));
                    const threshold = (0, utils_1.max)((0, utils_1.toBN)(targetBalance.threshold), (0, utils_1.toBN)(0));
                    return [chainId, { target, threshold }];
                }));
            }
            if (parsedValue?.routeRateModel) {
                routeRateModel = Object.fromEntries(Object.entries(parsedValue.routeRateModel).map(([path, routeRateModel]) => {
                    (0, assert_1.default)(this.isValidRateModel(routeRateModel) &&
                        `Invalid routeRateModel UBar for ${path} for ${l1Token} at transaction ${txnRef}, ${JSON.stringify(routeRateModel)}`);
                    return [path, JSON.stringify(routeRateModel)];
                }));
            }
        }
        return {
            spokeTargetBalances,
            rateModel: rateModelForToken,
            routeRateModel,
        };
    }
    isValidRateModel(rateModel) {
        return (0, utils_1.toBN)(rateModel.UBar).gt(0) && (0, utils_1.toBN)(rateModel.UBar).lt((0, utils_1.toWei)("1"));
    }
    filterDisabledChains(disabledChains) {
        return disabledChains.filter((chainId) => !isNaN(chainId) && Number.isInteger(chainId) && chainId !== 1);
    }
}
exports.AcrossConfigStoreClient = AcrossConfigStoreClient;
//# sourceMappingURL=AcrossConfigStoreClient.js.map