"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.XrpP2pWorker = void 0;
const events_1 = require("events");
const stream_1 = require("stream");
const Loggify_1 = require("../../../decorators/Loggify");
const logger_1 = __importDefault(require("../../../logger"));
const logger_2 = require("../../../logger");
const cache_1 = require("../../../models/cache");
const state_1 = require("../../../models/state");
const walletAddress_1 = require("../../../models/walletAddress");
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");
let XrpP2pWorker = class XrpP2pWorker extends p2p_1.BaseP2PWorker {
    constructor({ chain, network, chainConfig, blockModel = block_1.XrpBlockStorage }) {
        super({ chain, network, chainConfig, blockModel });
        this.blockModel = blockModel;
        this.chain = chain || 'XRP';
        this.network = network;
        this.chainConfig = chainConfig;
        this.syncing = false;
        this.initialSyncComplete = false;
        this.provider = new csp_1.RippleStateProvider(this.chain);
        this.events = new events_1.EventEmitter();
        this.invCache = {};
        this.invCacheLimits = {
            TX: 100000
        };
        this.disconnecting = false;
    }
    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.events.on('disconnected', async () => {
            logger_1.default.warn(`${(0, logger_2.timestamp)()} | Not connected to peer: ${host}:${port || ''} | Chain: ${this.chain} | Network: ${this.network}`);
        });
        this.events.on('connected', async () => {
            this.client?.rpc.on('ledgerClosed', async () => {
                this.sync();
            });
            this.client?.asyncRequest('subscribe', { streams: ['ledger'] });
        });
    }
    async teardownListeners() {
        this.client?.rpc.removeAllListeners('ledgerClosed');
        await this.client?.asyncRequest('unsubscribe', { streams: ['ledger'] });
    }
    async disconnect() {
        this.disconnecting = true;
        await this.teardownListeners();
    }
    async handleReconnects() {
        this.disconnecting = false;
        let firstConnect = true;
        let connected = false;
        let disconnected = false;
        const { host, port } = this.chainConfig.provider;
        while (!this.disconnecting && !this.stopping) {
            try {
                try {
                    this.client = await this.provider.getClient(this.network);
                    connected = this.client.rpc.isConnected();
                }
                catch {
                    connected = false;
                }
                if (connected) {
                    if (disconnected || firstConnect) {
                        this.events.emit('connected');
                    }
                }
                else {
                    this.client = await this.provider.getClient(this.network);
                    this.events.emit('disconnected');
                }
                if (disconnected && connected && !firstConnect) {
                    logger_1.default.warn(`${(0, logger_2.timestamp)()} | Reconnected to peer: ${host}:${port || ''} | Chain: ${this.chain} | Network: ${this.network}`);
                }
                if (connected && firstConnect) {
                    firstConnect = false;
                    logger_1.default.info(`${(0, logger_2.timestamp)()} | Connected to peer: ${host}:${port || ''} | Chain: ${this.chain} | Network: ${this.network}`);
                }
                disconnected = !connected;
            }
            catch { /* ignore error */ }
            await (0, utils_1.wait)(5000);
        }
    }
    async connect() {
        this.handleReconnects();
    }
    async getBlock(height) {
        return this.provider.getBlock({ chain: this.chain, network: this.network, blockId: height.toString() });
    }
    async syncWallets() {
        return new Promise(async (resolve) => {
            try {
                const { chain, network } = this;
                // After wallet syncs, start block sync from the current height
                const client = await this.provider.getClient(this.network);
                const { ledger: chainBestBlock } = await client.getBlock({ index: 'latest' });
                this.chainConfig.startHeight = chainBestBlock;
                const count = await walletAddress_1.WalletAddressStorage.collection.countDocuments({ chain, network });
                let done = 0;
                logger_1.default.info(`Syncing ${count} ${chain} ${network} wallets`);
                const lastLog = Date.now();
                const addressStream = walletAddress_1.WalletAddressStorage.collection.find({ chain, network }).stream();
                addressStream
                    .pipe(new stream_1.Transform({
                    objectMode: true,
                    transform: async (data, _, cb) => {
                        if (Date.now() - 5000 > lastLog) {
                            logger_1.default.info(`Syncing ${count - done} ${chain} ${network} wallets`);
                        }
                        const walletAddress = data;
                        const [lastTx] = await transaction_1.XrpTransactionStorage.collection
                            .find({ wallets: walletAddress.wallet, 'wallets.0': { $exists: true } })
                            .sort({ blockTimeNormalized: -1 })
                            .limit(1)
                            .toArray();
                        const synced = await cache_1.CacheStorage.getForWallet(walletAddress.wallet, `sync-${walletAddress.address}`);
                        if (synced) {
                            // if this is happening, it means initial sync wasn't completed the first time, likely due to a crash
                            return cb();
                        }
                        const txs = await this.provider.getAddressTransactions({
                            chain: this.chain,
                            network: this.network,
                            address: walletAddress.address,
                            args: {
                                ...(lastTx && !this.chainConfig.freshSync && { startTx: lastTx.txid })
                            }
                        });
                        if (txs.length) {
                            logger_1.default.info(`Saving ${txs.length} transactions`);
                        }
                        const blockTxs = new Array();
                        const blockCoins = new Array();
                        for (const tx of txs) {
                            const bitcoreTx = this.provider.transformAccountTx(tx, network);
                            const bitcoreCoins = this.provider.transformToCoins(tx, network);
                            const { transaction, coins } = await this.provider.tag(chain, network, bitcoreTx, bitcoreCoins);
                            blockTxs.push(transaction);
                            blockCoins.push(...coins);
                        }
                        await transaction_1.XrpTransactionStorage.batchImport({
                            txs: blockTxs,
                            coins: blockCoins,
                            chain,
                            network,
                            initialSyncComplete: false
                        });
                        await cache_1.CacheStorage.setForWallet(walletAddress.wallet, `sync-${walletAddress.address}`, true, cache_1.CacheStorage.Times.Hour / 2);
                        done++;
                        cb();
                    }
                }))
                    .on('finish', async () => {
                    logger_1.default.info(`FINISHED Syncing ${count} ${chain} ${network} wallets`);
                    this.initialSyncComplete = true;
                    await state_1.StateStorage.collection.findOneAndUpdate({}, { $addToSet: { initialSyncComplete: `${chain}:${network}` } }, { upsert: true });
                    resolve();
                });
            }
            catch (e) {
                logger_1.default.error('%o', e);
            }
        })
            .finally(() => {
            if (this.disconnecting) {
                this.client?.rpc.disconnect();
            }
        });
    }
    async syncBlocks() {
        const { chain, network } = this;
        const client = await this.provider.getClient(this.network);
        let ourBestBlock = await this.provider.getLocalTip({ chain, network });
        const serverInfo = await client.getServerInfo();
        const [earliestLedgerIdx, latestLedgerIdx] = serverInfo.complete_ledgers.split('-');
        let chainBestBlock = Number(latestLedgerIdx);
        const startTime = Date.now();
        let lastLog = Date.now();
        const configuredStart = Math.max(this.chainConfig.startHeight || 0, ourBestBlock?.height || 0, earliestLedgerIdx || 0);
        logger_1.default.info(`Starting XRP Sync @ ${configuredStart}`);
        ourBestBlock = { height: configuredStart };
        const startHeight = ourBestBlock.height;
        let currentHeight = startHeight;
        while (ourBestBlock.height < chainBestBlock && !this.disconnecting) {
            currentHeight = ourBestBlock.height + 1;
            const { ledger: block } = await client.getBlock({ index: currentHeight });
            if (!block) {
                await (0, utils_1.wait)(2000);
                continue;
            }
            const transformedBlock = this.provider.transformLedger(block, network);
            const coinsAndTxs = (block.transactions || [])
                .map((tx) => ({
                tx: this.provider.transform(tx, network, transformedBlock),
                coins: this.provider.transformToCoins(tx, network, transformedBlock)
            }))
                .filter(data => {
                return !!data.tx.txid;
            });
            const blockTxs = new Array();
            const blockCoins = new Array();
            for (const coinAndTx of coinsAndTxs) {
                const { transaction, coins } = await this.provider.tag(chain, network, coinAndTx.tx, coinAndTx.coins);
                if (this.chainConfig.walletOnlySync && !transaction.wallets.length) {
                    continue;
                }
                blockTxs.push(transaction);
                blockCoins.push(...coins);
            }
            await this.blockModel.processBlock({
                chain,
                network,
                block: transformedBlock,
                transactions: blockTxs,
                coins: blockCoins,
                initialSyncComplete: true
            });
            this.maybeLog(chain, network, startHeight, currentHeight, startTime, lastLog);
            lastLog = Date.now();
            ourBestBlock = await this.provider.getLocalTip({ chain, network });
            if (ourBestBlock.height === chainBestBlock) {
                ({ ledger: chainBestBlock } = await client.getBlock({ index: 'latest' }));
            }
        }
        if (this.disconnecting) {
            this.client?.rpc.disconnect();
        }
    }
    async sync() {
        if (this.syncing) {
            return false;
        }
        const { chain, network } = this;
        this.syncing = true;
        const state = await state_1.StateStorage.collection.findOne({});
        this.initialSyncComplete =
            state && state.initialSyncComplete && state.initialSyncComplete.includes(`${chain}:${network}`);
        try {
            if (this.chainConfig.walletOnlySync && !this.initialSyncComplete) {
                await this.syncWallets();
            }
            else {
                await this.syncBlocks();
            }
            logger_1.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;
        }
        catch (e) {
            logger_1.default.error('%o', e);
            this.syncing = false;
            await (0, utils_1.wait)(2000);
            return this.sync();
        }
    }
    maybeLog(chain, network, startHeight, currentHeight, startTime, lastLog) {
        const oneSecond = 1000;
        const now = Date.now();
        if (now - lastLog > oneSecond || startHeight === currentHeight) {
            const blocksProcessed = currentHeight - startHeight;
            const elapsedMinutes = (now - startTime) / (60 * oneSecond);
            logger_1.default.info(`${(0, logger_2.timestamp)()} | Syncing... | Chain: ${chain} | Network: ${network} |${(blocksProcessed / elapsedMinutes)
                .toFixed(2)
                .padStart(8)} blocks/min | Height: ${currentHeight.toString().padStart(7)}`);
        }
    }
    async syncDone() {
        return new Promise(resolve => this.events.once('SYNCDONE', resolve));
    }
    async stop() {
        this.stopping = true;
        logger_1.default.debug(`Stopping worker for chain ${this.chain} ${this.network}`);
        await this.disconnect();
    }
    async start() {
        logger_1.default.debug(`Started worker for chain ${this.chain} ${this.network}`);
        this.connect();
        await this.setupListeners();
        this.sync();
    }
};
exports.XrpP2pWorker = XrpP2pWorker;
exports.XrpP2pWorker = XrpP2pWorker = __decorate([
    Loggify_1.LoggifyClass
], XrpP2pWorker);
//# sourceMappingURL=index.js.map