"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;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.MoralisP2PWorker = void 0;
const crypto_rpc_1 = require("@bitpay-labs/crypto-rpc");
const logger_1 = __importStar(require("../../../logger"));
const events_1 = require("../../../models/events");
const state_1 = require("../../../models/state");
const walletAddress_1 = require("../../../models/walletAddress");
const webhook_1 = require("../../../models/webhook");
const block_1 = require("../../../providers/chain-state/evm/models/block");
const transaction_1 = require("../../../providers/chain-state/evm/models/transaction");
const p2p_1 = require("../../../services/p2p");
const utils_1 = require("../../../utils");
const csp_1 = require("../api/csp");
class MoralisP2PWorker extends p2p_1.BaseP2PWorker {
    constructor({ chain, network, chainConfig }) {
        super({ chain, network, chainConfig });
        this.chain = chain;
        this.network = network;
        this.chainConfig = chainConfig;
        this.csp = new csp_1.MoralisStateProvider(this.chain);
        this.addressSub = undefined;
        this.chainId = undefined;
        this.webhookTail = undefined;
        this.bestBlock = 0;
        this.chainNetworkStr = `${this.chain}:${this.network}`;
    }
    async start() {
        this.refreshSyncingNode();
    }
    async stop() {
        this.stopping = true;
        clearInterval(this.syncInterval);
        await this.webhookTail?.close();
        await this.unregisterSyncingNode();
        this.web3?.currentProvider?.disconnect?.();
    }
    async getWeb3() {
        const connectionErrors = [];
        try {
            try {
                await this.web3.eth.getBlockNumber();
            }
            catch {
                const providerConfigs = this.chainConfig.providers || (this.chainConfig.provider ? [this.chainConfig.provider] : []);
                for (const config of providerConfigs) {
                    try {
                        const rpc = new crypto_rpc_1.CryptoRpc({ chain: this.chain, isEVM: true, ...config }).get(this.chain);
                        await rpc.web3.eth.getBlockNumber();
                        this.web3 = rpc.web3;
                        return this.web3;
                    }
                    catch (e) {
                        connectionErrors.push(e);
                    }
                }
                logger_1.default.error('Unable to connect to web3 %o:%o instance: %o', this.chain, this.network, connectionErrors);
                // Notice we don't unset this.web3. At worst, the old connection starts working again.
            }
        }
        catch (e) {
            logger_1.default.error('Error getting web3 %o:%o instance: %o', this.chain, this.network, e);
        }
        return this.web3;
    }
    async getChainId() {
        if (this.chainId == null) {
            const web3 = await this.getWeb3();
            this.chainId = await web3.eth.getChainId();
        }
        return this.chainId;
    }
    async sync() {
        const { value: state } = await state_1.StateStorage.getSingletonState();
        let currentHeight = state?.verifiedBlockHeight?.[this.chain]?.[this.network] || 0;
        let syncing = false;
        this.syncInterval = setInterval(() => {
            if (this.stopping || !this.isSyncingNode) {
                return;
            }
            if (syncing) {
                return;
            }
            (async () => {
                syncing = true;
                const startTime = Date.now();
                let msgInterval;
                try {
                    const web3 = await this.getWeb3();
                    this.bestBlock = Number(await web3.eth.getBlockNumber());
                    currentHeight = Math.max(this.bestBlock - (this.chainConfig.maxBlocksToSync || 50), currentHeight || 0);
                    const startHeight = currentHeight;
                    if (this.bestBlock - currentHeight <= 0) {
                        return;
                    }
                    logger_1.default.info(`Syncing ${this.bestBlock - currentHeight} blocks for ${this.chain} ${this.network}`);
                    msgInterval = setInterval(() => {
                        const blocksProcessed = currentHeight - startHeight;
                        const elapsedMinutes = (Date.now() - startTime) / (60 * 1000);
                        logger_1.default.info(`${(0, logger_1.timestamp)()} | Syncing... | Chain: ${this.chain} | Network: ${this.network} |${(blocksProcessed / elapsedMinutes)
                            .toFixed(2)
                            .padStart(8)} blocks/min | Height: ${currentHeight.toString().padStart(7)}`);
                    }, 1000 * 2); // 2 seconds
                    let blockExists = false;
                    do {
                        const block = await web3.eth.getBlock(currentHeight, true);
                        blockExists = !!block;
                        if (block && !this.stopping) {
                            const blockEvent = block_1.EVMBlockStorage.convertRawBlock(this.chain, this.network, block);
                            await events_1.EventStorage.signalBlock(blockEvent); // .catch((e) => logger.error(`Error signaling ${this.chainNetworkStr} block event: %o`, e.stack || e.message || e));
                            for (const tx of block.transactions) {
                                const txEvent = transaction_1.EVMTransactionStorage.convertRawTx(this.chain, this.network, tx, blockEvent);
                                await events_1.EventStorage.signalTx(txEvent); // .catch((e) => logger.error(`Error signaling ${this.chainNetworkStr} tx event: %o`, e.stack || e.message || e));
                            }
                            await state_1.StateStorage.setVerifiedBlockHeight({ chain: this.chain, network: this.network, height: currentHeight });
                            currentHeight = Number(block.number) + 1;
                        }
                    } while (blockExists);
                    clearInterval(msgInterval); // clear before log below
                    logger_1.default.info(`${this.chainNetworkStr} up to date.`);
                }
                catch (err) {
                    logger_1.default.error(`Error syncing ${this.chainNetworkStr}: %o`, err.stack || err.message || err);
                }
                finally {
                    syncing = false;
                    clearInterval(msgInterval); // clear here too in case of a catch
                }
            })().catch(err => logger_1.default.error('Unhandled error in sync interval:', err));
        }, 1000 * (this.chainConfig.syncIntervalSecs || 10)); // default 10 seconds
        // Listen for webhooks and process any unprocessed ones in the db
        await this.syncAddressActivity();
        // Subscribe to external wallet activity provider
        while (!(await this.subscribeToAddressActivity()) && !this.stopping) {
            // subscription might fail if the API service is offline (webhook endpoint)
            logger_1.default.warn(`Retrying ${this.chainNetworkStr} address subscription in 10 seconds...`);
            await (0, utils_1.wait)(10 * 1000);
        }
        ;
    }
    async subscribeToAddressActivity() {
        try {
            const chainId = await this.getChainId();
            const subs = await this.csp.getAddressSubscriptions();
            this.addressSub = subs?.find(sub => sub.chainIds.includes('0x' + chainId.toString(16)));
            if (this.addressSub?.status === 'error') {
                await this.csp.deleteAddressSubscription({ sub: this.addressSub });
                this.addressSub = undefined;
            }
            if (!this.addressSub) {
                // Create new subscription if none exists
                this.addressSub = await this.csp.createAddressSubscription({
                    chain: this.chain,
                    network: this.network,
                    chainId
                });
            }
            logger_1.default[this.addressSub?.status === 'active' ? 'info' : 'warn'](`Address subscription for ${this.chainNetworkStr} is: ${this.addressSub.status}`);
            // todo check and update url
            // if (!sub.webhookUrl.startsWith(provider.baseWebhookUrl)) {
            //   // todo update url
            // }
            const ONE_DAY = 1000 * 60 * 60 * 24;
            const now = Date.now();
            { // Remove inactive addresses
                const state = await state_1.StateStorage.getSingletonState();
                const lastUpdate = state.value?.lastAddressSubscriptionUpdate?.[this.chain]?.[this.network];
                const inactiveAddresses = await walletAddress_1.WalletAddressStorage.collection.find({
                    chain: this.chain,
                    network: this.network,
                    lastQueryTime: { $gte: lastUpdate, $lt: now - ONE_DAY * 60 }
                }).toArray();
                this.addressSub = await this.csp.updateAddressSubscription({
                    sub: this.addressSub,
                    addressesToRemove: inactiveAddresses.map(a => a.address)
                });
            }
            { // Add new addresses
                const activeAddresses = await walletAddress_1.WalletAddressStorage.collection.find({
                    chain: this.chain,
                    network: this.network,
                    lastQueryTime: { $gte: now - ONE_DAY * 60 }
                }).toArray();
                this.addressSub = await this.csp.updateAddressSubscription({
                    sub: this.addressSub,
                    addressesToAdd: activeAddresses.map(a => a.address),
                });
            }
            // Set subscription to active
            this.addressSub = await this.csp.updateAddressSubscription({
                sub: this.addressSub,
                status: 'active'
            });
            logger_1.default[this.addressSub?.status === 'active' ? 'info' : 'warn'](`Address subscription for ${this.chainNetworkStr} is: ${this.addressSub.status}`);
            return true;
        }
        catch (err) {
            logger_1.default.error(`Error subscribing to ${this.chainNetworkStr} address activity: %o`, err.stack || err.message || err);
            return false;
        }
    }
    async syncAddressActivity() {
        // if this is a reconnect, remove old listeners
        this.webhookTail?.removeAllListeners();
        this.webhookTail = webhook_1.WebhookStorage.getTail({ chain: this.chain, network: this.network });
        logger_1.default.info(`Webhook tail initiated for ${this.chainNetworkStr}`);
        this.webhookTail.on('error', (err) => {
            logger_1.default.error(`Webhook ${this.chainNetworkStr} tail error: %o`, err.stack || err.message || err);
        });
        this.webhookTail.on('close', async () => {
            // clearInterval(heightInterval);
            if (!this.stopping) {
                logger_1.default.error(`Webhook tail for ${this.chainNetworkStr} unexpectedly closed. Trying to reconnect in 5 seconds...`);
                await (0, utils_1.wait)(5000); // wait 5 seconds before trying to reconnect
                this.syncAddressActivity();
            }
        });
        this.webhookTail.on('data', async (webhook) => {
            try {
                let coinEvents = [];
                if (webhook.source !== 'moralis') {
                    return;
                }
                coinEvents = this.csp.webhookToCoinEvents({ chain: this.chain, network: this.network, webhook });
                const result = await Promise.allSettled(coinEvents.map(evt => events_1.EventStorage.signalAddressCoin(evt)));
                const someFulfilled = result.some(r => r.status === 'fulfilled');
                if (someFulfilled) {
                    await webhook_1.WebhookStorage.setProcessed({ webhook });
                }
            }
            catch (err) {
                logger_1.default.error(`Error processing ${this.chainNetworkStr} webhook ${webhook._id?.toString()}: %o`, err.stack || err.message || err);
            }
        });
    }
}
exports.MoralisP2PWorker = MoralisP2PWorker;
//# sourceMappingURL=p2p.js.map