"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.EVMP2pWorker = void 0;
const events_1 = require("events");
const os = __importStar(require("os"));
const __1 = require("../../");
const logger_1 = require("../../../../logger");
const logger_2 = __importDefault(require("../../../../logger"));
const state_1 = require("../../../../models/state");
const p2p_1 = require("../../../../services/p2p");
const utils_1 = require("../../../../utils");
const csp_1 = require("../api/csp");
const block_1 = require("../models/block");
const transaction_1 = require("../models/transaction");
const rpcs_1 = require("./rpcs");
const sync_1 = require("./sync");
class EVMP2pWorker extends p2p_1.BaseP2PWorker {
    constructor({ chain, network, chainConfig, blockModel = block_1.EVMBlockStorage, txModel = transaction_1.EVMTransactionStorage }) {
        super({ chain, network, chainConfig, blockModel });
        this.chain = chain || 'ETH';
        this.network = network;
        this.chainConfig = chainConfig;
        this.syncing = false;
        this.initialSyncComplete = false;
        this.blockModel = blockModel;
        this.txModel = txModel;
        this.provider = new csp_1.BaseEVMStateProvider(this.chain);
        this.events = new events_1.EventEmitter();
        this.invCache = {};
        this.invCacheLimits = {
            TX: 100000
        };
        this.disconnecting = false;
        this.multiThreadSync = new sync_1.MultiThreadSync({ chain, network });
    }
    cacheInv(type, hash) {
        if (!this.invCache[type]) {
            this.invCache[type] = [];
        }
        if (this.invCache[type].length > this.invCacheLimits[type]) {
            this.invCache[type].shift();
        }
        this.invCache[type].push(hash);
    }
    isCachedInv(type, hash) {
        if (!this.invCache[type]) {
            this.invCache[type] = [];
        }
        return this.invCache[type].includes(hash);
    }
    async setupListeners() {
        const { host, port } = this.chainConfig.provider || this.chainConfig.providers[0];
        this.events.on('disconnected', async () => {
            logger_2.default.warn(`${(0, logger_1.timestamp)()} | Not connected to peer: ${host}:${port} | Chain: ${this.chain} | Network: ${this.network}`);
        });
        this.events.on('connected', async () => {
            try {
                if (this.txSubscription) {
                    await this.txSubscription.resubscribe();
                }
                else {
                    this.txSubscription = await this.web3.eth.subscribe('pendingTransactions');
                }
                this.txSubscription.on('data', async (txid) => {
                    try {
                        if (!this.isCachedInv('TX', txid)) {
                            this.cacheInv('TX', txid);
                            const tx = await this.web3.eth.getTransaction(txid);
                            if (tx) {
                                await this.processTransaction(tx);
                                this.events.emit('transaction', tx);
                            }
                        }
                    }
                    catch (err) {
                        if (err.message !== 'Transaction not found') {
                            logger_2.default.error('Error in pendingTransactions subscription:', err);
                        }
                    }
                });
                if (this.blockSubscription) {
                    await this.blockSubscription.resubscribe();
                }
                else {
                    this.blockSubscription = await this.web3.eth.subscribe('newBlockHeaders');
                }
                this.blockSubscription.on('data', (block) => {
                    try {
                        this.events.emit('block', block);
                        if (!this.syncing) {
                            this.sync();
                        }
                    }
                    catch (err) {
                        logger_2.default.error('Error in newBlockHeaders subscription:', err);
                    }
                });
            }
            catch (err) {
                logger_2.default.error('Error in connected handler:', err);
            }
        });
        this.multiThreadSync.once('INITIALSYNCDONE', () => {
            this.initialSyncComplete = true;
            this.events.emit('SYNCDONE');
        });
    }
    async disconnect() {
        this.disconnecting = true;
        try {
            if (this.txSubscription) {
                this.txSubscription.unsubscribe();
            }
            if (this.blockSubscription) {
                this.blockSubscription.unsubscribe();
            }
        }
        catch (e) {
            console.error(e);
        }
    }
    async getWeb3() {
        return this.provider.getWeb3(this.network);
    }
    async getClient() {
        try {
            const nodeVersion = await this.web3.eth.getNodeInfo();
            const client = nodeVersion.split('/')[0].toLowerCase();
            if (client !== 'erigon' && client !== 'geth') {
                // assume it's a geth fork, or at least more like geth.
                // this is helpful when using a dev solution like ganache.
                return 'geth';
            }
            return client;
        }
        catch (e) {
            console.error(e);
            return 'geth';
        }
    }
    async handleReconnects() {
        this.disconnecting = false;
        let firstConnect = true;
        let connected = false;
        let disconnected = false;
        const { host, port } = this.chainConfig.provider || this.chainConfig.providers[0];
        while (!this.disconnecting && !this.stopping) {
            try {
                if (!this.web3) {
                    const { web3 } = await this.getWeb3();
                    this.web3 = web3;
                }
                try {
                    if (!this.client || !this.rpc) {
                        this.client = await this.getClient();
                        this.rpc = new rpcs_1.Rpcs[this.client](this.web3);
                    }
                    connected = await this.web3.eth.net.isListening();
                }
                catch {
                    connected = false;
                }
                if (!connected) {
                    this.web3 = undefined;
                    this.client = undefined;
                    this.events.emit('disconnected');
                }
                else if (disconnected || firstConnect) {
                    this.events.emit('connected');
                }
                if (disconnected && connected && !firstConnect) {
                    logger_2.default.warn(`${(0, logger_1.timestamp)()} | Reconnected to peer: ${host}:${port} | Chain: ${this.chain} | Network: ${this.network}`);
                }
                if (connected && firstConnect) {
                    firstConnect = false;
                    logger_2.default.info(`${(0, logger_1.timestamp)()} | Connected to peer: ${host}:${port} | Chain: ${this.chain} | Network: ${this.network}`);
                }
                disconnected = !connected;
            }
            catch { /* ignore error */ }
            await (0, utils_1.wait)(2000);
        }
    }
    async connect() {
        this.handleReconnects();
        return new Promise(resolve => this.events.once('connected', resolve));
    }
    async getBlock(height) {
        return this.rpc.getBlock(height);
    }
    async processBlock(block, transactions) {
        await this.blockModel.addBlock({
            chain: this.chain,
            network: this.network,
            forkHeight: this.chainConfig.forkHeight,
            parentChain: this.chainConfig.parentChain,
            initialSyncComplete: this.initialSyncComplete,
            block,
            transactions
        });
        if (!this.syncing) {
            logger_2.default.info(`Added block ${block.hash}`, {
                chain: this.chain,
                network: this.network
            });
        }
    }
    async processTransaction(tx) {
        const now = new Date();
        const convertedTx = this.txModel.convertRawTx(this.chain, this.network, tx);
        this.txModel.batchImport({
            chain: this.chain,
            network: this.network,
            txs: [convertedTx],
            height: -1,
            mempoolTime: now,
            blockTime: now,
            blockTimeNormalized: now,
            initialSyncComplete: true
        });
    }
    useMultiThread() {
        if (this.chainConfig.threads == null) {
            // use multithread by default if there are >2 threads in the CPU
            return os.cpus().length > 2;
        }
        return this.chainConfig.threads > 0;
    }
    async sync() {
        if (this.syncing) {
            return false;
        }
        if (!this.initialSyncComplete && this.useMultiThread()) {
            return this.multiThreadSync.sync();
        }
        const { chain, chainConfig, network } = this;
        const { parentChain, forkHeight = 0 } = chainConfig;
        this.syncing = true;
        const state = await state_1.StateStorage.collection.findOne({});
        this.initialSyncComplete =
            state && state.initialSyncComplete && state.initialSyncComplete.includes(`${chain}:${network}`);
        let tip = await __1.ChainStateProvider.getLocalTip({ chain, network });
        if (parentChain && (!tip || tip.height < forkHeight)) {
            let parentTip = await __1.ChainStateProvider.getLocalTip({ chain: parentChain, network });
            while (!parentTip || parentTip.height < forkHeight) {
                logger_2.default.info(`Waiting until ${parentChain} syncs before ${chain} ${network}`);
                await new Promise(resolve => {
                    setTimeout(resolve, 5000);
                });
                parentTip = await __1.ChainStateProvider.getLocalTip({ chain: parentChain, network });
            }
        }
        const startHeight = tip ? tip.height : chainConfig.syncStartHeight || 0;
        const startTime = Date.now();
        try {
            let bestBlock = Number(await this.web3.eth.getBlockNumber());
            let lastLog = 0;
            let currentHeight = tip ? tip.height : chainConfig.syncStartHeight || 0;
            logger_2.default.info(`Syncing ${bestBlock - currentHeight} blocks for ${chain} ${network}`);
            while (currentHeight <= bestBlock) {
                const block = await this.getBlock(currentHeight);
                if (!block) {
                    await (0, utils_1.wait)(1000);
                    continue;
                }
                const { convertedBlock, convertedTxs } = await this.convertBlock(block);
                await this.processBlock(convertedBlock, convertedTxs);
                if (currentHeight === bestBlock) {
                    bestBlock = Number(await this.web3.eth.getBlockNumber());
                }
                tip = await __1.ChainStateProvider.getLocalTip({ chain, network });
                currentHeight = tip ? tip.height + 1 : 0;
                const oneSecond = 1000;
                const now = Date.now();
                if (now - lastLog > oneSecond) {
                    const blocksProcessed = currentHeight - startHeight;
                    const elapsedMinutes = (now - startTime) / (60 * oneSecond);
                    logger_2.default.info(`${(0, logger_1.timestamp)()} | Syncing... | Chain: ${chain} | Network: ${network} |${(blocksProcessed / elapsedMinutes)
                        .toFixed(2)
                        .padStart(8)} blocks/min | Height: ${currentHeight.toString().padStart(7)}`);
                    lastLog = Date.now();
                }
            }
        }
        catch (err) {
            logger_2.default.error(`Error syncing ${chain} ${network} -- %o`, err);
            await (0, utils_1.wait)(2000);
            this.syncing = false;
            return this.sync();
        }
        logger_2.default.info(`${chain}:${network} up to date.`);
        this.syncing = false;
        state_1.StateStorage.collection.findOneAndUpdate({}, { $addToSet: { initialSyncComplete: `${chain}:${network}` } }, { upsert: true });
        this.events.emit('SYNCDONE');
        return true;
    }
    async syncDone() {
        return new Promise(resolve => this.events.once('SYNCDONE', resolve));
    }
    getBlockReward(block) {
        // TODO: implement block reward
        block;
        return 0;
    }
    async convertBlock(block) {
        const blockTime = Number(block.timestamp) * 1000;
        const hash = block.hash;
        const height = block.number;
        const reward = this.getBlockReward(block);
        const convertedBlock = {
            chain: this.chain,
            network: this.network,
            height: Number(height),
            hash,
            coinbase: Buffer.from(block.miner),
            merkleRoot: Buffer.from(block.transactionsRoot),
            time: new Date(blockTime),
            timeNormalized: new Date(blockTime),
            nonce: Buffer.from(block.extraData),
            previousBlockHash: block.parentHash,
            difficulty: Number(block.difficulty).toString(),
            totalDifficulty: block.totalDifficulty != undefined ? Number(block.totalDifficulty).toString() : undefined,
            nextBlockHash: '',
            transactionCount: block.transactions.length,
            size: Number(block.size),
            reward,
            logsBloom: Buffer.from(block.logsBloom),
            sha3Uncles: Buffer.from(block.sha3Uncles),
            receiptsRoot: Buffer.from(block.receiptsRoot),
            processed: false,
            gasLimit: Number(block.gasLimit),
            gasUsed: Number(block.gasUsed),
            stateRoot: Buffer.from(block.stateRoot)
        };
        const convertedTxs = block.transactions.map(t => this.txModel.convertRawTx(this.chain, this.network, t, convertedBlock));
        const traceTxs = await this.rpc.getTransactionsFromBlock(convertedBlock.height);
        this.rpc.reconcileTraces(convertedBlock, convertedTxs, traceTxs);
        this.txModel.addEffectsToTxs(convertedTxs);
        return { convertedBlock, convertedTxs };
    }
    async stop() {
        this.stopping = true;
        this.multiThreadSync.stop();
        logger_2.default.debug(`Stopping worker for chain ${this.chain} ${this.network}`);
        await this.disconnect();
    }
    async start() {
        logger_2.default.debug(`Started worker for chain ${this.chain} ${this.network}`);
        this.setupListeners();
        await this.connect();
        this.sync();
    }
}
exports.EVMP2pWorker = EVMP2pWorker;
//# sourceMappingURL=p2p.js.map