bitcore-node
Version:
A blockchain indexing node with extended capabilities using bitcore
273 lines (257 loc) • 8.71 kB
text/typescript
import { ObjectID } from 'bson';
import { LoggifyClass } from '../../../decorators/Loggify';
import logger from '../../../logger';
import { MongoBound } from '../../../models/base';
import { BaseTransaction } from '../../../models/baseTransaction';
import { CacheStorage } from '../../../models/cache';
import { CoinStorage } from '../../../models/coin';
import { EventStorage } from '../../../models/events';
import { Config } from '../../../services/config';
import { Storage, StorageService } from '../../../services/storage';
import { SpentHeightIndicators } from '../../../types/Coin';
import { StreamingFindOptions } from '../../../types/Query';
import { TransformOptions } from '../../../types/TransformOptions';
import { partition, valueOrDefault } from '../../../utils';
import { IXrpCoin, IXrpTransaction, XrpTransactionJSON } from '../types';
export class XrpTransactionModel extends BaseTransaction<IXrpTransaction> {
constructor(storage: StorageService = Storage) {
super(storage);
}
onConnect() {
super.onConnect();
this.collection.createIndex({ chain: 1, network: 1, from: 1, nonce: 1 }, { background: true, sparse: true });
}
async batchImport(params: {
txs: Array<IXrpTransaction>;
coins: Array<IXrpCoin>;
height?: number;
mempoolTime?: Date;
blockTime?: Date;
blockHash?: string;
blockTimeNormalized?: Date;
parentChain?: string;
forkHeight?: number;
chain: string;
network: string;
initialSyncComplete: boolean;
}) {
const txOps = await this.addTransactions({ ...params });
const coinOps = (await this.addCoins({ ...params })) as Array<any>;
const batchSize = Config.get().maxPoolSize;
logger.debug('Writing Transactions: %o', txOps.length);
await Promise.all(
partition(txOps, txOps.length / batchSize).map(txBatch =>
this.collection.bulkWrite(
txBatch.map(op => this.toMempoolSafeUpsert(op, SpentHeightIndicators.minimum)),
{
ordered: false
}
)
)
);
await Promise.all(
partition(coinOps, coinOps.length / batchSize).map(coinBatch =>
CoinStorage.collection.bulkWrite(
coinBatch.map(op => this.toMempoolSafeUpsert(op, SpentHeightIndicators.minimum)),
{ ordered: false }
)
)
);
if (params.initialSyncComplete) {
await this.expireBalanceCache(coinOps);
}
// Create events for mempool txs
if (params.height != undefined && params.height < SpentHeightIndicators.minimum) {
for (let op of txOps) {
const filter = op.updateOne.filter;
const tx = { ...op.updateOne.update.$set, ...filter } as IXrpTransaction;
await EventStorage.signalTx(tx);
}
for (const coinOp of coinOps) {
const coin = { ...coinOp.updateOne.filter, ...coinOp.updateOne.update.$set } as IXrpCoin;
await EventStorage.signalAddressCoin({
address: coin.address,
coin: {
value: coin.value,
address: coin.address,
chain: params.chain,
network: params.network,
mintTxid: coin.mintTxid
}
});
}
}
}
async expireBalanceCache(coinOps: Array<any>) {
let batch = new Array<{ address: string; chain: string; network: string }>();
for (const coinOp of coinOps) {
const coin = { ...coinOp.updateOne.filter, ...coinOp.updateOne.update.$set } as IXrpCoin;
const { address, chain, network } = coin;
batch.push({ address, chain, network });
}
for (const payload of batch) {
const { address, chain, network } = payload;
const lowerAddress = address.toLowerCase();
const cacheKey = `getBalanceForAddress-${chain}-${network}-${lowerAddress}`;
await CacheStorage.expire(cacheKey);
}
}
async addTransactions(params: {
txs: Array<IXrpTransaction>;
height?: number;
blockTime?: Date;
blockHash?: string;
blockTimeNormalized?: Date;
parentChain?: string;
forkHeight?: number;
initialSyncComplete: boolean;
chain: string;
network: string;
mempoolTime?: Date;
}) {
let { blockTimeNormalized, chain, height, network, parentChain, forkHeight } = params;
if (parentChain && forkHeight && height != undefined && height < forkHeight) {
const parentTxs = await XrpTransactionStorage.collection
.find({ blockHeight: height, chain: parentChain, network })
.toArray();
return parentTxs.map(parentTx => {
return {
updateOne: {
filter: { txid: parentTx.txid, chain, network },
update: {
$set: {
...parentTx,
wallets: new Array<ObjectID>()
}
},
upsert: true,
forceServerObjectId: true
}
};
});
} else {
return Promise.all(
params.txs.map(async (tx: IXrpTransaction) => {
const { txid, wallets } = tx;
return {
updateOne: {
filter: { txid, chain, network },
update: {
$set: {
...tx,
blockTimeNormalized: tx.blockTimeNormalized || blockTimeNormalized,
wallets
}
},
upsert: true,
forceServerObjectId: true
}
};
})
);
}
}
async addCoins(params: {
coins: Array<IXrpCoin>;
height?: number;
blockTime?: Date;
blockHash?: string;
blockTimeNormalized?: Date;
parentChain?: string;
forkHeight?: number;
initialSyncComplete: boolean;
chain: string;
network: string;
mempoolTime?: Date;
}) {
let { chain, height, network, parentChain, forkHeight } = params;
if (parentChain && forkHeight && height != undefined && height < forkHeight) {
const parentChainCoins = await CoinStorage.collection
.find({ blockHeight: height, chain: parentChain, network })
.toArray();
return parentChainCoins.map((coin: IXrpCoin) => {
const { mintTxid } = coin;
return {
updateOne: {
filter: {
mintTxid,
mintIndex: coin.mintIndex,
chain,
network
},
update: {
$set: {
...coin,
wallets: []
}
},
upsert: true,
forceServerObjectId: true
}
};
});
} else {
return params.coins.map((coin: IXrpCoin) => {
const { mintTxid, wallets, address } = coin;
return {
updateOne: {
filter: {
mintTxid,
mintIndex: coin.mintIndex,
chain,
network
},
update: {
$set: {
chain,
network,
address,
mintHeight: coin.mintHeight || height,
coinbase: coin.coinbase,
value: coin.value
},
$setOnInsert: {
wallets
}
},
upsert: true,
forceServerObjectId: true
}
};
});
}
}
getTransactions(params: { query: any; options: StreamingFindOptions<IXrpTransaction> }) {
let originalQuery = params.query;
const { query, options } = Storage.getFindOptions(this, params.options);
const finalQuery = Object.assign({}, originalQuery, query);
return this.collection.find(finalQuery, options).addCursorFlag('noCursorTimeout', true);
}
_apiTransform(
tx: IXrpTransaction | Partial<MongoBound<IXrpTransaction>>,
options?: TransformOptions
): XrpTransactionJSON | string {
const transaction: XrpTransactionJSON = {
txid: tx.txid || '',
network: tx.network || '',
chain: tx.chain || '',
blockHeight: valueOrDefault(tx.blockHeight, -1),
blockHash: tx.blockHash || '',
blockTime: tx.blockTime ? tx.blockTime.toISOString() : '',
blockTimeNormalized: tx.blockTimeNormalized ? tx.blockTimeNormalized.toISOString() : '',
fee: valueOrDefault(tx.fee, -1),
value: valueOrDefault(tx.value, -1),
from: tx.from || '',
nonce: valueOrDefault(tx.nonce, -1),
to: valueOrDefault(tx.to, ''),
currency: valueOrDefault(tx.currency, 'XRP'),
invoiceID: valueOrDefault(tx.invoiceID, '')
};
if (options && options.object) {
return transaction;
}
return JSON.stringify(transaction);
}
}
export let XrpTransactionStorage = new XrpTransactionModel();