UNPKG

bitcore-node

Version:

A blockchain indexing node with extended capabilities using bitcore

235 lines (224 loc) 7.62 kB
import { CollectionAggregationOptions, ObjectID } from 'mongodb'; import { LoggifyClass } from '../decorators/Loggify'; import logger from '../logger'; import { Libs } from '../providers/libs'; import { StorageService } from '../services/storage'; import { CoinJSON, SpentHeightIndicators } from '../types/Coin'; import { valueOrDefault } from '../utils'; import { BaseModel, MongoBound } from './base'; import { BitcoinBlockStorage } from './block'; export interface ICoin { network: string; chain: string; mintTxid: string; mintIndex: number; mintHeight: number; coinbase: boolean; value: number; address: string; script: Buffer; wallets: Array<ObjectID>; spentTxid: string; spentHeight: number; confirmations?: number; sequenceNumber?: number; } @LoggifyClass export class CoinModel extends BaseModel<ICoin> { constructor(storage?: StorageService) { super('coins', storage); } allowedPaging = [ { key: 'mintHeight' as 'mintHeight', type: 'number' as 'number' }, { key: 'spentHeight' as 'spentHeight', type: 'number' as 'number' } ]; onConnect() { this.collection.createIndex({ mintTxid: 1, mintIndex: 1 }, { background: true }); this.collection.createIndex( { address: 1, chain: 1, network: 1 }, { background: true, partialFilterExpression: { spentHeight: { $lt: 0 } } } ); this.collection.createIndex({ address: 1 }, { background: true }); this.collection.createIndex({ chain: 1, network: 1, mintHeight: 1 }, { background: true }); this.collection.createIndex({ spentTxid: 1 }, { background: true, sparse: true }); this.collection.createIndex({ chain: 1, network: 1, spentHeight: 1 }, { background: true }); this.collection.createIndex( { wallets: 1, spentHeight: 1, value: 1, mintHeight: 1 }, { background: true, partialFilterExpression: { 'wallets.0': { $exists: true } } } ); this.collection.createIndex( { wallets: 1, spentTxid: 1 }, { background: true, partialFilterExpression: { 'wallets.0': { $exists: true } } } ); this.collection.createIndex( { wallets: 1, mintTxid: 1 }, { background: true, partialFilterExpression: { 'wallets.0': { $exists: true } } } ); } async getBalance(params: { query: any }, options: CollectionAggregationOptions = {}) { let { query } = params; const result = await this.collection .aggregate<{ _id: string; balance: number }>( [ { $match: query }, { $project: { value: 1, status: { $cond: { if: { $gte: ['$mintHeight', SpentHeightIndicators.minimum] }, then: 'confirmed', else: 'unconfirmed' } }, _id: 0 } }, { $group: { _id: '$status', balance: { $sum: '$value' } } } ], options ) .toArray(); return result.reduce<{ confirmed: number; unconfirmed: number; balance: number }>( (acc, cur) => { acc[cur._id] = cur.balance; acc.balance += cur.balance; return acc; }, { confirmed: 0, unconfirmed: 0, balance: 0 } ); } async getBalanceAtTime(params: { query: any; time: string; chain: string; network: string }) { let { query, time, chain, network } = params; const [block] = await BitcoinBlockStorage.collection .find({ $query: { chain, network, timeNormalized: { $lte: new Date(time) } } }) .limit(1) .sort({ timeNormalized: -1 }) .toArray(); const blockHeight = block!.height; const combinedQuery = Object.assign( {}, { $or: [{ spentHeight: { $gt: blockHeight } }, { spentHeight: { $lt: SpentHeightIndicators.minimum } }], mintHeight: { $lte: blockHeight } }, query ); return this.getBalance({ query: combinedQuery }, { hint: { wallets: 1, spentHeight: 1, value: 1, mintHeight: 1 } }); } resolveAuthhead(mintTxid: string, chain?: string, network?: string) { return this.collection .aggregate<{ chain: string; network: string; authbase: string; identityOutputs: ICoin[]; }>([ { $match: { mintTxid: mintTxid.toLowerCase(), mintIndex: 0, ...(typeof chain === 'string' ? { chain } : {}), ...(typeof network === 'string' ? { network } : {}) } }, { $graphLookup: { from: 'coins', startWith: '$spentTxid', connectFromField: 'spentTxid', connectToField: 'mintTxid', as: 'authheads', maxDepth: 1000000, restrictSearchWithMatch: { mintIndex: 0 } } }, { $project: { chain: '$chain', network: '$network', authbase: '$mintTxid', identityOutputs: { $filter: { input: '$authheads', as: 'authhead', cond: { $and: [ { $lte: ['$$authhead.spentHeight', -1] }, { $eq: ['$$authhead.chain', '$chain'] }, { $eq: ['$$authhead.network', '$network'] } ] } } } } } ]) .toArray(); } _apiTransform(coin: Partial<MongoBound<ICoin>>, options?: { object: boolean; confirmations?: number; }): any { // try to parse coin.address if its 'false' and script exists if (coin.address == 'false' && coin.script != undefined && coin.script.toString() != '') { try { const lib = Libs.get(coin.chain).lib; const address = lib .Script(coin.script.toString('hex')) .toAddress(coin.network) .toString(); if (lib.Address.isValid(address, coin.network)) { coin.address = address; // update coin record in db - do it asynchronously as we don't need to wait for result CoinStorage.collection.updateOne({ _id: coin._id }, { $set: { address: coin.address } }); } } catch (e) { logger.debug( `Could not parse address on "${coin.chain}:${coin.network}" for coin ${coin.mintTxid}[${coin.mintIndex}]` ); } } const transform: CoinJSON = { chain: valueOrDefault(coin.chain, ''), network: valueOrDefault(coin.network, ''), coinbase: valueOrDefault(coin.coinbase, false), mintIndex: valueOrDefault(coin.mintIndex, -1), spentTxid: valueOrDefault(coin.spentTxid, ''), mintTxid: valueOrDefault(coin.mintTxid, ''), mintHeight: valueOrDefault(coin.mintHeight, -1), spentHeight: valueOrDefault(coin.spentHeight, SpentHeightIndicators.error), address: valueOrDefault(coin.address, ''), script: valueOrDefault(coin.script, Buffer.alloc(0)).toString('hex'), value: valueOrDefault(coin.value, -1), confirmations: valueOrDefault(coin.confirmations ?? options?.confirmations, -1), sequenceNumber: valueOrDefault(coin.sequenceNumber, undefined) }; if (options && options.object) { return transform; } return JSON.stringify(transform); } } export let CoinStorage = new CoinModel();