UNPKG

bitcore-node

Version:

A blockchain indexing node with extended capabilities using bitcore

611 lines 25.4 kB
"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 crypto_wallet_core_1 = require("crypto-wallet-core"); const stream_1 = require("stream"); 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 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(); } 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 } = RPC_PEER; return new rpc_1.RPC(username, password, host, port); } 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; } streamAddressUtxos(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 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 */ } }; let 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); } let 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; let { startDate, endDate, date, since, direction, paging } = args; let { limit = 10, sort = { height: -1 } } = args; let options = { limit, sort, since, direction, paging }; if (!chain || !network) { throw new Error('Missing required param'); } let query = { chain, network: network.toLowerCase(), processed: true }; if (blockId) { if (blockId.length >= 64) { query.hash = blockId; } else { let height = parseInt(blockId, 10); if (Number.isNaN(height) || height.toString(10) !== blockId) { throw new Error('invalid block id provided'); } query.height = height; } } if (sinceBlock) { let 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) { let firstDate = new Date(date); let 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 [block] = await block_1.BitcoinBlockStorage.collection .find({ chain, network, timeNormalized: { $lte: date } }) .limit(1) .sort({ timeNormalized: -1 }) .toArray(); return block; } async streamTransactions(params) { const { chain, network, req, res, args } = params; let { blockHash, blockHeight } = args; if (!chain || !network) { throw new Error('Missing chain or network'); } let 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 { chain, network, txId } = params; if (typeof txId !== 'string' || !chain || !network) { throw new Error('Missing required param'); } network = network.toLowerCase(); let 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) { let { 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) { let { walletId, req, res } = params; let query = { wallet: walletId }; storage_1.Storage.apiStreamingFind(walletAddress_1.WalletAddressStorage, query, {}, req, res); } async walletCheck(params) { let { 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 */ }); } let 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; let 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; let 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) { let query = { chain: this.chain, wallet: walletId }; return walletAddress_1.WalletAddressStorage.collection .find(query) .addCursorFlag('noCursorTimeout', true) .toArray(); } }; exports.InternalStateProvider = InternalStateProvider; exports.InternalStateProvider = InternalStateProvider = __decorate([ Loggify_1.LoggifyClass ], InternalStateProvider); //# sourceMappingURL=internal.js.map