#!/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 });
exports.blocks = blocks;
exports.transactions = transactions;
const chai_1 = require("chai");
const config_1 = __importDefault(require("../../src/config"));
const logger_1 = __importDefault(require("../../src/logger"));
const block_1 = require("../../src/models/block");
const coin_1 = require("../../src/models/coin");
const transaction_1 = require("../../src/models/transaction");
const walletAddress_1 = require("../../src/models/walletAddress");
const chain_state_1 = require("../../src/providers/chain-state");
const rpc_1 = require("../../src/rpc");
const storage_1 = require("../../src/services/storage");
const SATOSHI = 100000000.0;
async function blocks(info, creds) {
    const rpc = new rpc_1.AsyncRPC(creds.username, creds.password, creds.host, creds.port);
    const tip = await chain_state_1.ChainStateProvider.getLocalTip({ chain: info.chain, network: info.network });
    const heights = new Array(tip.height).fill(false);
    const times = new Array(tip.height).fill(0);
    const normalizedTimes = new Array(tip.height).fill(0);
    // check each block
    const cursor = block_1.BitcoinBlockStorage.collection.find({
        chain: info.chain,
        network: info.network
    });
    while (await cursor.hasNext()) {
        const block = await cursor.next();
        if (!block)
            break;
        if (!block.processed)
            continue;
        logger_1.default.info(`verifying block ${block.hash}: ${block.height}`);
        // Check there's all unique heights
        (0, chai_1.expect)(block.height, 'block height').to.be.gte(1);
        (0, chai_1.expect)(block.height, 'block height').to.be.lte(tip.height);
        (0, chai_1.expect)(heights[block.height - 1], 'height already used').to.be.false;
        heights[block.height - 1] = true;
        // Check times are increasing
        times[block.height - 1] = block.time.getTime();
        normalizedTimes[block.height - 1] = block.timeNormalized.getTime();
        const truth = await rpc.verbose_block(block.hash);
        (0, chai_1.expect)(block.height, 'block height').to.equal(truth.height);
        (0, chai_1.expect)(block.hash, 'block hash').to.equal(truth.hash);
        (0, chai_1.expect)(block.version, 'block version').to.equal(truth.version);
        (0, chai_1.expect)(block.merkleRoot, 'block merkle root').to.equal(truth.merkleroot);
        (0, chai_1.expect)(block.nonce, 'block nonce').to.equal(truth.nonce);
        (0, chai_1.expect)(block.previousBlockHash, 'block prev hash').to.equal(truth.previousblockhash);
        (0, chai_1.expect)(block.transactionCount, 'block tx count').to.equal(truth.tx.length);
        if (info.network !== 'regtest') {
            (0, chai_1.expect)(block.size, 'block size').to.equal(truth.size);
        }
        (0, chai_1.expect)(block.bits.toString(16), 'block bits').to.equal(truth.bits);
        (0, chai_1.expect)(block.processed, 'block processed').to.equal(true);
        (0, chai_1.expect)(block.time.getTime(), 'block time').to.equal(truth.time * 1000);
        if (block.height < tip.height) {
            (0, chai_1.expect)(block.nextBlockHash, 'block next hash').to.equal(truth.nextblockhash);
        }
        // Transaction Specifics
        {
            const coinbase = truth.tx[0];
            // Check reward
            const reward = coinbase.vout.reduce((a, b) => a + b.value, 0);
            (0, chai_1.expect)(block.reward, 'block reward').to.equal(Math.round(reward * SATOSHI));
            // Check block only has all `truth`'s transactions
            const ours = await transaction_1.TransactionStorage.collection
                .find({
                chain: info.chain,
                network: info.network,
                txid: {
                    $in: truth.tx.map(tx => tx.txid)
                }
            })
                .project({
                txid: true,
                coinbase: true,
                blockHash: true,
                blockHeight: true,
                blockTime: true,
                blockTimeNormalized: true
            })
                .toArray();
            // Check coinbase flag
            const ourCoinbase = ours.filter(tx => tx.coinbase);
            (0, chai_1.expect)(ourCoinbase.length, 'number of coinbases').to.equal(1);
            (0, chai_1.expect)(ourCoinbase[0].txid, 'coinbase txid to match truth').to.equal(coinbase.txid);
            // Check both sets of txs are the same size and contain no duplicates
            const txidset = new Set(ours.map(tx => tx.txid));
            (0, chai_1.expect)(ours.length, 'number of txs').to.equal(truth.tx.length);
            (0, chai_1.expect)(txidset.size, 'number of unique txs').to.equal(truth.tx.length);
            for (const our of ours) {
                // Check every one of our txs is contained in `truth`
                const tx = truth.tx.find(tx => tx.txid === our.txid);
                (0, chai_1.expect)(tx, 'tx to be in the block').to.not.be.undefined;
                // Check our txs' block hash matches the mongo block
                (0, chai_1.expect)(our.blockHash, 'tx block hash').to.equal(block.hash);
                (0, chai_1.expect)(our.blockHeight, 'tx block height').to.equal(block.height);
                const time = our.blockTime && our.blockTime.getTime();
                (0, chai_1.expect)(time, 'tx block time').to.equal(block.time.getTime());
                const ntime = our.blockTimeNormalized && our.blockTimeNormalized.getTime();
                (0, chai_1.expect)(ntime, 'tx block time normalized').to.equal(block.timeNormalized.getTime());
            }
            // Check no other tx points to our block hash
            const extra = await transaction_1.TransactionStorage.collection.countDocuments({
                chain: info.chain,
                network: info.network,
                blockHash: block.hash,
                txid: {
                    $nin: truth.tx.map(tx => tx.txid)
                }
            });
            (0, chai_1.expect)(extra, 'number of extra transactions').to.equal(0);
        }
    }
    // Check the heights are all unique
    (0, chai_1.expect)(heights.filter(h => !h).length, 'no duplicate heights').to.equal(0);
    // Check increasing times
    const increases = l => !!l.reduce((prev, curr) => (prev < curr ? curr : undefined));
    (0, chai_1.expect)(increases(normalizedTimes), 'normalized block times only increase').to.be.true;
}
async function transactions(info, creds) {
    const rpc = new rpc_1.AsyncRPC(creds.username, creds.password, creds.host, creds.port);
    const txcursor = transaction_1.TransactionStorage.collection.find({
        chain: info.chain,
        network: info.network
    });
    while (await txcursor.hasNext()) {
        const tx = await txcursor.next();
        if (!tx) {
            break;
        }
        logger_1.default.info(`verifying tx ${tx.txid}: ${tx.blockHeight}`);
        const truth = await rpc.transaction(tx.txid, tx.blockHash);
        if (info.network !== 'regtest') {
            (0, chai_1.expect)(tx.size, 'tx size').to.equal(truth.size);
        }
        (0, chai_1.expect)(tx.locktime, 'tx locktime').to.equal(truth.locktime);
        {
            // Minted by this transaction
            const ours = await coin_1.CoinStorage.collection
                .find({
                network: info.network,
                chain: info.chain,
                mintTxid: tx.txid
            })
                .toArray();
            (0, chai_1.expect)(ours.length, 'number mint txids').to.equal(truth.vout.length);
            for (const our of ours) {
                // coins
                (0, chai_1.expect)(our.mintHeight, 'tx mint height').to.equal(tx.blockHeight);
                (0, chai_1.expect)(our.value, 'tx mint value').to.equal(Math.round(truth.vout[our.mintIndex].value * SATOSHI));
                // TODO: why?
                if (our.address && our.address !== 'false') {
                    (0, chai_1.expect)(truth.vout[our.mintIndex].scriptPubKey.addresses, 'tx mint address').to.include(our.address);
                }
                (0, chai_1.expect)(our.coinbase).to.equal(tx.coinbase);
                // wallets
                (0, chai_1.expect)(tx.wallets).to.include.members(Array.from(our.wallets));
                if (our.wallets.length > 0) {
                    const wallets = await walletAddress_1.WalletAddressStorage.collection
                        .find({
                        wallet: {
                            $in: our.wallets
                        },
                        address: our.address,
                        chain: info.chain,
                        network: info.network
                    })
                        .toArray();
                    (0, chai_1.expect)(wallets.length, 'wallet exists').to.be.greaterThan(0);
                }
            }
        }
        {
            // Spent by this transaction
            const ours = await coin_1.CoinStorage.collection
                .find({
                network: info.network,
                chain: info.chain,
                spentTxid: tx.txid
            })
                .toArray();
            const nspent = truth.vin.length + (tx.coinbase ? -1 : 0);
            (0, chai_1.expect)(ours.length, 'number spent txids').to.equal(nspent);
            for (const our of ours) {
                (0, chai_1.expect)(our.spentHeight, 'tx spent height').to.equal(tx.blockHeight);
                (0, chai_1.expect)(tx.wallets).to.include.members(Array.from(our.wallets));
            }
        }
    }
}
if (require.main === module)
    (async () => {
        const info = {
            chain: process.env.CHAIN || 'BTC',
            network: process.env.NETWORK || 'testnet'
        };
        const creds = config_1.default.chains[info.chain][info.network].rpc;
        await storage_1.Storage.start({});
        logger_1.default.info('verifying blocks');
        await blocks(info, creds);
        logger_1.default.info('verifying transactions');
        await transactions(info, creds);
        process.exit();
    })().catch(err => {
        logger_1.default.error('%o', err);
        process.exit(1);
    });
//# sourceMappingURL=rpc-verify.js.map