"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 });
const chai_1 = require("chai");
const crypto = __importStar(require("crypto"));
const crypto_wallet_core_1 = require("@bitpay-labs/crypto-wallet-core");
const coin_1 = require("../../../src/models/coin");
const transaction_1 = require("../../../src/models/transaction");
const helpers_1 = require("../../helpers");
const integration_1 = require("../../helpers/integration");
describe('Coin Model', function () {
    // eslint-disable-next-line @typescript-eslint/no-this-alias
    const suite = this;
    this.timeout(30000);
    function createNewTxid() {
        const seed = (Math.random() * 10000).toString();
        return crypto
            .createHash('sha256')
            .update(seed + 1)
            .digest()
            .toString('hex');
    }
    async function addTx(tx, outputs) {
        await transaction_1.TransactionStorage.collection.insertOne(tx);
        await coin_1.CoinStorage.collection.insertMany(outputs);
    }
    async function makeMempoolTxChain(chain, network, startingTxid, chainLength = 1) {
        let txid = startingTxid;
        let nextTxid = createNewTxid();
        const allTxids = new Array();
        for (let i = 1; i <= chainLength; i++) {
            const badMempoolTx = {
                chain,
                network,
                blockHeight: -1,
                txid
            };
            const badMempoolOutputs = [
                {
                    chain,
                    network,
                    mintHeight: -1,
                    mintTxid: txid,
                    spentTxid: i != chainLength ? nextTxid : '',
                    mintIndex: 0,
                    spentHeight: -1
                }
            ];
            await addTx(badMempoolTx, badMempoolOutputs);
            allTxids.push(txid);
            txid = nextTxid;
            nextTxid = createNewTxid();
        }
        return allTxids;
    }
    before(integration_1.intBeforeHelper);
    after(async () => (0, integration_1.intAfterHelper)(suite));
    beforeEach(async () => {
        await (0, helpers_1.resetDatabase)();
    });
    const chain = 'BTC';
    const network = 'integration';
    const blockTx = {
        chain,
        network,
        blockHeight: 1,
        txid: '01234'
    };
    const blockTxOutputs = {
        chain,
        network,
        mintHeight: 1,
        mintTxid: '01234',
        mintIndex: 0,
        spentHeight: -1,
        spentTxid: '12345'
    };
    const block2TxOutputs = {
        chain,
        network,
        mintHeight: 2,
        mintTxid: '123456',
        mintIndex: 0,
        spentHeight: -1
    };
    it('should appropriately mark coins related to transactions that are in mempool, but no longer valid', async () => {
        // insert a valid tx, with a valid output
        await transaction_1.TransactionStorage.collection.insertOne(blockTx);
        await coin_1.CoinStorage.collection.insertOne(blockTxOutputs);
        const chainLength = 5;
        const txids = await makeMempoolTxChain(chain, network, blockTxOutputs.spentTxid, chainLength);
        const allRelatedCoins = await transaction_1.TransactionStorage.findAllRelatedOutputs(blockTxOutputs.spentTxid);
        (0, chai_1.expect)(allRelatedCoins.length).to.eq(chainLength);
        const spentOps = new Array();
        spentOps.push({
            updateOne: {
                filter: {
                    chain,
                    network,
                    mintIndex: blockTxOutputs.mintIndex,
                    mintTxid: blockTxOutputs.mintTxid,
                    spentHeight: { $lt: 0 }
                },
                update: { $set: { spentHeight: block2TxOutputs.mintHeight, spentTxid: block2TxOutputs.mintTxid } }
            }
        });
        await transaction_1.TransactionStorage.pruneMempool({
            chain,
            network,
            initialSyncComplete: true,
            spendOps: spentOps
        });
        const badTxs = await transaction_1.TransactionStorage.collection.find({ chain, network, txid: { $in: txids } }).sort({ _id: 1 }).toArray();
        (0, chai_1.expect)(badTxs.length).to.eq(chainLength);
        // the replaced tx is marked as conflicting, all the rest still pending to be cleaned up by pruning service
        (0, chai_1.expect)(badTxs[0].blockHeight).to.eq(-3 /* SpentHeightIndicators.conflicting */);
        (0, chai_1.expect)(badTxs[0].replacedByTxid).to.exist;
        (0, chai_1.expect)(badTxs.slice(1).every(tx => tx.blockHeight === -1 /* SpentHeightIndicators.pending */)).to.equal(true);
        const goodTxs = await transaction_1.TransactionStorage.collection.find({ chain, network, txid: blockTx.txid }).toArray();
        (0, chai_1.expect)(goodTxs.length).to.eq(1);
        (0, chai_1.expect)(goodTxs[0].txid).to.eq(blockTx.txid);
        (0, chai_1.expect)(goodTxs[0].blockHeight).to.eq(blockTx.blockHeight);
        // Coins
        const badNewCoins = await coin_1.CoinStorage.collection.find({ chain, network, mintTxid: { $in: txids } }).toArray();
        (0, chai_1.expect)(badNewCoins.length).to.equal(badNewCoins.filter(c => c.spentHeight == -1 /* SpentHeightIndicators.pending */).length);
        const goodNewCoins = await coin_1.CoinStorage.collection.find({ chain, network, mintTxid: blockTx.txid }).toArray();
        (0, chai_1.expect)(goodNewCoins.length).to.equal(goodNewCoins.filter(c => c.spentHeight == -2 /* SpentHeightIndicators.unspent */).length);
    });
    it('should appropriately mark coins related to transactions that are RBFed', async () => {
        const privateKey = new crypto_wallet_core_1.BitcoreLib.PrivateKey('L1uyy5qTuGrVXrmrsvHWHgVzW9kKdrp27wBC7Vs6nZDTF2BRUVwy');
        const utxo1 = {
            txId: createNewTxid(),
            outputIndex: 0,
            address: '17XBj6iFEsf8kzDMGQk5ghZipxX49VXuaV',
            script: '76a91447862fe165e6121af80d5dde1ecb478ed170565b88ac',
            satoshis: 50000
        };
        // create tx with mutliple outputs
        const tx1 = new crypto_wallet_core_1.BitcoreLib.Transaction()
            .from(utxo1)
            .to('1Gokm82v6DmtwKEB8AiVhm82hyFSsEvBDK', 15000)
            .to('1Gokm82v6DmtwKEB8AiVhm82hyFSsEvBDK', 13000)
            .to('1Gokm82v6DmtwKEB8AiVhm82hyFSsEvBDK', 11000)
            .sign(privateKey);
        // import transaction in block 1
        await transaction_1.TransactionStorage.batchImport({
            txs: [tx1],
            height: 1,
            initialSyncComplete: true,
            chain: 'BTC',
            network: 'integration'
        });
        // insert mempool tx using all outputs from last tx
        const mempoolTxid = createNewTxid();
        const mempoolTx = {
            chain,
            network,
            blockHeight: -1, // pending
            txid: mempoolTxid
        };
        const mempoolOutputs = Array.from({ length: 3 }, (_v, i) => i).map(i => {
            return {
                chain,
                network,
                mintHeight: -1,
                mintTxid: mempoolTxid,
                mintIndex: i,
                spentHeight: -2 /* SpentHeightIndicators.unspent */
            };
        });
        await addTx(mempoolTx, mempoolOutputs);
        // update existing outputs to be spent by mempool tx
        await coin_1.CoinStorage.collection.updateMany({ chain, network, mintTxid: tx1.hash }, { $set: { spentTxid: mempoolTxid, spentHeight: -1 /* SpentHeightIndicators.pending */ } });
        // create new tx that uses one of the inputs
        const utxo2 = [
            {
                txId: tx1.hash,
                outputIndex: 0,
                address: '1Gokm82v6DmtwKEB8AiVhm82hyFSsEvBDK',
                script: '76a91447862fe165e6121af80d5dde1ecb478ed170565b88ac',
                satoshis: 15000
            }
        ];
        const tx2 = new crypto_wallet_core_1.BitcoreLib.Transaction()
            .from(utxo2)
            .to('bc1qm0jxvjvj6pzcc64lu4k7vccsg2x22pj60zke6c', 15000)
            .sign(privateKey);
        // import transaction in block 2
        await transaction_1.TransactionStorage.batchImport({
            txs: [tx2],
            height: 2,
            initialSyncComplete: true,
            chain: 'BTC',
            network: 'integration'
        });
        const tx1Outputs = await coin_1.CoinStorage.collection.find({ chain, network, mintTxid: tx1.hash }).toArray();
        const spentCoin = tx1Outputs.find(c => c.spentTxid === tx2.hash && c.spentHeight === 2);
        (0, chai_1.expect)(spentCoin).to.exist;
        const unspentCoins = tx1Outputs.filter(c => c.spentHeight < 0 /* SpentHeightIndicators.minimum */);
        (0, chai_1.expect)(unspentCoins.length).to.equal(2);
        (0, chai_1.expect)(unspentCoins.filter(c => c.spentHeight === -2 /* SpentHeightIndicators.unspent */ && !c.spentTxid).length).to.equal(2);
        const mempoolCoins = await coin_1.CoinStorage.collection.find({ chain, network, mintTxid: mempoolTxid }).toArray();
        (0, chai_1.expect)(mempoolCoins.length).to.equal(3);
        (0, chai_1.expect)(mempoolCoins.filter(c => c.mintHeight === -3 /* SpentHeightIndicators.conflicting */).length).to.equal(3);
    });
});
//# sourceMappingURL=coin.test.js.map