bitcore-node
Version:
A blockchain indexing node with extended capabilities using bitcore
319 lines • 14.9 kB
JavaScript
"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;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.XrpP2pWorker = void 0;
const events_1 = require("events");
const stream_1 = require("stream");
const Loggify_1 = require("../../../decorators/Loggify");
const logger_1 = __importDefault(require("../../../logger"));
const logger_2 = require("../../../logger");
const cache_1 = require("../../../models/cache");
const state_1 = require("../../../models/state");
const walletAddress_1 = require("../../../models/walletAddress");
const p2p_1 = require("../../../services/p2p");
const utils_1 = require("../../../utils");
const csp_1 = require("../api/csp");
const block_1 = require("../models/block");
const transaction_1 = require("../models/transaction");
let XrpP2pWorker = class XrpP2pWorker extends p2p_1.BaseP2PWorker {
constructor({ chain, network, chainConfig, blockModel = block_1.XrpBlockStorage }) {
super({ chain, network, chainConfig, blockModel });
this.blockModel = blockModel;
this.chain = chain || 'XRP';
this.network = network;
this.chainConfig = chainConfig;
this.syncing = false;
this.initialSyncComplete = false;
this.provider = new csp_1.RippleStateProvider(this.chain);
this.events = new events_1.EventEmitter();
this.invCache = {};
this.invCacheLimits = {
TX: 100000
};
this.disconnecting = false;
}
cacheInv(type, hash) {
if (!this.invCache[type]) {
this.invCache[type] = [];
}
if (this.invCache[type].length > this.invCacheLimits[type]) {
this.invCache[type].shift();
}
this.invCache[type].push(hash);
}
isCachedInv(type, hash) {
if (!this.invCache[type]) {
this.invCache[type] = [];
}
return this.invCache[type].includes(hash);
}
async setupListeners() {
const { host, port } = this.chainConfig.provider;
this.events.on('disconnected', async () => {
logger_1.default.warn(`${(0, logger_2.timestamp)()} | Not connected to peer: ${host}:${port || ''} | Chain: ${this.chain} | Network: ${this.network}`);
});
this.events.on('connected', async () => {
this.client.rpc.on('ledgerClosed', async () => {
this.sync();
});
this.client.asyncRequest('subscribe', { streams: ['ledger'] });
});
}
async teardownListeners() {
this.client.rpc.removeAllListeners('ledgerClosed');
await this.client.asyncRequest('unsubscribe', { streams: ['ledger'] });
}
async disconnect() {
this.disconnecting = true;
await this.teardownListeners();
}
async handleReconnects() {
this.disconnecting = false;
let firstConnect = true;
let connected = false;
let disconnected = false;
const { host, port } = this.chainConfig.provider;
while (!this.disconnecting && !this.stopping) {
try {
try {
this.client = await this.provider.getClient(this.network);
connected = this.client.rpc.isConnected();
}
catch (e) {
connected = false;
}
if (connected) {
if (disconnected || firstConnect) {
this.events.emit('connected');
}
}
else {
this.client = await this.provider.getClient(this.network);
this.events.emit('disconnected');
}
if (disconnected && connected && !firstConnect) {
logger_1.default.warn(`${(0, logger_2.timestamp)()} | Reconnected to peer: ${host}:${port || ''} | Chain: ${this.chain} | Network: ${this.network}`);
}
if (connected && firstConnect) {
firstConnect = false;
logger_1.default.info(`${(0, logger_2.timestamp)()} | Connected to peer: ${host}:${port || ''} | Chain: ${this.chain} | Network: ${this.network}`);
}
disconnected = !connected;
}
catch (e) { }
await (0, utils_1.wait)(5000);
}
}
async connect() {
this.handleReconnects();
}
async getBlock(height) {
return this.provider.getBlock({ chain: this.chain, network: this.network, blockId: height.toString() });
}
async syncWallets() {
return new Promise(async (resolve) => {
try {
const { chain, network } = this;
// After wallet syncs, start block sync from the current height
const client = await this.provider.getClient(this.network);
let { ledger: chainBestBlock } = await client.getBlock({ index: 'latest' });
this.chainConfig.startHeight = chainBestBlock;
const count = await walletAddress_1.WalletAddressStorage.collection.countDocuments({ chain, network });
let done = 0;
logger_1.default.info(`Syncing ${count} ${chain} ${network} wallets`);
let lastLog = Date.now();
const addressStream = walletAddress_1.WalletAddressStorage.collection.find({ chain, network }).stream();
addressStream
.pipe(new stream_1.Transform({
objectMode: true,
transform: async (data, _, cb) => {
if (Date.now() - 5000 > lastLog) {
logger_1.default.info(`Syncing ${count - done} ${chain} ${network} wallets`);
}
const walletAddress = data;
const [lastTx] = await transaction_1.XrpTransactionStorage.collection
.find({ wallets: walletAddress.wallet, 'wallets.0': { $exists: true } })
.sort({ blockTimeNormalized: -1 })
.limit(1)
.toArray();
const synced = await cache_1.CacheStorage.getForWallet(walletAddress.wallet, `sync-${walletAddress.address}`);
if (synced) {
// if this is happening, it means initial sync wasn't completed the first time, likely due to a crash
return cb();
}
const txs = await this.provider.getAddressTransactions({
chain: this.chain,
network: this.network,
address: walletAddress.address,
args: {
...(lastTx && !this.chainConfig.freshSync && { startTx: lastTx.txid })
}
});
if (txs.length) {
logger_1.default.info(`Saving ${txs.length} transactions`);
}
const blockTxs = new Array();
const blockCoins = new Array();
for (const tx of txs) {
const bitcoreTx = this.provider.transformAccountTx(tx, network);
const bitcoreCoins = this.provider.transformToCoins(tx, network);
const { transaction, coins } = await this.provider.tag(chain, network, bitcoreTx, bitcoreCoins);
blockTxs.push(transaction);
blockCoins.push(...coins);
}
await transaction_1.XrpTransactionStorage.batchImport({
txs: blockTxs,
coins: blockCoins,
chain,
network,
initialSyncComplete: false
});
await cache_1.CacheStorage.setForWallet(walletAddress.wallet, `sync-${walletAddress.address}`, true, cache_1.CacheStorage.Times.Hour / 2);
done++;
cb();
}
}))
.on('finish', async () => {
logger_1.default.info(`FINISHED Syncing ${count} ${chain} ${network} wallets`);
this.initialSyncComplete = true;
await state_1.StateStorage.collection.findOneAndUpdate({}, { $addToSet: { initialSyncComplete: `${chain}:${network}` } }, { upsert: true });
resolve();
});
}
catch (e) {
logger_1.default.error('%o', e);
}
})
.finally(() => {
if (this.disconnecting) {
this.client.rpc.disconnect();
}
});
}
async syncBlocks() {
const { chain, network } = this;
const client = await this.provider.getClient(this.network);
let ourBestBlock = await this.provider.getLocalTip({ chain, network });
const serverInfo = await client.getServerInfo();
const [earliestLedgerIdx, latestLedgerIdx] = serverInfo.complete_ledgers.split('-');
let chainBestBlock = Number(latestLedgerIdx);
const startTime = Date.now();
let lastLog = Date.now();
const configuredStart = Math.max(this.chainConfig.startHeight || 0, ourBestBlock?.height || 0, earliestLedgerIdx || 0);
logger_1.default.info(`Starting XRP Sync @ ${configuredStart}`);
ourBestBlock = { height: configuredStart };
const startHeight = ourBestBlock.height;
let currentHeight = startHeight;
while (ourBestBlock.height < chainBestBlock && !this.disconnecting) {
currentHeight = ourBestBlock.height + 1;
const { ledger: block } = await client.getBlock({ index: currentHeight });
if (!block) {
await (0, utils_1.wait)(2000);
continue;
}
const transformedBlock = this.provider.transformLedger(block, network);
const coinsAndTxs = (block.transactions || [])
.map((tx) => ({
tx: this.provider.transform(tx, network, transformedBlock),
coins: this.provider.transformToCoins(tx, network, transformedBlock)
}))
.filter(data => {
return !!data.tx.txid;
});
const blockTxs = new Array();
const blockCoins = new Array();
for (const coinAndTx of coinsAndTxs) {
const { transaction, coins } = await this.provider.tag(chain, network, coinAndTx.tx, coinAndTx.coins);
if (this.chainConfig.walletOnlySync && !transaction.wallets.length) {
continue;
}
blockTxs.push(transaction);
blockCoins.push(...coins);
}
await this.blockModel.processBlock({
chain,
network,
block: transformedBlock,
transactions: blockTxs,
coins: blockCoins,
initialSyncComplete: true
});
this.maybeLog(chain, network, startHeight, currentHeight, startTime, lastLog);
lastLog = Date.now();
ourBestBlock = await this.provider.getLocalTip({ chain, network });
if (ourBestBlock.height === chainBestBlock) {
({ ledger: chainBestBlock } = await client.getBlock({ index: 'latest' }));
}
}
if (this.disconnecting) {
this.client.rpc.disconnect();
}
}
async sync() {
if (this.syncing) {
return false;
}
const { chain, network } = this;
this.syncing = true;
const state = await state_1.StateStorage.collection.findOne({});
this.initialSyncComplete =
state && state.initialSyncComplete && state.initialSyncComplete.includes(`${chain}:${network}`);
try {
if (this.chainConfig.walletOnlySync && !this.initialSyncComplete) {
await this.syncWallets();
}
else {
await this.syncBlocks();
}
logger_1.default.info(`${chain}:${network} up to date.`);
this.syncing = false;
state_1.StateStorage.collection.findOneAndUpdate({}, { $addToSet: { initialSyncComplete: `${chain}:${network}` } }, { upsert: true });
this.events.emit('SYNCDONE');
return true;
}
catch (e) {
logger_1.default.error('%o', e);
this.syncing = false;
await (0, utils_1.wait)(2000);
return this.sync();
}
}
maybeLog(chain, network, startHeight, currentHeight, startTime, lastLog) {
const oneSecond = 1000;
const now = Date.now();
if (now - lastLog > oneSecond || startHeight === currentHeight) {
const blocksProcessed = currentHeight - startHeight;
const elapsedMinutes = (now - startTime) / (60 * oneSecond);
logger_1.default.info(`${(0, logger_2.timestamp)()} | Syncing... | Chain: ${chain} | Network: ${network} |${(blocksProcessed / elapsedMinutes)
.toFixed(2)
.padStart(8)} blocks/min | Height: ${currentHeight.toString().padStart(7)}`);
}
}
async syncDone() {
return new Promise(resolve => this.events.once('SYNCDONE', resolve));
}
async stop() {
this.stopping = true;
logger_1.default.debug(`Stopping worker for chain ${this.chain} ${this.network}`);
await this.disconnect();
}
async start() {
logger_1.default.debug(`Started worker for chain ${this.chain} ${this.network}`);
this.connect();
await this.setupListeners();
this.sync();
}
};
exports.XrpP2pWorker = XrpP2pWorker;
exports.XrpP2pWorker = XrpP2pWorker = __decorate([
Loggify_1.LoggifyClass
], XrpP2pWorker);
//# sourceMappingURL=index.js.map