"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.EVMBlockFinder = void 0;
exports.averageBlockTime = averageBlockTime;
const tslib_1 = require("tslib");
const assert_1 = tslib_1.__importDefault(require("assert"));
const lodash_1 = require("lodash");
const NetworkUtils_1 = require("../../utils/NetworkUtils");
const TypeGuards_1 = require("../../utils/TypeGuards");
const BlockFinder_1 = require("../../utils/BlockFinder");
const TimeUtils_1 = require("../../utils/TimeUtils");
const constants_1 = require("../../constants");
const defaultBlockRange = 120;
const defaultHighBlockOffset = 10;
const cacheTTL = 60 * 15;
const now = (0, TimeUtils_1.getCurrentTime)();
const blockTimes = {
    [constants_1.CHAIN_IDs.INK]: { average: 1, timestamp: now, blockRange: 1 },
    [constants_1.CHAIN_IDs.LINEA]: { average: 3, timestamp: now, blockRange: 1 },
    [constants_1.CHAIN_IDs.MAINNET]: { average: 12.5, timestamp: now, blockRange: 1 },
    [constants_1.CHAIN_IDs.MEGAETH]: { average: 1, timestamp: now, blockRange: 1 },
    [constants_1.CHAIN_IDs.OPTIMISM]: { average: 2, timestamp: now, blockRange: 1 },
    [constants_1.CHAIN_IDs.TRON]: { average: 3, timestamp: now, blockRange: 1 },
    [constants_1.CHAIN_IDs.UNICHAIN]: { average: 1, timestamp: now, blockRange: 1 },
};
async function averageBlockTime(provider, { highBlock, highBlockOffset, blockRange } = {}) {
    const { chainId } = await provider.getNetwork();
    const cache = blockTimes[chainId] ?? ((0, NetworkUtils_1.chainIsOPStack)(chainId) ? blockTimes[constants_1.CHAIN_IDs.OPTIMISM] : undefined);
    const now = (0, TimeUtils_1.getCurrentTime)();
    if ((0, TypeGuards_1.isDefined)(cache) && now < cache.timestamp + cacheTTL) {
        return { average: cache.average, blockRange: cache.blockRange };
    }
    if (!(0, TypeGuards_1.isDefined)(highBlock)) {
        highBlock = await provider.getBlockNumber();
        highBlock -= highBlockOffset ?? defaultHighBlockOffset;
    }
    blockRange ??= defaultBlockRange;
    const earliestBlockNumber = highBlock - blockRange;
    const [firstBlock, lastBlock] = await Promise.all([
        provider.getBlock(earliestBlockNumber),
        provider.getBlock(highBlock),
    ]);
    [firstBlock, lastBlock].forEach((block) => {
        if (!(0, TypeGuards_1.isDefined)(block?.timestamp)) {
            const network = (0, NetworkUtils_1.getNetworkName)(chainId);
            const blockNumber = block === firstBlock ? earliestBlockNumber : highBlock;
            throw new Error(`BlockFinder: Failed to fetch block ${blockNumber} on ${network}`);
        }
    });
    const average = (lastBlock.timestamp - firstBlock.timestamp) / blockRange;
    blockTimes[chainId] = { timestamp: now, average, blockRange };
    return { average, blockRange };
}
async function estimateBlocksElapsed(seconds, cushionPercentage = 0.0, provider) {
    const cushionMultiplier = cushionPercentage + 1.0;
    const { average } = await averageBlockTime(provider);
    return Math.floor((seconds * cushionMultiplier) / average);
}
class EVMBlockFinder extends BlockFinder_1.BlockFinder {
    provider;
    blocks;
    constructor(provider, blocks = []) {
        super();
        this.provider = provider;
        this.blocks = blocks;
    }
    async getBlockForTimestamp(timestamp, hints = {}) {
        timestamp = Number(timestamp);
        (0, assert_1.default)(timestamp !== undefined && timestamp !== null, "timestamp must be provided");
        if (this.blocks.length === 0 || this.blocks[this.blocks.length - 1].timestamp < timestamp) {
            const block = await this.getLatestBlock();
            if (timestamp >= block.timestamp)
                return block;
        }
        await Promise.all(Object.values(hints)
            .filter((blockNumber) => (0, TypeGuards_1.isDefined)(blockNumber))
            .map((blockNumber) => this.getBlock(blockNumber)));
        if (this.blocks[0].timestamp > timestamp) {
            const initialBlock = this.blocks[0];
            const cushion = 1;
            const incrementDistance = Math.max(await estimateBlocksElapsed(initialBlock.timestamp - timestamp, cushion, this.provider), 1);
            for (let multiplier = 1;; multiplier++) {
                const distance = multiplier * incrementDistance;
                const blockNumber = Math.max(0, initialBlock.number - distance);
                const block = await this.getBlock(blockNumber);
                if (block.timestamp <= timestamp)
                    break;
                (0, assert_1.default)(blockNumber > 0, "timestamp is before block 0");
            }
        }
        const index = (0, lodash_1.sortedIndexBy)(this.blocks, { timestamp }, "timestamp");
        return this.findBlock(this.blocks[index - 1], this.blocks[index], timestamp);
    }
    async getLatestBlock() {
        const block = await this.provider.getBlock("latest");
        const index = (0, lodash_1.sortedIndexBy)(this.blocks, block, "number");
        if (this.blocks[index]?.number !== block.number)
            this.blocks.splice(index, 0, block);
        return this.blocks[index];
    }
    async getBlock(number) {
        let index = (0, lodash_1.sortedIndexBy)(this.blocks, { number }, "number");
        if (this.blocks[index]?.number === number)
            return this.blocks[index];
        const block = await this.provider.getBlock(number);
        index = (0, lodash_1.sortedIndexBy)(this.blocks, { number }, "number");
        if (this.blocks[index]?.number === number)
            return this.blocks[index];
        this.blocks.splice(index, 0, block);
        return block;
    }
    async findBlock(_startBlock, _endBlock, timestamp) {
        const [startBlock, endBlock] = [_startBlock, _endBlock];
        if (endBlock.timestamp === timestamp)
            return endBlock;
        if (endBlock.number === startBlock.number + 1)
            return startBlock;
        (0, assert_1.default)(endBlock.number !== startBlock.number, "startBlock cannot equal endBlock");
        (0, assert_1.default)(timestamp < endBlock.timestamp && timestamp > startBlock.timestamp, "timestamp not in between start and end blocks");
        const totalTimeDifference = endBlock.timestamp - startBlock.timestamp;
        const totalBlockDistance = endBlock.number - startBlock.number;
        const blockPercentile = (timestamp - startBlock.timestamp) / totalTimeDifference;
        const estimatedBlock = startBlock.number + Math.round(blockPercentile * totalBlockDistance);
        const newBlock = await this.getBlock((0, lodash_1.clamp)(estimatedBlock, startBlock.number + 1, endBlock.number - 1));
        if (newBlock.timestamp < timestamp) {
            return this.findBlock(newBlock, endBlock, timestamp);
        }
        else {
            return this.findBlock(startBlock, newBlock, timestamp);
        }
    }
}
exports.EVMBlockFinder = EVMBlockFinder;
//# sourceMappingURL=BlockUtils.js.map