#!/usr/bin/env node
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs_1 = __importDefault(require("fs"));
const stream_1 = require("stream");
const block_1 = require("../../src/models/block");
const coin_1 = require("../../src/models/coin");
const transaction_1 = require("../../src/models/transaction");
const modules_1 = require("../../src/modules");
const config_1 = require("../../src/services/config");
const storage_1 = require("../../src/services/storage");
const verification_1 = require("../../src/services/verification");
(async () => {
    const { CHAIN, NETWORK, FILE, DRYRUN = true } = process.env;
    if (!CHAIN || !NETWORK || !FILE) {
        console.log('CHAIN, NETWORK, and FILE env variable are required');
        process.exit(1);
    }
    const dry = DRYRUN && DRYRUN !== 'false';
    const chain = CHAIN || '';
    const network = NETWORK || '';
    await storage_1.Storage.start();
    modules_1.Modules.loadConfigured();
    const chainConfig = config_1.Config.chainConfig({ chain, network });
    const workerClass = verification_1.Verification.get(chain, network);
    const worker = new workerClass({ chain, network, chainConfig });
    await worker.connect();
    const handleRepair = async (data) => {
        try {
            const tip = await block_1.BitcoinBlockStorage.getLocalTip({ chain, network });
            switch (data.type) {
                case 'DUPE_TRANSACTION':
                    {
                        const tx = data.payload.tx;
                        const dupeTxs = await transaction_1.TransactionStorage.collection
                            .find({ chain: tx.chain, network: tx.network, txid: tx.txid })
                            .sort({ blockHeight: -1 })
                            .toArray();
                        if (dupeTxs.length < 2) {
                            console.log('No action required.', dupeTxs.length, 'transaction');
                            return;
                        }
                        const toKeep = dupeTxs[0];
                        const wouldBeDeleted = dupeTxs.filter(c => c._id != toKeep._id);
                        if (dry) {
                            console.log('WOULD DELETE');
                            console.log(wouldBeDeleted);
                        }
                        else {
                            console.log('Deleting', wouldBeDeleted.length, 'transactions');
                            await transaction_1.TransactionStorage.collection.deleteMany({
                                chain,
                                network,
                                _id: { $in: wouldBeDeleted.map(c => c._id) }
                            });
                        }
                    }
                    break;
                case 'FORK_PRUNE_COIN':
                    {
                        const coin = data.payload.coin;
                        const forkCoins = await coin_1.CoinStorage.collection
                            .find({ chain, network, mintTxid: coin.mintTxid, mintIndex: coin.mintIndex, spentHeight: -2 })
                            .sort({ mintHeight: -1, spentHeight: -1 })
                            .toArray();
                        if (forkCoins.length === 0) {
                            console.log('No action required. Coin already pruned');
                            return;
                        }
                        const wouldBeDeleted = forkCoins;
                        if (dry) {
                            console.log('WOULD DELETE');
                            console.log(wouldBeDeleted);
                        }
                        else {
                            console.log('Deleting', wouldBeDeleted.length, 'coins');
                            await coin_1.CoinStorage.collection.deleteMany({
                                chain,
                                network,
                                _id: { $in: wouldBeDeleted.map(c => c._id) }
                            });
                        }
                    }
                    break;
                case 'DUPE_COIN':
                    {
                        const coin = data.payload.coin;
                        const dupeCoins = await coin_1.CoinStorage.collection
                            .find({ chain, network, mintTxid: coin.mintTxid, mintIndex: coin.mintIndex })
                            .sort({ mintHeight: -1, spentHeight: -1 })
                            .toArray();
                        if (dupeCoins.length < 2) {
                            console.log('No action required.', dupeCoins.length, 'coin');
                            return;
                        }
                        let toKeep = dupeCoins[0];
                        const spentCoin = dupeCoins.find(c => c.spentHeight > toKeep.spentHeight);
                        toKeep = spentCoin || toKeep;
                        const wouldBeDeleted = dupeCoins.filter(c => c._id != toKeep._id);
                        if (dry) {
                            console.log('WOULD DELETE');
                            console.log(wouldBeDeleted);
                        }
                        else {
                            const { mintIndex, mintTxid } = toKeep;
                            console.log('Deleting', wouldBeDeleted.length, 'coins');
                            await coin_1.CoinStorage.collection.deleteMany({
                                chain,
                                network,
                                mintTxid,
                                mintIndex,
                                _id: { $in: wouldBeDeleted.map(c => c._id) }
                            });
                        }
                    }
                    break;
                case 'COIN_HEIGHT_MISMATCH':
                case 'CORRUPTED_BLOCK':
                case 'MISSING_BLOCK':
                case 'MISSING_TX':
                case 'MISSING_COIN_FOR_TXID':
                case 'VALUE_MISMATCH':
                case 'COIN_SHOULD_BE_SPENT':
                case 'NEG_FEE':
                    const blockHeight = Number(data.payload.blockNum);
                    const { success } = await worker.validateDataForBlock(blockHeight, tip.height);
                    if (success) {
                        console.log('No errors found, repaired previously');
                        return;
                    }
                    if (dry) {
                        console.log('WOULD RESYNC BLOCKS', blockHeight, 'to', blockHeight + 1);
                        console.log(data.payload);
                    }
                    else {
                        console.log('Resyncing Blocks', blockHeight, 'to', blockHeight + 1);
                        await worker.resync(blockHeight - 1, blockHeight + 1);
                        const { success, errors } = await worker.validateDataForBlock(blockHeight, tip.height);
                        if (success) {
                            console.log('REPAIR SOLVED ISSUE');
                        }
                        else {
                            console.log('REPAIR FAILED TO SOLVE ISSUE');
                            console.log(JSON.stringify(errors, null, 2));
                        }
                    }
                    break;
                case 'DUPE_BLOCKHEIGHT':
                case 'DUPE_BLOCKHASH':
                    const dupeBlock = await block_1.BitcoinBlockStorage.collection
                        .find({ chain, network, height: data.payload.blockNum })
                        .toArray();
                    if (dupeBlock.length < 2) {
                        console.log('No action required.', dupeBlock.length, 'block');
                        return;
                    }
                    let toKeepBlock = dupeBlock[0];
                    const processedBlock = dupeBlock.find(b => b.processed === true);
                    toKeepBlock = processedBlock || toKeepBlock;
                    const wouldBeDeletedBlock = dupeBlock.filter(c => c._id !== toKeepBlock._id);
                    if (dry) {
                        console.log('WOULD DELETE');
                        console.log(wouldBeDeletedBlock);
                    }
                    else {
                        console.log('Deleting', wouldBeDeletedBlock.length, 'block');
                        await block_1.BitcoinBlockStorage.collection.deleteMany({
                            chain,
                            network,
                            _id: { $in: wouldBeDeletedBlock.map(c => c._id) }
                        });
                    }
                    break;
                default:
                    console.log('skipping');
            }
        }
        catch (e) {
            console.error(e);
        }
    };
    function getLinesFromChunk(chunk) {
        return chunk.toString().split('\n');
    }
    async function repairLineIfValidJson(line) {
        const dataStr = line.trim();
        if (dataStr && dataStr.length > 2) {
            if (dataStr.startsWith('{') && dataStr.endsWith('}')) {
                try {
                    const parsedData = JSON.parse(line);
                    console.log('Inspecting...');
                    console.log(dataStr);
                    await handleRepair(parsedData);
                }
                catch (err) { /* ignore error */ }
            }
        }
    }
    async function transformFileChunks(chunk, _, cb) {
        for (const line of getLinesFromChunk(chunk)) {
            await repairLineIfValidJson(line);
        }
        cb();
    }
    const getFileContents = FILE => {
        fs_1.default.createReadStream(FILE)
            .pipe(new stream_1.Transform({
            write: transformFileChunks
        }))
            .on('end', () => {
            process.exit(0);
        })
            .on('finish', () => {
            process.exit(0);
        });
    };
    getFileContents(FILE);
})();
//# sourceMappingURL=db-repair.js.map