"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const bson_1 = require("bson");
const chai_1 = require("chai");
const sinon_1 = __importDefault(require("sinon"));
const coin_1 = require("../../../src/models/coin");
const transaction_1 = require("../../../src/models/transaction");
const pruning_1 = require("../../../src/services/pruning");
require("../../../src/utils/polyfills");
const helpers_1 = require("../../helpers");
const integration_1 = require("../../helpers/integration");
const rpc_1 = require("../../../src/rpc");
const logger_1 = __importDefault(require("../../../src/logger"));
describe('Pruning Service', function () {
    const Pruning = new pruning_1.PruningService({ transactionModel: transaction_1.TransactionStorage, coinModel: coin_1.CoinStorage });
    // eslint-disable-next-line @typescript-eslint/no-this-alias
    const suite = this;
    this.timeout(30000);
    const sandbox = sinon_1.default.createSandbox();
    before(integration_1.intBeforeHelper);
    after(async () => (0, integration_1.intAfterHelper)(suite));
    beforeEach(async () => {
        await (0, helpers_1.resetDatabase)();
        Pruning.lastRunTimeInvalid = 0;
        Pruning.lastRunTimeOld = 0;
        Pruning.registerRpcs();
        Pruning.rpcs['BTC:mainnet'] = new rpc_1.RPC('user', 'pw', 'host', 'port');
        sandbox.stub(Pruning.rpcs['BTC:mainnet'], 'getBlockHeight').resolves(1240);
        process.env.DRYRUN = 'false';
        sandbox.spy(logger_1.default, 'info');
        sandbox.spy(logger_1.default, 'warn');
        sandbox.spy(logger_1.default, 'error');
    });
    afterEach(() => {
        process.env.DRYRUN = undefined;
        sandbox.restore();
    });
    const replacementTx = {
        chain: 'BTC',
        network: 'mainnet',
        blockHeight: 1234,
        txid: 'replacementTx',
    };
    const invalidTx = {
        chain: 'BTC',
        network: 'mainnet',
        blockHeight: -3 /* SpentHeightIndicators.conflicting */,
        txid: 'invalidCoin',
        replacedByTxid: 'replacementTx'
    };
    const invalidCoin = {
        chain: 'BTC',
        network: 'mainnet',
        mintHeight: -1 /* SpentHeightIndicators.pending */,
        mintTxid: 'invalidCoin',
        spentHeight: -1 /* SpentHeightIndicators.pending */,
        spentTxid: 'spentInMempool'
    };
    const mempoolCoin = {
        chain: 'BTC',
        network: 'mainnet',
        mintHeight: -1 /* SpentHeightIndicators.pending */,
        mintTxid: 'spentInMempool',
        spentHeight: -1 /* SpentHeightIndicators.pending */,
        spentTxid: 'spentInMempoolAgain'
    };
    const mempoolCoin2 = {
        chain: 'BTC',
        network: 'mainnet',
        mintHeight: -1 /* SpentHeightIndicators.pending */,
        mintTxid: 'spentInMempoolAgain'
    };
    const oldMempoolTx = {
        chain: 'BTC',
        network: 'mainnet',
        blockHeight: -1 /* SpentHeightIndicators.pending */,
        txid: 'oldMempoolTx',
        blockTimeNormalized: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
    };
    const oldMempoolTxOutput = {
        chain: 'BTC',
        network: 'mainnet',
        mintHeight: -1 /* SpentHeightIndicators.pending */,
        mintTxid: 'oldMempoolTx',
        spentTxid: 'oldMempoolTx2'
    };
    const oldMempoolTx2Output = {
        chain: 'BTC',
        network: 'mainnet',
        mintHeight: -1 /* SpentHeightIndicators.pending */,
        mintTxid: 'oldMempoolTx2',
        spentTxid: ''
    };
    const parentTxOutput1 = {
        chain: 'BTC',
        network: 'mainnet',
        mintHeight: 1234,
        mintTxid: 'parentTx',
        spentHeight: -1 /* SpentHeightIndicators.pending */,
        spentTxid: 'oldMempoolTx' // this output is an input for oldMempoolTx
    };
    const parentTxOutput2 = {
        chain: 'BTC',
        network: 'mainnet',
        mintHeight: 1234,
        mintTxid: 'parentTx',
        spentHeight: -1 /* SpentHeightIndicators.pending */,
        spentTxid: 'imaginaryTx' // another imaginary tx spent this output. This is here to make sure we don't mark this output as unspent
    };
    function modTxid(orig, i) {
        return orig + (i ? '_' + i : '');
    }
    async function insertBadCoins() {
        await transaction_1.TransactionStorage.collection.insertOne(invalidTx);
        await coin_1.CoinStorage.collection.insertOne(invalidCoin);
        await coin_1.CoinStorage.collection.insertOne(mempoolCoin);
        await coin_1.CoinStorage.collection.insertOne(mempoolCoin2);
        await transaction_1.TransactionStorage.collection.insertOne(replacementTx);
        return [invalidCoin, mempoolCoin, mempoolCoin2];
    }
    async function insertOldTx(i = 0) {
        await coin_1.CoinStorage.collection.insertOne({ ...parentTxOutput1, spentTxid: modTxid(parentTxOutput1.spentTxid, i) });
        await coin_1.CoinStorage.collection.insertOne({ ...parentTxOutput2, spentTxid: modTxid(parentTxOutput2.spentTxid, i) });
        await transaction_1.TransactionStorage.collection.insertOne({ ...oldMempoolTx, txid: modTxid(oldMempoolTx.txid, i) });
        await coin_1.CoinStorage.collection.insertOne({ ...oldMempoolTxOutput, mintTxid: modTxid(oldMempoolTxOutput.mintTxid, i), spentTxid: modTxid(oldMempoolTxOutput.spentTxid, i) });
        await coin_1.CoinStorage.collection.insertOne({ ...oldMempoolTx2Output, mintTxid: modTxid(oldMempoolTx2Output.mintTxid, i) });
    }
    describe('processAllInvalidTxs', function () {
        it('should detect coins that should be invalid but are not', async () => {
            await insertBadCoins();
            const { chain, network } = invalidCoin;
            await Pruning.processAllInvalidTxs(chain, network);
            const shouldBeInvalid = await coin_1.CoinStorage.collection
                .find({ chain, network, mintTxid: { $in: [invalidCoin.mintTxid, mempoolCoin.mintTxid, mempoolCoin2.mintTxid] } })
                .toArray();
            for (const coin of shouldBeInvalid) {
                (0, chai_1.expect)(coin.mintHeight).eq(-3 /* SpentHeightIndicators.conflicting */);
            }
            (0, chai_1.expect)(shouldBeInvalid.length).eq(3);
        });
        it('should mark detected coins as invalid', async () => {
            await insertBadCoins();
            const { chain, network } = invalidCoin;
            await Pruning.processAllInvalidTxs(chain, network);
            const shouldBeInvalid = await coin_1.CoinStorage.collection
                .find({ chain, network, mintTxid: { $in: [invalidCoin.mintTxid, mempoolCoin.mintTxid, mempoolCoin2.mintTxid] } })
                .toArray();
            for (const coin of shouldBeInvalid) {
                (0, chai_1.expect)(coin.mintHeight).eq(-3 /* SpentHeightIndicators.conflicting */);
            }
            (0, chai_1.expect)(shouldBeInvalid.length).eq(3);
        });
        it('should not invalidate a mined tx', async function () {
            await insertBadCoins();
            const { chain, network } = invalidCoin;
            await Pruning.invalidateTx(chain, network, replacementTx);
            const shouldBeInvalidStill = await coin_1.CoinStorage.collection
                .find({ chain, network, mintTxid: { $in: [invalidCoin.mintTxid, mempoolCoin.mintTxid, mempoolCoin2.mintTxid] } })
                .toArray();
            (0, chai_1.expect)(shouldBeInvalidStill.every(coin => coin.mintHeight === -1 /* SpentHeightIndicators.pending */)).to.equal(true);
            (0, chai_1.expect)(shouldBeInvalidStill.length).eq(3);
            const msg = `Tx ${replacementTx.txid} is already mined`;
            (0, chai_1.expect)(logger_1.default.warn.args.find((args) => args[0] === msg)).to.exist;
        });
        it('should not go into an infinite loop if there is a circular replacedByTxid reference', async function () {
            await insertBadCoins();
            await transaction_1.TransactionStorage.collection.insertOne({
                ...replacementTx,
                _id: new bson_1.ObjectId(),
                txid: 'replacementTx2',
                blockHeight: -1 /* SpentHeightIndicators.pending */,
                replacedByTxid: replacementTx.txid
            });
            await transaction_1.TransactionStorage.collection.updateOne({
                txid: replacementTx.txid
            }, {
                $set: {
                    replacedByTxid: 'replacementTx2'
                }
            });
            // at this point, invalidTx => replacementTx => replacementTx2 => replacementTx
            const { chain, network } = invalidCoin;
            await Pruning.processAllInvalidTxs(chain, network);
            const shouldBeInvalid = await coin_1.CoinStorage.collection
                .find({ chain, network, mintTxid: { $in: [invalidCoin.mintTxid, mempoolCoin.mintTxid, mempoolCoin2.mintTxid] } })
                .toArray();
            (0, chai_1.expect)(shouldBeInvalid.every(coin => coin.mintHeight === -3 /* SpentHeightIndicators.conflicting */)).to.equal(true);
            (0, chai_1.expect)(shouldBeInvalid.length).eq(3);
        });
        it('should not go into an infinite loop if there is a circular, unconfirmed replacedByTxid reference', async function () {
            await insertBadCoins();
            await transaction_1.TransactionStorage.collection.insertOne({
                ...replacementTx,
                _id: new bson_1.ObjectId(),
                txid: 'replacementTx2',
                blockHeight: -1 /* SpentHeightIndicators.pending */,
                replacedByTxid: replacementTx.txid
            });
            await transaction_1.TransactionStorage.collection.updateOne({
                txid: replacementTx.txid
            }, {
                $set: {
                    replacedByTxid: 'replacementTx2',
                    blockHeight: -1 /* SpentHeightIndicators.pending */
                }
            });
            // at this point, invalidTx => replacementTx => replacementTx2 => replacementTx
            // but replacementTx is still unconfirmed
            const { chain, network } = invalidCoin;
            await Pruning.processAllInvalidTxs(chain, network);
            const shouldBePendingStill = await coin_1.CoinStorage.collection
                .find({ chain, network, mintTxid: { $in: [invalidCoin.mintTxid, mempoolCoin.mintTxid, mempoolCoin2.mintTxid] } })
                .toArray();
            (0, chai_1.expect)(shouldBePendingStill.every(coin => coin.mintHeight === -1 /* SpentHeightIndicators.pending */)).to.equal(true);
            (0, chai_1.expect)(shouldBePendingStill.length).eq(3);
            const msg = `Skipping invalidation of ${invalidTx.txid} with immature replacement => ${invalidTx.replacedByTxid}`;
            (0, chai_1.expect)(logger_1.default.info.args.find((args) => args[0] === msg)).to.exist;
        });
    });
    describe('processOldMempoolTxs', function () {
        it('should remove old transactions', async () => {
            sandbox.stub(rpc_1.RPC.prototype, 'getTransaction').resolves(null);
            await insertOldTx();
            const { chain, network } = oldMempoolTx;
            const count = await transaction_1.TransactionStorage.collection.countDocuments({
                chain,
                network,
                blockHeight: -1,
                blockTimeNormalized: { $lt: new Date() }
            });
            (0, chai_1.expect)(count).eq(1);
            await Pruning.processOldMempoolTxs(chain, network, 29);
            const shouldBeExpiredTx = await transaction_1.TransactionStorage.collection
                .find({ chain, network, txid: { $in: [oldMempoolTx.txid] } })
                .toArray();
            const shouldBeExpiredCoins = await coin_1.CoinStorage.collection
                .find({ chain, network, mintTxid: { $in: [oldMempoolTxOutput.mintTxid, oldMempoolTx2Output.mintTxid] } })
                .toArray();
            const parentTxOutputs = await coin_1.CoinStorage.collection
                .find({ chain, network, mintTxid: parentTxOutput1.mintTxid })
                .toArray();
            (0, chai_1.expect)(shouldBeExpiredTx.length).eq(1);
            (0, chai_1.expect)(shouldBeExpiredTx.every(tx => tx.blockHeight === -5 /* SpentHeightIndicators.expired */)).to.equal(true);
            (0, chai_1.expect)(shouldBeExpiredCoins.length).eq(2);
            (0, chai_1.expect)(shouldBeExpiredCoins.every(coin => coin.mintHeight === -5 /* SpentHeightIndicators.expired */)).to.equal(true);
            (0, chai_1.expect)(parentTxOutputs.length).eq(2);
            (0, chai_1.expect)(parentTxOutputs.filter(coin => coin.spentHeight === -2 /* SpentHeightIndicators.unspent */).length).to.equal(1);
        });
        it('should skip removing transactions still in mempool', async () => {
            const rpcStub = sandbox.stub(rpc_1.RPC.prototype, 'getTransaction');
            rpcStub.onCall(0).resolves(null);
            rpcStub.onCall(1).resolves({});
            rpcStub.onCall(2).resolves(null);
            await insertOldTx(0);
            await insertOldTx(1);
            await insertOldTx(2);
            const { chain, network } = oldMempoolTx;
            const count = await transaction_1.TransactionStorage.collection.countDocuments({
                chain,
                network,
                blockHeight: -1 /* SpentHeightIndicators.pending */,
                blockTimeNormalized: { $lt: new Date() }
            });
            (0, chai_1.expect)(count).eq(3);
            await Pruning.processOldMempoolTxs(chain, network, 29);
            const processedTxs = await transaction_1.TransactionStorage.collection
                .find({ chain, network, txid: { $in: [modTxid(oldMempoolTx.txid, 0), modTxid(oldMempoolTx.txid, 1), modTxid(oldMempoolTx.txid, 2)] } })
                .toArray();
            const processedCoins = await coin_1.CoinStorage.collection
                .find({ chain, network, mintTxid: { $in: [modTxid(oldMempoolTxOutput.mintTxid, 0), modTxid(oldMempoolTx2Output.mintTxid, 0), modTxid(oldMempoolTxOutput.mintTxid, 1), modTxid(oldMempoolTx2Output.mintTxid, 1), modTxid(oldMempoolTxOutput.mintTxid, 2), modTxid(oldMempoolTx2Output.mintTxid, 2)] } })
                .toArray();
            (0, chai_1.expect)(processedTxs.length).eq(3);
            (0, chai_1.expect)(processedTxs.filter(tx => tx.blockHeight === -5 /* SpentHeightIndicators.expired */).length).eq(2);
            (0, chai_1.expect)(processedTxs.filter(tx => tx.blockHeight === -1 /* SpentHeightIndicators.pending */).length).eq(1); // still in mempool
            (0, chai_1.expect)(processedCoins.length).eq(6);
            (0, chai_1.expect)(processedCoins.filter(coin => coin.mintHeight === -5 /* SpentHeightIndicators.expired */).length).eq(4);
            (0, chai_1.expect)(processedCoins.filter(coin => coin.mintHeight === -1 /* SpentHeightIndicators.pending */).length).eq(2); // still in mempool
        });
        it('should skip removing transactions on rpc error', async () => {
            const rpcStub = sandbox.stub(rpc_1.RPC.prototype, 'getTransaction');
            rpcStub.onCall(0).rejects({ code: -1, message: 'hahaha' });
            await insertOldTx();
            const { chain, network } = oldMempoolTx;
            const count = await transaction_1.TransactionStorage.collection.countDocuments({
                chain,
                network,
                blockHeight: -1 /* SpentHeightIndicators.pending */,
                blockTimeNormalized: { $lt: new Date() }
            });
            (0, chai_1.expect)(count).eq(1);
            await Pruning.processOldMempoolTxs(chain, network, 29);
            const shouldBeGoneTx = await transaction_1.TransactionStorage.collection
                .find({ chain, network, txid: { $in: [oldMempoolTx.txid] } })
                .toArray();
            const shouldBeGoneCoins = await coin_1.CoinStorage.collection
                .find({ chain, network, mintTxid: { $in: [oldMempoolTxOutput.mintTxid, oldMempoolTx2Output.mintTxid] } })
                .toArray();
            (0, chai_1.expect)(shouldBeGoneTx.length).eq(1);
            (0, chai_1.expect)(shouldBeGoneCoins.length).eq(2);
        });
        it('should skip removing transactions if coin has >0 confs', async () => {
            const rpcStub = sandbox.stub(rpc_1.RPC.prototype, 'getTransaction');
            rpcStub.onCall(0).rejects({ code: -5, message: 'already exists' });
            const oldMempoolTx2OutputHeight = oldMempoolTx2Output.mintHeight;
            oldMempoolTx2Output.mintHeight = 1;
            await insertOldTx();
            oldMempoolTx2Output.mintHeight = oldMempoolTx2OutputHeight; // reset
            const { chain, network } = oldMempoolTx;
            const count = await transaction_1.TransactionStorage.collection.countDocuments({
                chain,
                network,
                blockHeight: -1 /* SpentHeightIndicators.pending */,
                blockTimeNormalized: { $lt: new Date() }
            });
            (0, chai_1.expect)(count).eq(1);
            await Pruning.processOldMempoolTxs(chain, network, 29);
            const shouldBeGoneTx = await transaction_1.TransactionStorage.collection
                .find({ chain, network, txid: { $in: [oldMempoolTx.txid] } })
                .toArray();
            const shouldBeGoneCoins = await coin_1.CoinStorage.collection
                .find({ chain, network, mintTxid: { $in: [oldMempoolTxOutput.mintTxid, oldMempoolTx2Output.mintTxid] } })
                .toArray();
            (0, chai_1.expect)(shouldBeGoneTx.length).eq(1);
            (0, chai_1.expect)(shouldBeGoneCoins.length).eq(2);
        });
    });
});
//# sourceMappingURL=pruning.test.js.map