"use strict";
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.EVMBlockFinder = exports.averageBlockTime = void 0;
var tslib_1 = require("tslib");
var assert_1 = tslib_1.__importDefault(require("assert"));
var lodash_1 = require("lodash");
var NetworkUtils_1 = require("../../utils/NetworkUtils");
var TypeGuards_1 = require("../../utils/TypeGuards");
var BlockFinder_1 = require("../../utils/BlockFinder");
var TimeUtils_1 = require("../../utils/TimeUtils");
var constants_1 = require("../../constants");
var defaultBlockRange = 120;
var defaultHighBlockOffset = 10;
var cacheTTL = 60 * 15;
var now = (0, TimeUtils_1.getCurrentTime)();
var blockTimes = (_a = {},
    _a[constants_1.CHAIN_IDs.INK] = { average: 1, timestamp: now, blockRange: 1 },
    _a[constants_1.CHAIN_IDs.LINEA] = { average: 3, timestamp: now, blockRange: 1 },
    _a[constants_1.CHAIN_IDs.MAINNET] = { average: 12.5, timestamp: now, blockRange: 1 },
    _a[constants_1.CHAIN_IDs.OPTIMISM] = { average: 2, timestamp: now, blockRange: 1 },
    _a[constants_1.CHAIN_IDs.UNICHAIN] = { average: 1, timestamp: now, blockRange: 1 },
    _a);
function averageBlockTime(provider, _a) {
    var _b;
    var _c = _a === void 0 ? {} : _a, highBlock = _c.highBlock, highBlockOffset = _c.highBlockOffset, blockRange = _c.blockRange;
    return tslib_1.__awaiter(this, void 0, void 0, function () {
        var chainId, cache, now, earliestBlockNumber, _d, firstBlock, lastBlock, average;
        return tslib_1.__generator(this, function (_e) {
            switch (_e.label) {
                case 0: return [4, provider.getNetwork()];
                case 1:
                    chainId = (_e.sent()).chainId;
                    cache = (_b = blockTimes[chainId]) !== null && _b !== void 0 ? _b : ((0, NetworkUtils_1.chainIsOPStack)(chainId) ? blockTimes[constants_1.CHAIN_IDs.OPTIMISM] : undefined);
                    now = (0, TimeUtils_1.getCurrentTime)();
                    if ((0, TypeGuards_1.isDefined)(cache) && now < cache.timestamp + cacheTTL) {
                        return [2, { average: cache.average, blockRange: cache.blockRange }];
                    }
                    if (!!(0, TypeGuards_1.isDefined)(highBlock)) return [3, 3];
                    return [4, 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, 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 (!(0, TypeGuards_1.isDefined)(block === null || block === void 0 ? void 0 : block.timestamp)) {
                            var network = (0, NetworkUtils_1.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, { average: average, blockRange: blockRange }];
            }
        });
    });
}
exports.averageBlockTime = averageBlockTime;
function estimateBlocksElapsed(seconds, cushionPercentage, provider) {
    if (cushionPercentage === void 0) { cushionPercentage = 0.0; }
    return tslib_1.__awaiter(this, void 0, void 0, function () {
        var cushionMultiplier, average;
        return tslib_1.__generator(this, function (_a) {
            switch (_a.label) {
                case 0:
                    cushionMultiplier = cushionPercentage + 1.0;
                    return [4, averageBlockTime(provider)];
                case 1:
                    average = (_a.sent()).average;
                    return [2, Math.floor((seconds * cushionMultiplier) / average)];
            }
        });
    });
}
var EVMBlockFinder = (function (_super) {
    tslib_1.__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;
    }
    EVMBlockFinder.prototype.getBlockForTimestamp = function (timestamp, hints) {
        if (hints === void 0) { hints = {}; }
        return tslib_1.__awaiter(this, void 0, void 0, function () {
            var block, initialBlock, cushion, incrementDistance, _a, _b, multiplier, distance, blockNumber, block, index;
            var _this = this;
            return tslib_1.__generator(this, function (_c) {
                switch (_c.label) {
                    case 0:
                        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)) return [3, 2];
                        return [4, this.getLatestBlock()];
                    case 1:
                        block = _c.sent();
                        if (timestamp >= block.timestamp)
                            return [2, block];
                        _c.label = 2;
                    case 2: return [4, Promise.all(Object.values(hints)
                            .filter(function (blockNumber) { return (0, TypeGuards_1.isDefined)(blockNumber); })
                            .map(function (blockNumber) { return _this.getBlock(blockNumber); }))];
                    case 3:
                        _c.sent();
                        if (!(this.blocks[0].timestamp > timestamp)) return [3, 8];
                        initialBlock = this.blocks[0];
                        cushion = 1;
                        _b = (_a = Math).max;
                        return [4, estimateBlocksElapsed(initialBlock.timestamp - timestamp, cushion, this.provider)];
                    case 4:
                        incrementDistance = _b.apply(_a, [_c.sent(), 1]);
                        multiplier = 1;
                        _c.label = 5;
                    case 5:
                        distance = multiplier * incrementDistance;
                        blockNumber = Math.max(0, initialBlock.number - distance);
                        return [4, this.getBlock(blockNumber)];
                    case 6:
                        block = _c.sent();
                        if (block.timestamp <= timestamp)
                            return [3, 8];
                        (0, assert_1.default)(blockNumber > 0, "timestamp is before block 0");
                        _c.label = 7;
                    case 7:
                        multiplier++;
                        return [3, 5];
                    case 8:
                        index = (0, lodash_1.sortedIndexBy)(this.blocks, { timestamp: timestamp }, "timestamp");
                        return [2, this.findBlock(this.blocks[index - 1], this.blocks[index], timestamp)];
                }
            });
        });
    };
    EVMBlockFinder.prototype.getLatestBlock = function () {
        var _a;
        return tslib_1.__awaiter(this, void 0, void 0, function () {
            var block, index;
            return tslib_1.__generator(this, function (_b) {
                switch (_b.label) {
                    case 0: return [4, this.provider.getBlock("latest")];
                    case 1:
                        block = _b.sent();
                        index = (0, lodash_1.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, this.blocks[index]];
                }
            });
        });
    };
    EVMBlockFinder.prototype.getBlock = function (number) {
        var _a, _b;
        return tslib_1.__awaiter(this, void 0, void 0, function () {
            var index, block;
            return tslib_1.__generator(this, function (_c) {
                switch (_c.label) {
                    case 0:
                        index = (0, lodash_1.sortedIndexBy)(this.blocks, { number: number }, "number");
                        if (((_a = this.blocks[index]) === null || _a === void 0 ? void 0 : _a.number) === number)
                            return [2, this.blocks[index]];
                        return [4, this.provider.getBlock(number)];
                    case 1:
                        block = _c.sent();
                        index = (0, lodash_1.sortedIndexBy)(this.blocks, { number: number }, "number");
                        if (((_b = this.blocks[index]) === null || _b === void 0 ? void 0 : _b.number) === number)
                            return [2, this.blocks[index]];
                        this.blocks.splice(index, 0, block);
                        return [2, block];
                }
            });
        });
    };
    EVMBlockFinder.prototype.findBlock = function (_startBlock, _endBlock, timestamp) {
        return tslib_1.__awaiter(this, void 0, void 0, function () {
            var _a, startBlock, endBlock, totalTimeDifference, totalBlockDistance, blockPercentile, estimatedBlock, newBlock;
            return tslib_1.__generator(this, function (_b) {
                switch (_b.label) {
                    case 0:
                        _a = [_startBlock, _endBlock], startBlock = _a[0], endBlock = _a[1];
                        if (endBlock.timestamp === timestamp)
                            return [2, endBlock];
                        if (endBlock.number === startBlock.number + 1)
                            return [2, 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");
                        totalTimeDifference = endBlock.timestamp - startBlock.timestamp;
                        totalBlockDistance = endBlock.number - startBlock.number;
                        blockPercentile = (timestamp - startBlock.timestamp) / totalTimeDifference;
                        estimatedBlock = startBlock.number + Math.round(blockPercentile * totalBlockDistance);
                        return [4, this.getBlock((0, lodash_1.clamp)(estimatedBlock, startBlock.number + 1, endBlock.number - 1))];
                    case 1:
                        newBlock = _b.sent();
                        if (newBlock.timestamp < timestamp) {
                            return [2, this.findBlock(newBlock, endBlock, timestamp)];
                        }
                        else {
                            return [2, this.findBlock(startBlock, newBlock, timestamp)];
                        }
                        return [2];
                }
            });
        });
    };
    return EVMBlockFinder;
}(BlockFinder_1.BlockFinder));
exports.EVMBlockFinder = EVMBlockFinder;
//# sourceMappingURL=BlockUtils.js.map