bitcore-node
Version:
A blockchain indexing node with extended capabilities using bitcore
611 lines • 25.4 kB
JavaScript
;
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