bitcore-node
Version:
A blockchain indexing node with extended capabilities using bitcore
268 lines • 13.4 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.MoralisP2PWorker = void 0;
const crypto_rpc_1 = require("crypto-rpc");
const logger_1 = __importStar(require("../../../logger"));
const events_1 = require("../../../models/events");
const state_1 = require("../../../models/state");
const walletAddress_1 = require("../../../models/walletAddress");
const webhook_1 = require("../../../models/webhook");
const block_1 = require("../../../providers/chain-state/evm/models/block");
const transaction_1 = require("../../../providers/chain-state/evm/models/transaction");
const p2p_1 = require("../../../services/p2p");
const utils_1 = require("../../../utils");
const csp_1 = require("../api/csp");
class MoralisP2PWorker extends p2p_1.BaseP2PWorker {
constructor({ chain, network, chainConfig }) {
super({ chain, network, chainConfig });
this.chain = chain;
this.network = network;
this.chainConfig = chainConfig;
this.csp = new csp_1.MoralisStateProvider(this.chain);
this.addressSub = undefined;
this.chainId = undefined;
this.webhookTail = undefined;
this.bestBlock = 0;
this.chainNetworkStr = `${this.chain}:${this.network}`;
}
async start() {
this.refreshSyncingNode();
}
async stop() {
this.stopping = true;
clearInterval(this.syncInterval);
await this.webhookTail?.close();
await this.unregisterSyncingNode();
this.web3?.currentProvider?.disconnect?.();
}
async getWeb3() {
let connectionErrors = [];
try {
try {
await this.web3.eth.getBlockNumber();
}
catch (e) {
const providerConfigs = this.chainConfig.providers || (this.chainConfig.provider ? [this.chainConfig.provider] : []);
for (const config of providerConfigs) {
try {
const rpc = new crypto_rpc_1.CryptoRpc({ chain: this.chain, network: this.network, isEVM: true, ...config }).get(this.chain);
await rpc.web3.eth.getBlockNumber();
this.web3 = rpc.web3;
return this.web3;
}
catch (e) {
connectionErrors.push(e);
}
}
logger_1.default.error('Unable to connect to web3 %o:%o instance: %o', connectionErrors);
// Notice we don't unset this.web3. At worst, the old connection starts working again.
}
}
catch (e) {
logger_1.default.error('Error getting web3 %o:%o instance: %o', this.chain, this.network, e);
}
return this.web3;
}
async getChainId() {
if (this.chainId == null) {
const web3 = await this.getWeb3();
this.chainId = await web3.eth.getChainId();
}
return this.chainId;
}
async sync() {
const { value: state } = await state_1.StateStorage.getSingletonState();
let currentHeight = state?.verifiedBlockHeight?.[this.chain]?.[this.network] || 0;
let syncing = false;
this.syncInterval = setInterval(async () => {
if (this.stopping || !this.isSyncingNode) {
return;
}
if (syncing) {
return;
}
syncing = true;
const startTime = Date.now();
let msgInterval;
let block = null;
try {
const web3 = await this.getWeb3();
this.bestBlock = await web3.eth.getBlockNumber();
currentHeight = Math.max(this.bestBlock - (this.chainConfig.maxBlocksToSync || 50), currentHeight || 0);
const startHeight = currentHeight;
if (this.bestBlock - currentHeight <= 0) {
return;
}
logger_1.default.info(`Syncing ${this.bestBlock - currentHeight} blocks for ${this.chain} ${this.network}`);
msgInterval = setInterval(() => {
const blocksProcessed = currentHeight - startHeight;
const elapsedMinutes = (Date.now() - startTime) / (60 * 1000);
logger_1.default.info(`${(0, logger_1.timestamp)()} | Syncing... | Chain: ${this.chain} | Network: ${this.network} |${(blocksProcessed / elapsedMinutes)
.toFixed(2)
.padStart(8)} blocks/min | Height: ${currentHeight.toString().padStart(7)}`);
}, 1000 * 2); // 2 seconds
do {
block = await web3.eth.getBlock(currentHeight, true);
if (block && !this.stopping) {
const blockEvent = block_1.EVMBlockStorage.convertRawBlock(this.chain, this.network, block);
await events_1.EventStorage.signalBlock(blockEvent); // .catch((e) => logger.error(`Error signaling ${this.chainNetworkStr} block event: %o`, e.stack || e.message || e));
for (const tx of block.transactions) {
const txEvent = transaction_1.EVMTransactionStorage.convertRawTx(this.chain, this.network, tx, blockEvent);
await events_1.EventStorage.signalTx(txEvent); // .catch((e) => logger.error(`Error signaling ${this.chainNetworkStr} tx event: %o`, e.stack || e.message || e));
}
await state_1.StateStorage.setVerifiedBlockHeight({ chain: this.chain, network: this.network, height: currentHeight });
currentHeight = block.number + 1;
}
} while (block);
clearInterval(msgInterval); // clear before log below
logger_1.default.info(`${this.chainNetworkStr} up to date.`);
}
catch (err) {
logger_1.default.error(`Error syncing ${this.chainNetworkStr}: %o`, err.stack || err.message || err);
}
finally {
syncing = false;
clearInterval(msgInterval); // clear here too in case of a catch
}
}, 1000 * (this.chainConfig.syncIntervalSecs || 10)); // default 10 seconds
// Listen for webhooks and process any unprocessed ones in the db
await this.syncAddressActivity();
// Subscribe to external wallet activity provider
while (!(await this.subscribeToAddressActivity()) && !this.stopping) {
// subscription might fail if the API service is offline (webhook endpoint)
logger_1.default.warn(`Retrying ${this.chainNetworkStr} address subscription in 10 seconds...`);
await (0, utils_1.wait)(10 * 1000);
}
;
}
async subscribeToAddressActivity() {
try {
const chainId = await this.getChainId();
const subs = await this.csp.getAddressSubscriptions();
this.addressSub = subs?.find(sub => sub.chainIds.includes('0x' + chainId.toString(16)));
if (this.addressSub?.status === 'error') {
await this.csp.deleteAddressSubscription({ sub: this.addressSub });
this.addressSub = undefined;
}
if (!this.addressSub) {
// Create new subscription if none exists
this.addressSub = await this.csp.createAddressSubscription({
chain: this.chain,
network: this.network,
chainId
});
}
logger_1.default[this.addressSub?.status === 'active' ? 'info' : 'warn'](`Address subscription for ${this.chainNetworkStr} is: ${this.addressSub.status}`);
// todo check and update url
// if (!sub.webhookUrl.startsWith(provider.baseWebhookUrl)) {
// // todo update url
// }
const ONE_DAY = 1000 * 60 * 60 * 24;
const now = Date.now();
{ // Remove inactive addresses
const state = await state_1.StateStorage.getSingletonState();
const lastUpdate = state.value?.lastAddressSubscriptionUpdate?.[this.chain]?.[this.network];
const inactiveAddresses = await walletAddress_1.WalletAddressStorage.collection.find({
chain: this.chain,
network: this.network,
lastQueryTime: { $gte: lastUpdate, $lt: now - ONE_DAY * 60 }
}).toArray();
this.addressSub = await this.csp.updateAddressSubscription({
sub: this.addressSub,
addressesToRemove: inactiveAddresses.map(a => a.address)
});
}
{ // Add new addresses
const activeAddresses = await walletAddress_1.WalletAddressStorage.collection.find({
chain: this.chain,
network: this.network,
lastQueryTime: { $gte: now - ONE_DAY * 60 }
}).toArray();
this.addressSub = await this.csp.updateAddressSubscription({
sub: this.addressSub,
addressesToAdd: activeAddresses.map(a => a.address),
});
}
// Set subscription to active
this.addressSub = await this.csp.updateAddressSubscription({
sub: this.addressSub,
status: 'active'
});
logger_1.default[this.addressSub?.status === 'active' ? 'info' : 'warn'](`Address subscription for ${this.chainNetworkStr} is: ${this.addressSub.status}`);
return true;
}
catch (err) {
logger_1.default.error(`Error subscribing to ${this.chainNetworkStr} address activity: %o`, err.stack || err.message || err);
return false;
}
}
async syncAddressActivity() {
// if this is a reconnect, remove old listeners
this.webhookTail?.removeAllListeners();
this.webhookTail = webhook_1.WebhookStorage.getTail({ chain: this.chain, network: this.network });
logger_1.default.info(`Webhook tail initiated for ${this.chainNetworkStr}`);
this.webhookTail.on('error', (err) => {
logger_1.default.error(`Webhook ${this.chainNetworkStr} tail error: %o`, err.stack || err.message || err);
});
this.webhookTail.on('close', async () => {
// clearInterval(heightInterval);
if (!this.stopping) {
logger_1.default.error(`Webhook tail for ${this.chainNetworkStr} unexpectedly closed. Trying to reconnect in 5 seconds...`);
await (0, utils_1.wait)(5000); // wait 5 seconds before trying to reconnect
this.syncAddressActivity();
}
});
this.webhookTail.on('data', async (webhook) => {
try {
let coinEvents = [];
if (webhook.source !== 'moralis') {
return;
}
coinEvents = this.csp.webhookToCoinEvents({ chain: this.chain, network: this.network, webhook });
const result = await Promise.allSettled(coinEvents.map(evt => events_1.EventStorage.signalAddressCoin(evt)));
const someFulfilled = result.some(r => r.status === 'fulfilled' || (false && logger_1.default.error(`Error signaling ${this.chainNetworkStr} for webhook ${webhook._id?.toString()}: %o`, r.reason)));
if (someFulfilled) {
await webhook_1.WebhookStorage.setProcessed({ webhook });
}
}
catch (err) {
logger_1.default.error(`Error processing ${this.chainNetworkStr} webhook ${webhook._id?.toString()}: %o`, err.stack || err.message || err);
}
});
}
}
exports.MoralisP2PWorker = MoralisP2PWorker;
//# sourceMappingURL=p2p.js.map