"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;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.InternalStateProvider = void 0;
const stream_1 = require("stream");
const crypto_wallet_core_1 = require("@bitpay-labs/crypto-wallet-core");
const lru_cache_1 = require("lru-cache");
const Loggify_1 = require("../../../decorators/Loggify");
const block_1 = require("../../../models/block");
const cache_1 = require("../../../models/cache");
const coin_1 = require("../../../models/coin");
const state_1 = require("../../../models/state");
const transaction_1 = require("../../../models/transaction");
const wallet_1 = require("../../../models/wallet");
const walletAddress_1 = require("../../../models/walletAddress");
const rpc_1 = require("../../../rpc");
const config_1 = require("../../../services/config");
const storage_1 = require("../../../services/storage");
const utils_1 = require("../../../utils");
const jsonStream_1 = require("../../../utils/jsonStream");
const transforms_1 = require("./transforms");
let InternalStateProvider = class InternalStateProvider {
    constructor(chain, WalletStreamTransform = transforms_1.ListTransactionsStream) {
        this.WalletStreamTransform = WalletStreamTransform;
        this.chain = chain;
        this.chain = this.chain.toUpperCase();
        this.blockAtTimeCache = {};
    }
    getRPC(chain, network) {
        const RPC_PEER = config_1.Config.chainConfig({ chain, network }).rpc;
        if (!RPC_PEER) {
            throw new Error(`RPC not configured for ${chain} ${network}`);
        }
        const { username, password, host, port, protocol } = RPC_PEER;
        return new rpc_1.RPC(username, password, host, port, protocol);
    }
    getAddressQuery(params) {
        const { chain, network, address, args } = params;
        if (typeof address !== 'string' || !chain || !network) {
            throw new Error('Missing required param');
        }
        const query = { chain, network: network.toLowerCase(), address };
        if (args.unspent) {
            query.spentHeight = { $lt: 0 /* SpentHeightIndicators.minimum */ };
        }
        if (args.excludeConflicting) {
            query.mintHeight = { $gt: -3 /* SpentHeightIndicators.conflicting */ };
        }
        return query;
    }
    async streamAddressTransactions(params) {
        const { req, res, args } = params;
        const { limit, since } = args;
        const query = this.getAddressQuery(params);
        storage_1.Storage.apiStreamingFind(coin_1.CoinStorage, query, { limit, since, paging: '_id' }, req, res);
    }
    async getBalanceForAddress(params) {
        const { chain, network, address } = params;
        const query = {
            chain,
            network,
            address,
            spentHeight: { $lt: 0 /* SpentHeightIndicators.minimum */ },
            mintHeight: { $gt: -3 /* SpentHeightIndicators.conflicting */ }
        };
        const balance = await coin_1.CoinStorage.getBalance({ query });
        return balance;
    }
    streamBlocks(params) {
        const { req, res } = params;
        const { query, options } = this.getBlocksQuery(params);
        storage_1.Storage.apiStreamingFind(block_1.BitcoinBlockStorage, query, options, req, res);
    }
    async getBlocks(params) {
        const { query, options } = this.getBlocksQuery(params);
        let cursor = block_1.BitcoinBlockStorage.collection.find(query, options).addCursorFlag('noCursorTimeout', true);
        if (options.sort) {
            cursor = cursor.sort(options.sort);
        }
        const blocks = await cursor.toArray();
        const tip = await this.getLocalTip(params);
        const tipHeight = tip ? tip.height : 0;
        const blockTransform = (b) => {
            let confirmations = 0;
            if (b.height > -1) {
                confirmations = tipHeight - b.height + 1;
            }
            const convertedBlock = block_1.BitcoinBlockStorage._apiTransform(b, { object: true });
            return { ...convertedBlock, confirmations };
        };
        return blocks.map(blockTransform);
    }
    getBlocksQuery(params) {
        const { chain, network, sinceBlock, blockId, args = {} } = params;
        const { startDate, endDate, date, since, direction, paging } = args;
        const { limit = 10, sort = { height: -1 } } = args;
        const options = { limit, sort, since, direction, paging };
        if (!chain || !network) {
            throw new Error('Missing required param');
        }
        const query = {
            chain,
            network: network.toLowerCase(),
            processed: true
        };
        if (blockId) {
            if (blockId.length >= 64) {
                query.hash = blockId;
            }
            else {
                const height = parseInt(blockId, 10);
                if (Number.isNaN(height) || height.toString(10) !== blockId) {
                    throw new Error('invalid block id provided');
                }
                query.height = height;
            }
        }
        if (sinceBlock) {
            const height = Number(sinceBlock);
            if (Number.isNaN(height) || height.toString(10) !== sinceBlock) {
                throw new Error('invalid block id provided');
            }
            query.height = { $gt: height };
        }
        if (startDate) {
            query.time = { $gt: new Date(startDate) };
        }
        if (endDate) {
            query.time = Object.assign({}, query.time, { $lt: new Date(endDate) });
        }
        if (date) {
            const firstDate = new Date(date);
            const nextDate = new Date(date);
            nextDate.setDate(nextDate.getDate() + 1);
            query.time = { $gt: firstDate, $lt: nextDate };
        }
        return { query, options };
    }
    async getBlock(params) {
        const blocks = await this.getBlocks(params);
        return blocks[0];
    }
    async getBlockBeforeTime(params) {
        const { chain, network, time } = params;
        const date = new Date(time || Date.now());
        const chainNetwork = (0, utils_1.normalizeChainNetwork)(chain, network);
        if (!this.blockAtTimeCache[chainNetwork]) {
            this.blockAtTimeCache[chainNetwork] = new lru_cache_1.LRUCache({ max: 1000 });
        }
        const cachedBlock = this.blockAtTimeCache[chainNetwork].get(date.toISOString());
        if (cachedBlock !== undefined) {
            return cachedBlock;
        }
        const [block] = await block_1.BitcoinBlockStorage.collection
            .find({
            chain,
            network,
            timeNormalized: { $lte: date }
        })
            .limit(1)
            .sort({ timeNormalized: -1 })
            .toArray();
        this.blockAtTimeCache[chainNetwork].set(date.toISOString(), block || null);
        return block || null;
    }
    async streamTransactions(params) {
        const { chain, network, req, res, args } = params;
        const { blockHash, blockHeight } = args;
        if (!chain || !network) {
            throw new Error('Missing chain or network');
        }
        const query = {
            chain,
            network: network.toLowerCase()
        };
        if (blockHeight !== undefined) {
            query.blockHeight = Number(blockHeight);
        }
        if (blockHash !== undefined) {
            query.blockHash = blockHash;
        }
        const tip = await this.getLocalTip(params);
        const tipHeight = tip ? tip.height : 0;
        return storage_1.Storage.apiStreamingFind(transaction_1.TransactionStorage, query, args, req, res, t => {
            let confirmations = 0;
            if (t.blockHeight !== undefined && t.blockHeight >= 0) {
                confirmations = tipHeight - t.blockHeight + 1;
            }
            const convertedTx = transaction_1.TransactionStorage._apiTransform(t, { object: true });
            return JSON.stringify({ ...convertedTx, confirmations });
        });
    }
    async getTransaction(params) {
        let { network } = params;
        const { chain, txId } = params;
        if (typeof txId !== 'string' || !chain || !network) {
            throw new Error('Missing required param');
        }
        network = network.toLowerCase();
        const query = { chain, network, txid: txId };
        const tip = await this.getLocalTip(params);
        const tipHeight = tip ? tip.height : 0;
        const found = await transaction_1.TransactionStorage.collection.findOne(query);
        if (found) {
            let confirmations = 0;
            if (found.blockHeight != null && found.blockHeight >= 0) {
                confirmations = tipHeight - found.blockHeight + 1;
            }
            const convertedTx = transaction_1.TransactionStorage._apiTransform(found, { object: true });
            return { ...convertedTx, confirmations };
        }
        else {
            return undefined;
        }
    }
    async getAuthhead(params) {
        const { chain, network, txId } = params;
        if (typeof txId !== 'string') {
            throw new Error('Missing required param');
        }
        const found = (await coin_1.CoinStorage.resolveAuthhead(txId, chain, network))[0];
        if (found) {
            const transformedCoins = found.identityOutputs.map(output => coin_1.CoinStorage._apiTransform(output, { object: true }));
            return {
                chain: found.chain,
                network: found.network,
                authbase: found.authbase,
                identityOutputs: transformedCoins
            };
        }
        else {
            return undefined;
        }
    }
    async createWallet(params) {
        const { chain, network, name, pubKey, path, singleAddress } = params;
        if (typeof name !== 'string' || !network) {
            throw new Error('Missing required param');
        }
        const state = await state_1.StateStorage.collection.findOne({});
        const initialSyncComplete = state && state.initialSyncComplete && state.initialSyncComplete.includes(`${chain}:${network}`);
        const walletConfig = config_1.Config.for('api').wallets;
        const canCreate = walletConfig && walletConfig.allowCreationBeforeCompleteSync;
        const isP2P = this.isP2p({ chain, network });
        if (isP2P && !initialSyncComplete && !canCreate) {
            throw new Error('Wallet creation not permitted before intitial sync is complete');
        }
        const wallet = {
            chain,
            network,
            name,
            pubKey,
            path,
            singleAddress
        };
        await wallet_1.WalletStorage.collection.insertOne(wallet);
        return wallet;
    }
    async getWallet(params) {
        const { chain, pubKey } = params;
        return wallet_1.WalletStorage.collection.findOne({ chain, pubKey });
    }
    streamWalletAddresses(params) {
        const { chain, network, walletId, req, res } = params;
        const query = { chain, network, wallet: walletId };
        storage_1.Storage.apiStreamingFind(walletAddress_1.WalletAddressStorage, query, {}, req, res);
    }
    async walletCheck(params) {
        const { chain, network, wallet } = params;
        return new Promise(resolve => {
            const addressStream = walletAddress_1.WalletAddressStorage.collection.find({ chain, network, wallet }).project({ address: 1 });
            let sum = 0;
            let lastAddress;
            addressStream.on('data', (walletAddress) => {
                if (walletAddress.address) {
                    lastAddress = walletAddress.address;
                    const addressSum = Buffer.from(walletAddress.address).reduce((tot, cur) => (tot + cur) % Number.MAX_SAFE_INTEGER);
                    sum = (sum + addressSum) % Number.MAX_SAFE_INTEGER;
                }
            });
            addressStream.on('end', () => {
                resolve({ lastAddress, sum });
            });
        });
    }
    isP2p({ chain, network }) {
        return config_1.Config.chainConfig({ chain, network })?.chainSource !== 'p2p';
    }
    async streamMissingWalletAddresses(params) {
        const { chain, network, pubKey, res } = params;
        const wallet = await wallet_1.WalletStorage.collection.findOne({ pubKey });
        const walletId = wallet._id;
        const query = { chain, network, wallets: walletId, spentHeight: { $gte: 0 /* SpentHeightIndicators.minimum */ } };
        const cursor = coin_1.CoinStorage.collection.find(query).addCursorFlag('noCursorTimeout', true);
        const seen = {};
        const stringifyWallets = (wallets) => wallets.map(w => w.toHexString());
        const allMissingAddresses = new Array();
        let totalMissingValue = 0;
        const missingStream = cursor.pipe(new stream_1.Transform({
            objectMode: true,
            async transform(spentCoin, _, next) {
                if (!seen[spentCoin.spentTxid]) {
                    seen[spentCoin.spentTxid] = true;
                    // find coins that were spent with my coins
                    const spends = await coin_1.CoinStorage.collection
                        .find({ chain, network, spentTxid: spentCoin.spentTxid })
                        .addCursorFlag('noCursorTimeout', true)
                        .toArray();
                    const missing = spends
                        .filter(coin => !stringifyWallets(coin.wallets).includes(walletId.toHexString()))
                        .map(coin => {
                        const { _id, wallets, address, value } = coin;
                        totalMissingValue += value;
                        allMissingAddresses.push(address);
                        return { _id, wallets, address, value, expected: walletId.toHexString() };
                    });
                    if (missing.length > 0) {
                        return next(undefined, { txid: spentCoin.spentTxid, missing });
                    }
                }
                return next();
            },
            flush(done) {
                done(null, { allMissingAddresses, totalMissingValue });
            }
        }));
        missingStream.pipe(new jsonStream_1.StringifyJsonStream()).pipe(res);
    }
    async updateWallet(params) {
        const { wallet, addresses, reprocess = false } = params;
        await walletAddress_1.WalletAddressStorage.updateCoins({ wallet, addresses, opts: { reprocess } });
    }
    async streamWalletTransactions(params) {
        const { chain, network, wallet, res, args } = params;
        const query = {
            chain,
            network,
            wallets: wallet._id,
            'wallets.0': { $exists: true }
        };
        if (wallet.chain === 'BTC' && ['testnet3', 'testnet4'].includes(wallet.network)) {
            query['network'] = wallet.network;
        }
        if (args) {
            if (args.startBlock || args.endBlock) {
                query.$or = [];
                if (args.includeMempool) {
                    query.$or.push({ blockHeight: -1 /* SpentHeightIndicators.pending */ });
                }
                const blockRangeQuery = {};
                if (args.startBlock) {
                    blockRangeQuery.$gte = Number(args.startBlock);
                }
                if (args.endBlock) {
                    blockRangeQuery.$lte = Number(args.endBlock);
                }
                query.$or.push({ blockHeight: blockRangeQuery });
            }
            else {
                if (args.startDate) {
                    const startDate = new Date(args.startDate);
                    if (startDate.getTime()) {
                        query.blockTimeNormalized = { $gte: new Date(args.startDate) };
                    }
                }
                if (args.endDate) {
                    const endDate = new Date(args.endDate);
                    if (endDate.getTime()) {
                        query.blockTimeNormalized = query.blockTimeNormalized || {};
                        query.blockTimeNormalized.$lt = new Date(args.endDate);
                    }
                }
            }
        }
        const transactionStream = transaction_1.TransactionStorage.collection
            .find(query)
            .sort({ blockTimeNormalized: 1 })
            .addCursorFlag('noCursorTimeout', true);
        const listTransactionsStream = new this.WalletStreamTransform(wallet);
        transactionStream.pipe(listTransactionsStream).pipe(res);
    }
    async getWalletBalance(params) {
        const query = {
            wallets: params.wallet._id,
            'wallets.0': { $exists: true },
            spentHeight: { $lt: 0 /* SpentHeightIndicators.minimum */ },
            mintHeight: { $gt: -3 /* SpentHeightIndicators.conflicting */ }
        };
        if (params.wallet.chain === 'BTC' && ['testnet3', 'testnet4'].includes(params.wallet.network)) {
            query['network'] = params.wallet.network;
        }
        return coin_1.CoinStorage.getBalance({ query });
    }
    async getWalletBalanceAtTime(params) {
        const { chain, network, time } = params;
        const query = { wallets: params.wallet._id, 'wallets.0': { $exists: true } };
        if (params.wallet.chain === 'BTC' && ['testnet3', 'testnet4'].includes(params.wallet.network)) {
            query['network'] = params.wallet.network;
        }
        return coin_1.CoinStorage.getBalanceAtTime({ query, time, chain, network });
    }
    async streamWalletUtxos(params) {
        const { wallet, limit, args = {}, req, res } = params;
        const query = {
            wallets: wallet._id,
            'wallets.0': { $exists: true },
            mintHeight: { $gt: -3 /* SpentHeightIndicators.conflicting */ }
        };
        if (wallet.chain === 'BTC' && ['testnet3', 'testnet4'].includes(wallet.network)) {
            query['network'] = wallet.network;
        }
        if (args.includeSpent !== 'true') {
            if (args.includePending === 'true') {
                query.spentHeight = { $lte: -1 /* SpentHeightIndicators.pending */ };
            }
            else {
                query.spentHeight = { $lt: -1 /* SpentHeightIndicators.pending */ };
            }
        }
        const tip = await this.getLocalTip(params);
        const tipHeight = tip ? tip.height : 0;
        const utxoTransform = (c) => {
            let confirmations = 0;
            if (c.mintHeight && c.mintHeight >= 0) {
                confirmations = tipHeight - c.mintHeight + 1;
            }
            c.confirmations = confirmations;
            return coin_1.CoinStorage._apiTransform(c);
        };
        storage_1.Storage.apiStreamingFind(coin_1.CoinStorage, query, { limit }, req, res, utxoTransform);
    }
    async getFee(params) {
        const { chain, network, target, mode } = params;
        const cacheKey = `getFee-${chain}-${network}-${target}${mode ? '-' + mode.toLowerCase() : ''}`;
        return cache_1.CacheStorage.getGlobalOrRefresh(cacheKey, async () => {
            return this.getRPC(chain, network).getEstimateSmartFee(Number(target), mode);
        }, 5 * cache_1.CacheStorage.Times.Minute);
    }
    async broadcastTransaction(params) {
        const { chain, network, rawTx } = params;
        const txids = new Array();
        const rawTxs = typeof rawTx === 'string' ? [rawTx] : rawTx;
        for (const tx of rawTxs) {
            const txid = await this.getRPC(chain, network).sendTransaction(tx);
            txids.push(txid);
        }
        return txids.length === 1 ? txids[0] : txids;
    }
    async getCoinsForTx({ chain, network, txid }) {
        const tx = await transaction_1.TransactionStorage.collection.findOne({ txid });
        if (!tx) {
            throw new Error(`No such transaction ${txid}`);
        }
        const tip = await this.getLocalTip({ chain, network });
        const confirmations = (tip && tx.blockHeight > -1) ? tip.height - tx.blockHeight + 1 : 0;
        const inputs = await coin_1.CoinStorage.collection
            .find({
            chain,
            network,
            spentTxid: txid
        })
            .addCursorFlag('noCursorTimeout', true)
            .toArray();
        const outputs = await coin_1.CoinStorage.collection
            .find({
            chain,
            network,
            mintTxid: txid
        })
            .addCursorFlag('noCursorTimeout', true)
            .toArray();
        return {
            inputs: inputs.map(input => coin_1.CoinStorage._apiTransform(input, { object: true, confirmations })),
            outputs: outputs.map(output => coin_1.CoinStorage._apiTransform(output, { object: true, confirmations }))
        };
    }
    async getDailyTransactions(params) {
        const { chain, network, startDate, endDate } = params;
        const formatDate = (d) => new Date(d.toISOString().split('T')[0]);
        const todayTruncatedUTC = formatDate(new Date());
        let oneMonth = new Date(todayTruncatedUTC);
        oneMonth.setDate(todayTruncatedUTC.getDate() - 30);
        oneMonth = formatDate(oneMonth);
        const isValidDate = (d) => {
            return new Date(d).toString() !== 'Invalid Date';
        };
        const start = startDate && isValidDate(startDate) ? new Date(startDate) : oneMonth;
        const end = endDate && isValidDate(endDate) ? formatDate(new Date(endDate)) : todayTruncatedUTC;
        const results = await block_1.BitcoinBlockStorage.collection
            .aggregate([
            {
                $match: {
                    chain,
                    network,
                    timeNormalized: {
                        $gte: start,
                        $lt: end
                    }
                }
            },
            {
                $group: {
                    _id: {
                        $dateToString: {
                            format: '%Y-%m-%d',
                            date: '$timeNormalized'
                        }
                    },
                    transactionCount: {
                        $sum: '$transactionCount'
                    }
                }
            },
            {
                $project: {
                    _id: 0,
                    date: '$_id',
                    transactionCount: '$transactionCount'
                }
            },
            {
                $sort: {
                    date: 1
                }
            }
        ])
            .toArray();
        return {
            chain,
            network,
            results
        };
    }
    async getLocalTip({ chain, network }) {
        return block_1.BitcoinBlockStorage.getLocalTip({ chain, network });
    }
    /**
     * Get a series of hashes that come before a given height, or the 30 most recent hashes
     *
     * @returns {Promise<Array<string>>}
     */
    async getLocatorHashes(params) {
        const { chain, network, startHeight, endHeight } = params;
        const query = startHeight && endHeight
            ? {
                processed: true,
                chain,
                network,
                height: { $gt: startHeight, $lt: endHeight }
            }
            : {
                processed: true,
                chain,
                network
            };
        const locatorBlocks = await block_1.BitcoinBlockStorage.collection
            .find(query).sort({ height: -1 }).limit(30)
            .addCursorFlag('noCursorTimeout', true)
            .toArray();
        if (locatorBlocks.length < 2) {
            return [Array(65).join('0')];
        }
        return locatorBlocks.map(block => block.hash);
    }
    isValid(params) {
        const { input } = params;
        if (this.isValidBlockOrTx(input)) {
            return { isValid: true, type: 'blockOrTx' };
        }
        else if (this.isValidAddress(params)) {
            return { isValid: true, type: 'addr' };
        }
        else if (this.isValidBlockIndex(input)) {
            return { isValid: true, type: 'blockOrTx' };
        }
        else {
            return { isValid: false, type: 'invalid' };
        }
    }
    isValidBlockOrTx(inputValue) {
        const regexp = /^[0-9a-fA-F]{64}$/;
        if (regexp.test(inputValue)) {
            return true;
        }
        else {
            return false;
        }
    }
    isValidAddress(params) {
        const { chain, network, input } = params;
        const addr = this.extractAddress(input);
        return !!crypto_wallet_core_1.Validation.validateAddress(chain, network, addr);
    }
    isValidBlockIndex(inputValue) {
        return isFinite(inputValue);
    }
    extractAddress(address) {
        const extractedAddress = address.replace(/^(bitcoincash:|bchtest:|bitcoin:)/i, '').replace(/\?.*/, '');
        return extractedAddress || address;
    }
    async getWalletAddresses(walletId) {
        const query = { chain: this.chain, wallet: walletId };
        return walletAddress_1.WalletAddressStorage.collection
            .find(query)
            .addCursorFlag('noCursorTimeout', true)
            .toArray();
    }
    async getBlockFee(params) {
        const { chain, network, blockId } = params;
        const transactions = blockId.length >= 64
            ? await transaction_1.TransactionStorage.collection.find({ chain, network, blockHash: blockId }).toArray()
            : await transaction_1.TransactionStorage.collection.find({ chain, network, blockHeight: parseInt(blockId, 10) }).toArray();
        if (transactions.length <= 1)
            return { feeTotal: 0, mean: 0, median: 0, mode: 0 };
        let feeRateSum = 0;
        let feeTotal = 0;
        const feeRates = [];
        const freq = {};
        let mode = 0, maxCount = 0;
        for (const tx of transactions) {
            if (tx.coinbase)
                continue; // skip coinbase transaction
            const rate = tx.fee && tx.size ? tx.fee / tx.size : 0; // does not add fee rate 0 or divide by zero
            feeRates.push(rate);
            feeRateSum += rate;
            feeTotal += tx.fee || 0;
            freq[rate] = (freq[rate] || 0) + 1;
            if (freq[rate] > maxCount) {
                mode = rate;
                maxCount = freq[rate];
            }
        }
        const mean = feeRateSum / feeRates.length;
        feeRates.sort((a, b) => a - b);
        const median = feeRates.length % 2 === 1
            ? feeRates[Math.floor(feeRates.length / 2)]
            : (feeRates[feeRates.length / 2 - 1] + feeRates[feeRates.length / 2]) / 2;
        return { feeTotal, mean, median, mode };
    }
};
exports.InternalStateProvider = InternalStateProvider;
exports.InternalStateProvider = InternalStateProvider = __decorate([
    Loggify_1.LoggifyClass
], InternalStateProvider);
//# sourceMappingURL=internal.js.map