"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.EVMBlockStorage = exports.EVMBlockModel = void 0;
const bson_1 = require("bson");
const Loggify_1 = require("../../../../decorators/Loggify");
const logger_1 = __importDefault(require("../../../../logger"));
const baseBlock_1 = require("../../../../models/baseBlock");
const events_1 = require("../../../../models/events");
const transaction_1 = require("./transaction");
let EVMBlockModel = class EVMBlockModel extends baseBlock_1.BaseBlock {
    constructor(storage) {
        super(storage);
    }
    async onConnect() {
        super.onConnect();
    }
    async addBlock(params) {
        // TODO: add skipReorg to config for development purposes
        const { block, chain, network } = params;
        let reorg = false;
        if (params.initialSyncComplete) {
            const headers = await this.validateLocatorHashes({ chain, network });
            if (headers.length) {
                const last = headers[headers.length - 1];
                reorg = await this.handleReorg({ block: last, chain, network });
            }
            reorg = reorg || (await this.handleReorg({ block, chain, network }));
        }
        if (reorg) {
            return Promise.reject('reorg');
        }
        return this.processBlock(params);
    }
    async processBlock(params) {
        const { chain, network, transactions, parentChain, forkHeight, initialSyncComplete } = params;
        const blockOp = await this.getBlockOp(params);
        const convertedBlock = blockOp.updateOne.update.$set;
        const { height, timeNormalized, time } = convertedBlock;
        // Put in the transactions first
        await transaction_1.EVMTransactionStorage.batchImport({
            txs: transactions,
            blockHash: convertedBlock.hash,
            blockTime: new Date(time),
            blockTimeNormalized: new Date(timeNormalized),
            height,
            chain,
            network,
            parentChain,
            forkHeight,
            initialSyncComplete
        });
        const previousBlock = await this.collection.findOne({ hash: convertedBlock.previousBlockHash, chain, network });
        await this.collection.bulkWrite([blockOp]);
        if (previousBlock) {
            await this.collection.updateOne({ chain, network, hash: previousBlock.hash }, { $set: { nextBlockHash: convertedBlock.hash } });
            logger_1.default.debug('Updating previous block.nextBlockHash: %o', convertedBlock.hash);
        }
        if (initialSyncComplete) {
            events_1.EventStorage.signalBlock(convertedBlock);
        }
        await this.collection.updateOne({ hash: convertedBlock.hash, chain, network }, { $set: { processed: true } });
    }
    async getBlockOp(params) {
        const { block, chain, network } = params;
        const blockTime = block.time;
        const prevHash = block.previousBlockHash;
        const previousBlock = await this.collection.findOne({ hash: prevHash, chain, network });
        const timeNormalized = (() => {
            const prevTime = previousBlock ? new Date(previousBlock.timeNormalized) : null;
            if (prevTime && blockTime.getTime() <= prevTime.getTime()) {
                return new Date(prevTime.getTime() + 1);
            }
            else {
                return blockTime;
            }
        })();
        const height = block.height;
        logger_1.default.debug('Setting blockheight: %o', height);
        return {
            updateOne: {
                filter: {
                    hash: block.hash,
                    chain,
                    network
                },
                update: {
                    $set: { ...block, timeNormalized }
                },
                upsert: true
            }
        };
    }
    async handleReorg(params) {
        const { block, chain, network } = params;
        const prevHash = block.previousBlockHash;
        let localTip = await this.getLocalTip(params);
        if (block != null && localTip != null && (localTip.hash === prevHash || localTip.hash === block.hash)) {
            return false;
        }
        if (!localTip || localTip.height === 0) {
            return false;
        }
        if (block) {
            const prevBlock = await this.collection.findOne({ chain, network, hash: prevHash });
            if (prevBlock) {
                localTip = prevBlock;
            }
            else {
                logger_1.default.error("Previous block isn't in the DB need to roll back until we have a block in common");
            }
            logger_1.default.info(`Resetting tip to ${localTip.height - 1}`, { chain, network });
        }
        const reorgOps = [
            this.collection.deleteMany({ chain, network, height: { $gte: localTip.height } }),
            transaction_1.EVMTransactionStorage.collection.deleteMany({ chain, network, blockHeight: { $gte: localTip.height } })
        ];
        await Promise.all(reorgOps);
        logger_1.default.debug('Removed data from above blockHeight: %o', localTip.height);
        return localTip.hash !== prevHash;
    }
    _apiTransform(block, options) {
        const transform = {
            _id: block._id,
            chain: block.chain,
            network: block.network,
            hash: block.hash,
            height: block.height,
            size: block.size,
            gasLimit: block.gasLimit,
            gasUsed: block.gasUsed,
            merkleRoot: block.merkleRoot,
            time: block.time,
            timeNormalized: block.timeNormalized,
            nonce: block.nonce,
            previousBlockHash: block.previousBlockHash,
            nextBlockHash: block.nextBlockHash,
            reward: block.reward,
            transactionCount: block.transactionCount,
            difficulty: block.difficulty,
            totalDifficulty: block.totalDifficulty
        };
        if (options && options.object) {
            return transform;
        }
        return JSON.stringify(transform);
    }
    async getBlockSyncGaps(params) {
        const { chain, network, startHeight = 0, endHeight } = params;
        return new Promise(async (resolve, reject) => {
            let timeout;
            try {
                const heightQuery = { $gte: startHeight };
                if (endHeight) {
                    heightQuery['$lte'] = endHeight;
                }
                const stream = this.collection
                    .find({
                    chain,
                    network,
                    processed: true,
                    height: heightQuery
                })
                    .sort({ chain: 1, network: 1, processed: 1, height: -1 }) // guarantee index use by using this sort
                    .addCursorFlag('noCursorTimeout', true);
                let block = (await stream.next());
                const maxBlock = block;
                if (!maxBlock) {
                    return resolve([]);
                }
                const maxHeight = maxBlock.height;
                let prevBlock;
                const outOfSync = [];
                timeout = setInterval(() => logger_1.default.info(`${chain}:${network} Block verification progress: ${(((maxHeight - block.height) / (maxHeight - startHeight)) *
                    100).toFixed(1)}%`), 1000 * 2);
                // we are descending in blockHeight as we iterate
                for (let syncHeight = maxHeight; syncHeight >= startHeight; syncHeight--) {
                    if (!block || block.height !== syncHeight) {
                        outOfSync.push(syncHeight);
                    }
                    else {
                        // prevBlock should be the next block up in height
                        if (prevBlock && !block.nextBlockHash && block.height === prevBlock.height - 1) {
                            const res = await this.collection.updateOne({ chain, network, hash: block.hash }, { $set: { nextBlockHash: prevBlock.hash } });
                            if (res.modifiedCount === 1) {
                                block.nextBlockHash = prevBlock.hash;
                            }
                        }
                        prevBlock = block;
                        block = (await stream.next());
                        while (block && prevBlock && block.height === prevBlock.height) { // uncaught reorg?
                            logger_1.default.error('Conflicting blocks found at height %o. %o <-> %o', block.height, block.hash, prevBlock.hash);
                            block = (await stream.next());
                        }
                    }
                }
                resolve(outOfSync.reverse()); // reverse order so that they are in ascending order
            }
            catch (err) {
                reject(err);
            }
            finally {
                clearTimeout(timeout);
            }
        });
    }
    convertRawBlock(chain, network, block) {
        return {
            chain,
            network,
            height: Number(block.number),
            hash: block.hash,
            coinbase: new bson_1.Binary(Buffer.from(block.miner)),
            merkleRoot: new bson_1.Binary(Buffer.from(block.transactionsRoot)), // TODO: rm `as any` if web3 is updated and fixes itself
            time: new Date(Number(block.timestamp) * 1000),
            timeNormalized: new Date(Number(block.timestamp) * 1000),
            nonce: new bson_1.Binary(Buffer.from(block.extraData)),
            previousBlockHash: block.parentHash,
            difficulty: block.difficulty.toString(),
            totalDifficulty: block.totalDifficulty?.toString(),
            nextBlockHash: '',
            transactionCount: block.transactions.length,
            size: Number(block.size),
            reward: 0,
            logsBloom: new bson_1.Binary(Buffer.from(block.logsBloom)),
            sha3Uncles: new bson_1.Binary(Buffer.from(block.sha3Uncles)),
            receiptsRoot: new bson_1.Binary(Buffer.from(block.receiptsRoot)),
            processed: false,
            gasLimit: Number(block.gasLimit),
            gasUsed: Number(block.gasUsed),
            baseFeePerGas: Number(block.baseFeePerGas),
            stateRoot: new bson_1.Binary(Buffer.from(block.stateRoot)),
        };
    }
};
exports.EVMBlockModel = EVMBlockModel;
exports.EVMBlockModel = EVMBlockModel = __decorate([
    Loggify_1.LoggifyClass
], EVMBlockModel);
exports.EVMBlockStorage = new EVMBlockModel();
//# sourceMappingURL=block.js.map