var _a;
import { __awaiter, __extends, __generator } from "tslib";
import assert from "assert";
import { clamp, sortedIndexBy } from "lodash";
import { chainIsOPStack, getNetworkName } from "../../utils/NetworkUtils";
import { isDefined } from "../../utils/TypeGuards";
import { BlockFinder, } from "../../utils/BlockFinder";
import { getCurrentTime } from "../../utils/TimeUtils";
import { CHAIN_IDs } from "../../constants";
// Archive requests typically commence at 128 blocks past the head of the chain.
// Round down to 120 blocks to avoid slipping into archive territory.
var defaultBlockRange = 120;
// Default offset to the high block number. This is subtracted from the block number of the high block
// when it is queried from the network, rather than having been specified by the caller. This is useful
// since the supplied Provider instance may be backed by multiple RPC providers, which can lead to some
// providers running slower than others and taking time to synchronise on the latest block.
var defaultHighBlockOffset = 10;
// Retain computations for 15 minutes.
var cacheTTL = 60 * 15;
var now = getCurrentTime(); // Seed the cache with initial values.
var blockTimes = (_a = {},
    _a[CHAIN_IDs.INK] = { average: 1, timestamp: now, blockRange: 1 },
    _a[CHAIN_IDs.LINEA] = { average: 3, timestamp: now, blockRange: 1 },
    _a[CHAIN_IDs.MAINNET] = { average: 12.5, timestamp: now, blockRange: 1 },
    _a[CHAIN_IDs.OPTIMISM] = { average: 2, timestamp: now, blockRange: 1 },
    _a[CHAIN_IDs.UNICHAIN] = { average: 1, timestamp: now, blockRange: 1 },
    _a);
/**
 * @description Compute the average block time over a block range.
 * @returns Average number of seconds per block.
 */
export function averageBlockTime(provider, _a) {
    var _b;
    var _c = _a === void 0 ? {} : _a, highBlock = _c.highBlock, highBlockOffset = _c.highBlockOffset, blockRange = _c.blockRange;
    return __awaiter(this, void 0, void 0, function () {
        var chainId, cache, now, earliestBlockNumber, _d, firstBlock, lastBlock, average;
        return __generator(this, function (_e) {
            switch (_e.label) {
                case 0: return [4 /*yield*/, provider.getNetwork()];
                case 1:
                    chainId = (_e.sent()).chainId;
                    cache = (_b = blockTimes[chainId]) !== null && _b !== void 0 ? _b : (chainIsOPStack(chainId) ? blockTimes[CHAIN_IDs.OPTIMISM] : undefined);
                    now = getCurrentTime();
                    if (isDefined(cache) && now < cache.timestamp + cacheTTL) {
                        return [2 /*return*/, { average: cache.average, blockRange: cache.blockRange }];
                    }
                    if (!!isDefined(highBlock)) return [3 /*break*/, 3];
                    return [4 /*yield*/, provider.getBlockNumber()];
                case 2:
                    highBlock = _e.sent();
                    highBlock -= highBlockOffset !== null && highBlockOffset !== void 0 ? highBlockOffset : defaultHighBlockOffset;
                    _e.label = 3;
                case 3:
                    blockRange !== null && blockRange !== void 0 ? blockRange : (blockRange = defaultBlockRange);
                    earliestBlockNumber = highBlock - blockRange;
                    return [4 /*yield*/, Promise.all([
                            provider.getBlock(earliestBlockNumber),
                            provider.getBlock(highBlock),
                        ])];
                case 4:
                    _d = _e.sent(), firstBlock = _d[0], lastBlock = _d[1];
                    [firstBlock, lastBlock].forEach(function (block) {
                        if (!isDefined(block === null || block === void 0 ? void 0 : block.timestamp)) {
                            var network = getNetworkName(chainId);
                            var blockNumber = block === firstBlock ? earliestBlockNumber : highBlock;
                            throw new Error("BlockFinder: Failed to fetch block ".concat(blockNumber, " on ").concat(network));
                        }
                    });
                    average = (lastBlock.timestamp - firstBlock.timestamp) / blockRange;
                    blockTimes[chainId] = { timestamp: now, average: average, blockRange: blockRange };
                    return [2 /*return*/, { average: average, blockRange: blockRange }];
            }
        });
    });
}
function estimateBlocksElapsed(seconds, cushionPercentage, provider) {
    if (cushionPercentage === void 0) { cushionPercentage = 0.0; }
    return __awaiter(this, void 0, void 0, function () {
        var cushionMultiplier, average;
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0:
                    cushionMultiplier = cushionPercentage + 1.0;
                    return [4 /*yield*/, averageBlockTime(provider)];
                case 1:
                    average = (_a.sent()).average;
                    return [2 /*return*/, Math.floor((seconds * cushionMultiplier) / average)];
            }
        });
    });
}
var EVMBlockFinder = /** @class */ (function (_super) {
    __extends(EVMBlockFinder, _super);
    function EVMBlockFinder(provider, blocks) {
        if (blocks === void 0) { blocks = []; }
        var _this = _super.call(this) || this;
        _this.provider = provider;
        _this.blocks = blocks;
        return _this;
    }
    /**
     * @notice Gets the latest block whose timestamp is <= the provided timestamp.
     * @param number Timestamp timestamp to search.
     * @param hints Optional low and high block to bound the search space.
     */
    EVMBlockFinder.prototype.getBlockForTimestamp = function (timestamp, hints) {
        if (hints === void 0) { hints = {}; }
        return __awaiter(this, void 0, void 0, function () {
            var block, initialBlock, cushion, incrementDistance, _a, _b, multiplier, distance, blockNumber, block, index;
            var _this = this;
            return __generator(this, function (_c) {
                switch (_c.label) {
                    case 0:
                        timestamp = Number(timestamp);
                        assert(timestamp !== undefined && timestamp !== null, "timestamp must be provided");
                        if (!(this.blocks.length === 0 || this.blocks[this.blocks.length - 1].timestamp < timestamp)) return [3 /*break*/, 2];
                        return [4 /*yield*/, this.getLatestBlock()];
                    case 1:
                        block = _c.sent();
                        if (timestamp >= block.timestamp)
                            return [2 /*return*/, block];
                        _c.label = 2;
                    case 2: 
                    // Prime the BlockFinder cache with any supplied hints.
                    // If the hint is accurate, then this will bypass the subsequent estimation.
                    return [4 /*yield*/, Promise.all(Object.values(hints)
                            .filter(function (blockNumber) { return isDefined(blockNumber); })
                            .map(function (blockNumber) { return _this.getBlock(blockNumber); }))];
                    case 3:
                        // Prime the BlockFinder cache with any supplied hints.
                        // If the hint is accurate, then this will bypass the subsequent estimation.
                        _c.sent();
                        if (!(this.blocks[0].timestamp > timestamp)) return [3 /*break*/, 8];
                        initialBlock = this.blocks[0];
                        cushion = 1;
                        _b = (_a = Math).max;
                        // Ensure the increment block distance is _at least_ a single block to prevent an infinite loop.
                        return [4 /*yield*/, estimateBlocksElapsed(initialBlock.timestamp - timestamp, cushion, this.provider)];
                    case 4:
                        incrementDistance = _b.apply(_a, [
                            // Ensure the increment block distance is _at least_ a single block to prevent an infinite loop.
                            _c.sent(), 1]);
                        multiplier = 1;
                        _c.label = 5;
                    case 5:
                        distance = multiplier * incrementDistance;
                        blockNumber = Math.max(0, initialBlock.number - distance);
                        return [4 /*yield*/, this.getBlock(blockNumber)];
                    case 6:
                        block = _c.sent();
                        if (block.timestamp <= timestamp)
                            return [3 /*break*/, 8]; // Found an earlier block.
                        assert(blockNumber > 0, "timestamp is before block 0"); // Block 0 was not earlier than this timestamp. The row.
                        _c.label = 7;
                    case 7:
                        multiplier++;
                        return [3 /*break*/, 5];
                    case 8:
                        index = sortedIndexBy(this.blocks, { timestamp: timestamp }, "timestamp");
                        return [2 /*return*/, this.findBlock(this.blocks[index - 1], this.blocks[index], timestamp)];
                }
            });
        });
    };
    // Grabs the most recent block and caches it.
    EVMBlockFinder.prototype.getLatestBlock = function () {
        var _a;
        return __awaiter(this, void 0, void 0, function () {
            var block, index;
            return __generator(this, function (_b) {
                switch (_b.label) {
                    case 0: return [4 /*yield*/, this.provider.getBlock("latest")];
                    case 1:
                        block = _b.sent();
                        index = sortedIndexBy(this.blocks, block, "number");
                        if (((_a = this.blocks[index]) === null || _a === void 0 ? void 0 : _a.number) !== block.number)
                            this.blocks.splice(index, 0, block);
                        return [2 /*return*/, this.blocks[index]];
                }
            });
        });
    };
    // Grabs the block for a particular number and caches it.
    EVMBlockFinder.prototype.getBlock = function (number) {
        var _a, _b;
        return __awaiter(this, void 0, void 0, function () {
            var index, block;
            return __generator(this, function (_c) {
                switch (_c.label) {
                    case 0:
                        index = sortedIndexBy(this.blocks, { number: number }, "number");
                        if (((_a = this.blocks[index]) === null || _a === void 0 ? void 0 : _a.number) === number)
                            return [2 /*return*/, this.blocks[index]]; // Return early if block already exists.
                        return [4 /*yield*/, this.provider.getBlock(number)];
                    case 1:
                        block = _c.sent();
                        // Recompute the index after the async call since the state of this.blocks could have changed!
                        index = sortedIndexBy(this.blocks, { number: number }, "number");
                        // Rerun this check to avoid duplicate insertion.
                        if (((_b = this.blocks[index]) === null || _b === void 0 ? void 0 : _b.number) === number)
                            return [2 /*return*/, this.blocks[index]];
                        this.blocks.splice(index, 0, block); // A simple insert at index.
                        return [2 /*return*/, block];
                }
            });
        });
    };
    // Return the latest block, between startBlock and endBlock, whose timestamp is <= timestamp.
    // Effectively, this is an interpolation search algorithm to minimize block requests.
    // Note: startBlock and endBlock _must_ be different blocks.
    EVMBlockFinder.prototype.findBlock = function (_startBlock, _endBlock, timestamp) {
        return __awaiter(this, void 0, void 0, function () {
            var _a, startBlock, endBlock, totalTimeDifference, totalBlockDistance, blockPercentile, estimatedBlock, newBlock;
            return __generator(this, function (_b) {
                switch (_b.label) {
                    case 0:
                        _a = [_startBlock, _endBlock], startBlock = _a[0], endBlock = _a[1];
                        // In the case of equality, the endBlock is expected to be passed as the one whose timestamp === the requested
                        // timestamp.
                        if (endBlock.timestamp === timestamp)
                            return [2 /*return*/, endBlock];
                        // If there's no equality, but the blocks are adjacent, return the startBlock, since we want the returned block's
                        // timestamp to be <= the requested timestamp.
                        if (endBlock.number === startBlock.number + 1)
                            return [2 /*return*/, startBlock];
                        assert(endBlock.number !== startBlock.number, "startBlock cannot equal endBlock");
                        assert(timestamp < endBlock.timestamp && timestamp > startBlock.timestamp, "timestamp not in between start and end blocks");
                        totalTimeDifference = endBlock.timestamp - startBlock.timestamp;
                        totalBlockDistance = endBlock.number - startBlock.number;
                        blockPercentile = (timestamp - startBlock.timestamp) / totalTimeDifference;
                        estimatedBlock = startBlock.number + Math.round(blockPercentile * totalBlockDistance);
                        return [4 /*yield*/, this.getBlock(clamp(estimatedBlock, startBlock.number + 1, endBlock.number - 1))];
                    case 1:
                        newBlock = _b.sent();
                        // Depending on whether the new block is below or above the timestamp, narrow the search space accordingly.
                        if (newBlock.timestamp < timestamp) {
                            return [2 /*return*/, this.findBlock(newBlock, endBlock, timestamp)];
                        }
                        else {
                            return [2 /*return*/, this.findBlock(startBlock, newBlock, timestamp)];
                        }
                        return [2 /*return*/];
                }
            });
        });
    };
    return EVMBlockFinder;
}(BlockFinder));
export { EVMBlockFinder };
//# sourceMappingURL=BlockUtils.js.map