bitcore-node
Version:
A blockchain indexing node with extended capabilities using bitcore
492 lines • 21.3 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MoralisStateProvider = void 0;
const os_1 = __importDefault(require("os"));
const request = require("request");
const web3_1 = __importDefault(require("web3"));
const config_1 = __importDefault(require("../../../config"));
const logger_1 = __importDefault(require("../../../logger"));
const cache_1 = require("../../../models/cache");
const walletAddress_1 = require("../../../models/walletAddress");
const csp_1 = require("../../../providers/chain-state/evm/api/csp");
const block_1 = require("../../../providers/chain-state/evm/models/block");
const transaction_1 = require("../../../providers/chain-state/evm/models/transaction");
const apiStream_1 = require("../../../providers/chain-state/external/streams/apiStream");
const utils_1 = require("../../../utils");
const streamWithEventPipe_1 = require("../../../utils/streamWithEventPipe");
class MoralisStateProvider extends csp_1.BaseEVMStateProvider {
constructor(chain) {
super(chain);
this.baseUrl = 'https://deep-index.moralis.io/api/v2.2';
this.baseStreamUrl = 'https://api.moralis-streams.com/streams/evm';
this.apiKey = config_1.default.externalProviders?.moralis?.apiKey;
this.baseWebhookurl = config_1.default.externalProviders?.moralis?.webhookBaseUrl;
this.headers = {
'Content-Type': 'application/json',
'X-API-Key': this.apiKey,
};
}
// @override
async getBlockBeforeTime(params) {
const { chain, network, time } = params;
const date = new Date(time || Date.now());
const chainId = await this.getChainId({ network });
const blockNum = await this._getBlockNumberByDate({ chainId, date });
if (!blockNum) {
return null;
}
const blockId = blockNum.toString();
const blocks = await this._getBlocks({ chain, network, blockId, args: { limit: 1 } });
return blocks.blocks[0] || null;
}
// @override
async getFee(params) {
let { network, target = 4, txType } = params;
const chain = this.chain;
if (network === 'livenet') {
network = 'mainnet';
}
let cacheKey = `getFee-${chain}-${network}-${target}`;
if (txType) {
cacheKey += `-type${txType}`;
}
return cache_1.CacheStorage.getGlobalOrRefresh(cacheKey, async () => {
let feerate;
const { rpc } = await this.getWeb3(network, { type: 'historical' });
feerate = await rpc.estimateFee({ nBlocks: target, txType });
return { feerate, blocks: target };
}, cache_1.CacheStorage.Times.Minute);
}
// @override
async getLocalTip({ chain, network }) {
const { web3 } = await this.getWeb3(network);
const block = await web3.eth.getBlock('latest');
return block_1.EVMBlockStorage.convertRawBlock(chain, network, block);
}
// @override
async streamBlocks(params) {
const { chain, network, req, res } = params;
const { web3 } = await this.getWeb3(network);
const chainId = await this.getChainId({ network });
const blockRange = await this.getBlocksRange({ ...params, chainId });
const tipHeight = await web3.eth.getBlockNumber();
let isReading = false;
const stream = new streamWithEventPipe_1.ReadableWithEventPipe({
objectMode: true,
async read() {
if (isReading) {
return;
}
isReading = true;
let block;
let nextBlock;
try {
for (const blockNum of blockRange) {
// stage next block in new var so `nextBlock` doesn't get overwritten if needed for `block`
const thisNextBlock = parseInt(block?.number) === blockNum + 1 ? block : await web3.eth.getBlock(blockNum + 1);
block = parseInt(nextBlock?.number) === blockNum ? nextBlock : await web3.eth.getBlock(blockNum);
if (!block) {
continue;
}
nextBlock = thisNextBlock;
const convertedBlock = block_1.EVMBlockStorage.convertRawBlock(chain, network, block);
convertedBlock.nextBlockHash = nextBlock?.hash;
convertedBlock.confirmations = tipHeight - block.number + 1;
this.push(convertedBlock);
}
}
catch (e) {
logger_1.default.error('Error streaming blocks: %o', e);
}
this.push(null);
}
});
return apiStream_1.ExternalApiStream.onStream(stream, req, res);
}
// @override
async _getTransaction(params) {
let { chain, network, txId } = params;
network = network.toLowerCase();
const { web3 } = await this.getWeb3(network, { type: 'historical' });
const tipHeight = await web3.eth.getBlockNumber();
const chainId = await this.getChainId({ network });
const found = await this._getTransactionFromMoralis({ chain, network, chainId, txId });
return { tipHeight, found };
}
// @override
async _buildAddressTransactionsStream(params) {
const { req, res, args, network, address } = params;
const chainId = await this.getChainId({ network });
const txStream = await this._streamAddressTransactionsFromMoralis({
chainId,
chain: this.chain,
network,
address,
args: {
limit: 10, // default limit when querying by address
...args
}
});
// TODO unify `ExternalApiStream.onStream` and `Storage.apiStream` which are effectively doing the same thing
const result = await apiStream_1.ExternalApiStream.onStream(txStream, req, res);
if (!result?.success) {
logger_1.default.error('Error mid-stream (streamAddressTransactions): %o', result.error?.log || result.error);
}
}
// @override
async _buildWalletTransactionsStream(params, streamParams) {
const { network, args } = params;
let { transactionStream, walletAddresses } = streamParams;
const chainId = await this.getChainId({ network });
for (const address of walletAddresses) {
const txStream = await this._streamAddressTransactionsFromMoralis({
chainId,
chain: this.chain,
network,
address,
args: {
limit: args.limit, // no default limit when querying by wallet. Note: BWS caches txs
order: 'ASC',
...args
}
});
transactionStream = txStream.eventPipe(transactionStream);
// Do not await these promises. They are not critical to the stream.
walletAddress_1.WalletAddressStorage.updateLastQueryTime({ chain: this.chain, network, address })
.catch(e => logger_1.default.warn(`Failed to update ${this.chain}:${network} address lastQueryTime: %o`, e)),
this._addAddressToSubscription({ chainId, address })
.catch(e => logger_1.default.warn(`Failed to add address to ${this.chain}:${network} Moralis subscription: %o`, e));
}
return transactionStream;
}
// @override
async _getBlocks(params) {
const { chain, network } = params;
const blocks = [];
const { web3 } = await this.getWeb3(network);
const chainId = await this.getChainId({ network });
const blockRange = await this.getBlocksRange({ ...params, chainId });
for (const blockNum of blockRange) {
const block = await web3.eth.getBlock(blockNum);
const nextBlock = await web3.eth.getBlock(block.number + 1);
const convertedBlock = block_1.EVMBlockStorage.convertRawBlock(chain, network, block);
convertedBlock.nextBlockHash = nextBlock?.hash;
blocks.push(convertedBlock);
}
const tipHeight = await web3.eth.getBlockNumber();
return { tipHeight, blocks };
}
// @override
async _getBlockNumberByDate({ chainId, date }) {
if (!date || !(0, utils_1.isDateValid)(date)) {
throw new Error('Invalid date');
}
if (!chainId) {
throw new Error('Invalid chainId');
}
const query = this._transformQueryParams({ chainId, args: { date } });
const queryStr = this._buildQueryString(query);
return new Promise((resolve, reject) => {
request({
method: 'GET',
url: `${this.baseUrl}/dateToBlock${queryStr}`,
headers: this.headers,
json: true
}, (err, _data, body) => {
if (err) {
return reject(err);
}
return resolve(body.block);
});
});
}
/** MORALIS METHODS */
async _getTransactionFromMoralis(params) {
const { chain, network, chainId, txId } = params;
const query = this._buildQueryString({ chain: chainId, include: 'internal_transactions' });
return new Promise((resolve, reject) => {
request({
method: 'GET',
url: `${this.baseUrl}/transaction/${txId}${query}`,
headers: this.headers,
json: true
}, (err, data) => {
if (err) {
return reject(err);
}
if (typeof data === 'string') {
return reject(new Error(data));
}
const tx = data.body;
return resolve(this._transformTransaction({ chain, network, ...tx }));
});
});
}
_streamAddressTransactionsFromMoralis(params) {
const { chainId, chain, network, address, args } = params;
if (args.tokenAddress) {
return this._streamERC20TransactionsByAddress({ chainId, chain, network, address, tokenAddress: args.tokenAddress, args });
}
if (!address) {
throw new Error('Missing address');
}
if (!chainId) {
throw new Error('Invalid chainId');
}
const query = this._transformQueryParams({ chainId, args }); // throws if no chain or network
const queryStr = this._buildQueryString({
...query,
order: args.order || 'DESC', // default to descending order
limit: args.pageSize || 10, // limit per request/page. total limit (args.limit) is checked in apiStream._read()
include: 'internal_transactions'
});
args.transform = (tx) => {
const _tx = this._transformTransaction({ chain, network, ...tx });
const confirmations = this._calculateConfirmations(tx, args.tipHeight);
return transaction_1.EVMTransactionStorage._apiTransform({ ..._tx, confirmations }, { object: true });
};
return new apiStream_1.ExternalApiStream(`${this.baseUrl}/${address}${queryStr}`, this.headers, args);
}
_streamERC20TransactionsByAddress({ chainId, chain, network, address, tokenAddress, args }) {
if (!address) {
throw new Error('Missing address');
}
if (!tokenAddress) {
throw new Error('Missing token address');
}
if (!chainId) {
throw new Error('Invalid chainId');
}
const queryTransform = this._transformQueryParams({ chainId, args }); // throws if no chain or network
const queryStr = this._buildQueryString({
...queryTransform,
order: args.order || 'DESC', // default to descending order
limit: args.pageSize || 10, // limit per request/page. total limit (args.limit) is checked in apiStream._read()
contract_addresses: [tokenAddress],
});
args.transform = (tx) => {
const _tx = this._transformTokenTransfer({ chain, network, ...tx });
const confirmations = this._calculateConfirmations(tx, args.tipHeight);
return transaction_1.EVMTransactionStorage._apiTransform({ ..._tx, confirmations }, { object: true });
};
return new apiStream_1.ExternalApiStream(`${this.baseUrl}/${address}/erc20/transfers${queryStr}`, this.headers, args);
}
_transformTransaction(tx) {
const transformed = {
chain: tx.chain,
network: tx.network,
txid: tx.hash || tx.transaction_hash, // erc20 transfer txs have transaction_hash
blockHeight: Number(tx.block_number ?? tx.blockNumber),
blockHash: tx.block_hash ?? tx.blockHash,
blockTime: new Date(tx.block_timestamp ?? tx.blockTimestamp),
blockTimeNormalized: new Date(tx.block_timestamp ?? tx.blockTimestamp),
value: tx.value,
gasLimit: tx.gas ?? 0,
gasPrice: tx.gas_price ?? tx.gasPrice ?? 0,
fee: Number(tx.receipt_gas_used ?? tx.receiptGasUsed ?? 0) * Number(tx.gas_price ?? tx.gasPrice ?? 0),
nonce: tx.nonce,
to: web3_1.default.utils.toChecksumAddress(tx.to_address ?? tx.toAddress),
from: web3_1.default.utils.toChecksumAddress(tx.from_address ?? tx.fromAddress),
data: tx.input,
internal: [],
calls: tx?.internal_transactions?.map(t => this._transformInternalTransaction(t)) || [],
effects: [],
category: tx.category,
wallets: [],
transactionIndex: tx.transaction_index ?? tx.transactionIndex
};
transaction_1.EVMTransactionStorage.addEffectsToTxs([transformed]);
return transformed;
}
_transformInternalTransaction(tx) {
return {
from: web3_1.default.utils.toChecksumAddress(tx.from),
to: web3_1.default.utils.toChecksumAddress(tx.to),
gas: tx.gas,
gasUsed: tx.gas_used,
input: tx.input,
output: tx.output,
type: tx.type,
value: tx.value,
abiType: transaction_1.EVMTransactionStorage.abiDecode(tx.input)
};
}
_transformTokenTransfer(transfer) {
let _transfer = this._transformTransaction(transfer);
return {
..._transfer,
transactionHash: transfer.transaction_hash,
transactionIndex: transfer.transaction_index,
contractAddress: transfer.contract_address ?? transfer.address,
name: transfer.token_name
};
}
_transformQueryParams(params) {
const { chainId, args } = params;
let query = {
chain: this._formatChainId(chainId),
};
if (args) {
if (args.startBlock || args.endBlock) {
if (args.startBlock) {
query.from_block = Number(args.startBlock);
}
if (args.endBlock) {
query.to_block = Number(args.endBlock);
}
}
else {
if (args.startDate) {
query.from_date = args.startDate;
}
if (args.endDate) {
query.to_date = args.endDate;
}
}
if (args.direction) {
query.order = Number(args.direction) > 0 ? 'ASC' : 'DESC';
}
if (args.date) {
query.date = new Date(args.date).getTime();
}
}
return query;
}
_calculateConfirmations(tx, tip) {
let confirmations = 0;
if (tx.blockHeight && tx.blockHeight >= 0) {
confirmations = tip - tx.blockHeight + 1;
}
return confirmations;
}
_buildQueryString(params) {
const query = [];
if (params.chain) {
params.chain = this._formatChainId(params.chain);
}
for (const [key, value] of Object.entries(params)) {
if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++) {
// add array values in the form of key[i]=value
if (value[i] != null)
query.push(`${key}%5B${i}%5D=${value[i]}`);
}
}
else if (value != null) {
query.push(`${key}=${value}`);
}
}
return query.length ? `?${query.join('&')}` : '';
}
_formatChainId(chainId) {
return '0x' + parseInt(chainId).toString(16);
}
/**
* Request wrapper for moralis Streams (subscriptions)
* @param method
* @param url
* @param body
* @returns
*/
_subsRequest(method, url, body) {
return new Promise((resolve, reject) => {
request({
method,
url,
headers: this.headers,
json: true,
body
}, (err, data) => {
if (err) {
logger_1.default.error(`Error with Moralis subscription call ${method}:${url}: ${err.stack || err.message || err}`);
return reject(err);
}
if (typeof data === 'string') {
logger_1.default.warn(`Moralis subscription ${method}:${url} returned a string: ${data}`);
return reject(new Error(data));
}
return resolve(data.body);
});
});
}
async createAddressSubscription(params) {
const { chain, network, chainId } = params;
const _chainId = this._formatChainId(chainId);
const result = await this._subsRequest('PUT', this.baseStreamUrl, {
description: `Bitcore ${_chainId} - ${os_1.default.hostname()} - addresses`,
// tag: '',
chainIds: [_chainId],
webhookUrl: `${this.baseWebhookurl}/${chain}/${network}/moralis`,
includeNativeTxs: true,
includeInternalTxs: true
});
if (!result.id) {
throw new Error('Failed to create subscription: ' + JSON.stringify(result));
}
return result;
}
async getAddressSubscriptions() {
const subs = await this._subsRequest('GET', this.baseStreamUrl + '?limit=100');
return subs.result;
}
deleteAddressSubscription(params) {
const { sub } = params;
return this._subsRequest('DELETE', `${this.baseStreamUrl}/${sub.id}`);
}
async updateAddressSubscription(params) {
const { sub, addressesToAdd, addressesToRemove, status } = params;
let moralisSub = null;
if (addressesToAdd && addressesToAdd.length > 0) {
moralisSub = await this._subsRequest('POST', `${this.baseStreamUrl}/${sub.id}/address`, { address: addressesToAdd });
}
else if (addressesToRemove && addressesToRemove.length > 0) {
moralisSub = await this._subsRequest('DELETE', `${this.baseStreamUrl}/${sub.id}/address`, { address: addressesToRemove });
}
else if (status) {
moralisSub = await this._subsRequest('POST', `${this.baseStreamUrl}/${sub.id}/status`, { status });
}
if (moralisSub?.message) {
throw new Error(moralisSub.message);
}
return moralisSub?.id ? moralisSub : sub; // fallback to sub in case there's nothing to update (e.g. addressesToAdd is an empty array)
}
webhookToCoinEvents(params) {
const { chain, network, webhook } = params;
if (webhook.body.confirmed) {
// Moralis broadcasts both confirmed and unconfirmed.
// Filtering out confirmed de-duplicates events.
return [];
}
const coinEvents = webhook.body.txs.flatMap(tx => this._transformWebhookTransaction({ chain, network, tx, webhook: webhook.body }));
return coinEvents;
}
_transformWebhookTransaction(params) {
const { chain, network, tx } = params;
const events = [];
for (const address of tx.triggered_by) {
events.push({
address,
coin: {
chain,
network,
value: tx.value,
address: tx.toAddress,
mintTxid: tx.hash
}
});
}
return events;
}
async _addAddressToSubscription({ chainId, address }) {
const subs = await this.getAddressSubscriptions();
const sub = subs?.find(sub => sub.chainIds.includes('0x' + chainId.toString(16)));
if (sub) {
await this.updateAddressSubscription({ sub, addressesToAdd: [address] });
}
}
}
exports.MoralisStateProvider = MoralisStateProvider;
//# sourceMappingURL=csp.js.map