bitcore-node
Version:
A blockchain indexing node with extended capabilities using bitcore
585 lines • 26.2 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 __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 __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;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.EVMTransactionStorage = exports.EVMTransactionModel = void 0;
const _ = __importStar(require("lodash"));
const Loggify_1 = require("../../../../decorators/Loggify");
const logger_1 = __importDefault(require("../../../../logger"));
const baseTransaction_1 = require("../../../../models/baseTransaction");
const cache_1 = require("../../../../models/cache");
const events_1 = require("../../../../models/events");
const walletAddress_1 = require("../../../../models/walletAddress");
const config_1 = require("../../../../services/config");
const storage_1 = require("../../../../services/storage");
const utils_1 = require("../../../../utils");
const erc20_1 = require("../abi/erc20");
const erc721_1 = require("../abi/erc721");
const invoice_1 = require("../abi/invoice");
const multisend_1 = require("../abi/multisend");
const multisig_1 = require("../abi/multisig");
const web3_1 = __importDefault(require("web3"));
function requireUncached(module) {
delete require.cache[require.resolve(module)];
return require(module);
}
const Erc20Decoder = requireUncached('abi-decoder');
Erc20Decoder.addABI(erc20_1.ERC20Abi);
function getErc20Decoder() {
return Erc20Decoder;
}
const Erc721Decoder = requireUncached('abi-decoder');
Erc721Decoder.addABI(erc721_1.ERC721Abi);
function getErc721Decoder() {
return Erc721Decoder;
}
const InvoiceDecoder = requireUncached('abi-decoder');
InvoiceDecoder.addABI(invoice_1.InvoiceAbi);
function getInvoiceDecoder() {
return InvoiceDecoder;
}
const MultisigDecoder = requireUncached('abi-decoder');
MultisigDecoder.addABI(multisig_1.MultisigAbi);
function getMultisigDecoder() {
return MultisigDecoder;
}
const MultisendDecoder = requireUncached('abi-decoder');
MultisendDecoder.addABI(multisend_1.MultisendAbi);
function getMultisendDecoder() {
return MultisendDecoder;
}
let EVMTransactionModel = class EVMTransactionModel extends baseTransaction_1.BaseTransaction {
constructor(storage = storage_1.Storage) {
super(storage);
}
async onConnect() {
super.onConnect();
this.collection.createIndex({ chain: 1, network: 1, to: 1 }, { background: true, sparse: true });
this.collection.createIndex({ chain: 1, network: 1, from: 1 }, { background: true, sparse: true });
this.collection.createIndex({ chain: 1, network: 1, from: 1, nonce: 1 }, { background: true, sparse: true });
this.collection.createIndex({ chain: 1, network: 1, 'abiType.params.0.value': 1, blockTimeNormalized: 1 }, {
background: true,
partialFilterExpression: { 'abiType.type': 'ERC20', 'abiType.name': 'transfer' }
});
this.collection.createIndex({ chain: 1, network: 1, 'calls.abiType.params.value': 1, blockTimeNormalized: 1 }, {
background: true,
partialFilterExpression: { 'calls.abiType.type': 'ERC20', 'calls.abiType.params.type': 'address' }
});
this.collection.createIndex({ chain: 1, network: 1, 'internal.action.to': 1 }, {
background: true,
sparse: true
});
this.collection.createIndex({ chain: 1, network: 1, 'calls.to': 1 }, {
background: true,
sparse: true
});
this.collection.createIndex({ chain: 1, network: 1, 'effects.to': 1, blockTimeNormalized: 1 }, {
background: true,
partialFilterExpression: { 'effects.to': { $exists: true } }
});
this.collection.createIndex({ chain: 1, network: 1, 'effects.from': 1, blockTimeNormalized: 1 }, {
background: true,
partialFilterExpression: { 'effects.from': { $exists: true } }
});
}
async batchImport(params) {
const operations = [];
operations.push(this.pruneMempool({ ...params }));
const txOps = await this.addTransactions({ ...params });
logger_1.default.debug('Writing Transactions: %o', txOps.length);
operations.push(...(0, utils_1.partition)(txOps, txOps.length / config_1.Config.get().maxPoolSize).map(txBatch => this.collection.bulkWrite(txBatch.map(op => this.toMempoolSafeUpsert(op, params.height)), { ordered: false })));
await Promise.all(operations);
if (params.initialSyncComplete) {
await this.expireBalanceCache(txOps);
}
// Create events for mempool txs
if (params.height < 0 /* SpentHeightIndicators.minimum */) {
for (let op of txOps) {
const filter = op.updateOne.filter;
const tx = { ...op.updateOne.update.$set, ...filter };
await events_1.EventStorage.signalTx(tx);
await events_1.EventStorage.signalAddressCoin({
address: tx.to,
coin: { value: tx.value, address: tx.to, chain: params.chain, network: params.network, mintTxid: tx.txid }
});
}
}
}
getAllTouchedAddresses(tx) {
const { to, from, effects } = tx;
let toBatch = new Set();
let fromBatch = new Set();
const addToBatch = (batch, obj) => {
// Adds string representation to batch to guard uniqueness since {} != {} but '{}' == '{}'
batch.add(JSON.stringify(obj));
};
addToBatch(toBatch, { address: to });
addToBatch(fromBatch, { address: from });
if (effects && effects.length) {
for (const effect of effects) {
// Handle internal value transfers
if (!effect.contractAddress) {
addToBatch(toBatch, { address: effect.to });
addToBatch(fromBatch, { address: effect.from });
}
else if (effect.type == 'ERC20:transfer') {
// Handle ERC20s
addToBatch(toBatch, { address: effect.to, tokenAddress: effect.contractAddress });
addToBatch(fromBatch, { address: effect.from, tokenAddress: effect.contractAddress });
}
}
}
// Convert Set made up of unique strings back to object representations
const tos = Array.from(toBatch).map(strObj => JSON.parse(strObj));
const froms = Array.from(fromBatch).map(strObj => JSON.parse(strObj));
return { tos, froms };
}
async expireBalanceCache(txOps) {
for (const op of txOps) {
const { chain, network } = op.updateOne.filter;
const { tos, froms } = this.getAllTouchedAddresses(op.updateOne.update.$set);
const uniqueBatch = tos.concat(froms);
for (const payload of uniqueBatch) {
const lowerAddress = payload.address.toLowerCase();
const cacheKey = payload.tokenAddress
? `getBalanceForAddress-${chain}-${network}-${lowerAddress}-${payload.tokenAddress.toLowerCase()}`
: `getBalanceForAddress-${chain}-${network}-${lowerAddress}`;
await cache_1.CacheStorage.expire(cacheKey);
}
}
}
async addTransactions(params) {
let { blockTimeNormalized, chain, height, network, parentChain, forkHeight } = params;
if (parentChain && forkHeight && height < forkHeight) {
const parentTxs = await exports.EVMTransactionStorage.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()
}
},
upsert: true,
forceServerObjectId: true
}
};
});
}
else {
return Promise.all(
// Get all "to" and "from" addresses so we can add the any corresponding wallets
params.txs.map(async (tx) => {
const { tos, froms } = this.getAllTouchedAddresses(tx);
const toAddresses = tos.map(a => a.address);
const fromAddresses = froms.map(a => a.address);
const walletsAddys = await walletAddress_1.WalletAddressStorage.collection
.find({ chain, network, address: { $in: [...fromAddresses, ...toAddresses] } })
.toArray();
const wallets = _.uniqBy(walletsAddys.map(w => w.wallet), w => w.toHexString());
// If config value is set then only store needed tx properties
let leanTx = tx;
if (config_1.Config.chainConfig({ chain, network }).leanTransactionStorage) {
leanTx = exports.EVMTransactionStorage.toLeanTransaction(tx);
}
return {
updateOne: {
filter: { txid: tx.txid, chain, network },
update: {
$set: {
...leanTx,
blockTimeNormalized,
wallets
}
},
upsert: true,
forceServerObjectId: true
}
};
}));
}
}
async pruneMempool(params) {
const { chain, network, initialSyncComplete, txs } = params;
if (!initialSyncComplete) {
return;
}
for (const tx of txs) {
await this.collection.update({
chain,
network,
from: tx.from,
nonce: tx.nonce,
txid: { $ne: tx.txid },
blockHeight: -1 /* SpentHeightIndicators.pending */
}, { $set: { blockHeight: -3 /* SpentHeightIndicators.conflicting */, replacedByTxid: tx.txid } }, { w: 0, j: false, multi: true });
}
return;
}
getTransactions(params) {
let originalQuery = params.query;
const { query, options } = storage_1.Storage.getFindOptions(this, params.options);
const finalQuery = Object.assign({}, originalQuery, query);
return this.collection.find(finalQuery, options).addCursorFlag('noCursorTimeout', true);
}
abiDecode(input) {
try {
const erc20Data = getErc20Decoder().decodeMethod(input);
if (erc20Data) {
return {
type: 'ERC20',
...erc20Data
};
}
}
catch (e) { }
try {
const erc721Data = getErc721Decoder().decodeMethod(input);
if (erc721Data) {
return {
type: 'ERC721',
...erc721Data
};
}
}
catch (e) { }
try {
const invoiceData = getInvoiceDecoder().decodeMethod(input);
if (invoiceData) {
return {
type: 'INVOICE',
...invoiceData
};
}
}
catch (e) { }
try {
const multisendData = getMultisendDecoder().decodeMethod(input);
if (multisendData) {
return {
type: 'MUTLISEND',
...multisendData
};
}
}
catch (e) { }
try {
const multisigData = getMultisigDecoder().decodeMethod(input);
if (multisigData) {
return {
type: 'MULTISIG',
...multisigData
};
}
}
catch (e) { }
return undefined;
}
/**
* Creates an object with param names as keys instead of an array of objects
* @param abi
* @returns object of abi param values that can be accessed with the name as a key
*/
parseAbiParams(abi) {
const params = abi.params;
const parsed = {};
for (let param of params) {
const { value } = param;
parsed[param.name] = value;
}
return parsed;
}
/**
* Adds effects details object to in process txs
*/
addEffectsToTxs(txs) {
for (let tx of txs) {
tx.effects = this.getEffects(tx);
}
}
/**
* Creates an array of all effects for a given tx
* @param tx A tx object that contains extra data that we don't want to store long term
* @returns An array of all effects for the transaction
*/
getEffects(tx) {
const effects = [];
try {
if (tx.calls?.length) { // Geth trace calls[]
for (let call of tx.calls) {
if (call.value && BigInt(call.value) > 0) {
// Handle native asset transfer
const effect = this._getEffectForNativeTransfer(BigInt(call.value).toString(), call.to, call.from, call.depth);
effects.push(effect);
}
if (call.abiType) { // If there was a known ABI (ERC20, Invoice) transfer within the tx execution
// Handle Abi related effects
let effect;
if (call.type === 'DELEGATECALL') { // Delegate calls are proxy calls within a smart contract
// find parent call that's one level up. E.g. if depth = '0_1_2', then find '0_1'
const parent = tx.calls.find(c => c.depth === call.depth.split('_').slice(0, -1).join('_')) || { to: tx.to, from: tx.from, input: null }; // Fallback to tx.to and tx.from if no parent found
if (parent?.to === call.from && parent?.input === call.input) {
// If parent is the same as the current call, then it's just a proxy call
continue;
}
effect = this._getEffectForAbiType(call.abiType, parent.to, parent.from, call.depth);
}
else {
effect = this._getEffectForAbiType(call.abiType, call.to, call.from, call.depth);
}
if (effect) {
effects.push(effect);
}
}
}
}
else if (tx.internal?.length) { // LEGACY: Used for converting old OpenEthereum/Parity db entries with internal[]
for (let internalTx of tx.internal) {
if (internalTx.action.value && BigInt(internalTx.action.value) > 0) {
// Handle native asset transfer
const effect = this._getEffectForNativeTransfer(BigInt(internalTx.action.value).toString(), internalTx.action.to, internalTx.action.from || tx.from, internalTx.traceAddress.join('_'));
effects.push(effect);
}
if (internalTx.abiType) {
// Handle Abi related effects
const effect = this._getEffectForAbiType(internalTx.abiType, internalTx.action.to, internalTx.action.from || tx.from, internalTx.traceAddress.join('_'));
if (effect) {
effects.push(effect);
}
}
}
}
else if (tx.abiType) { // We recognized upstream that this is a known ABI tx
// Handle Abi related effects
const effect = this._getEffectForAbiType(tx.abiType, tx.to, tx.from, '');
if (effect) {
effects.push(effect);
}
}
}
catch (err) {
logger_1.default.error('Error Getting Effects For TxId: %o ::%o', tx.txid, err);
}
return effects;
}
_getEffectForAbiType(abi, to, from, callStack) {
// Check that the params are valid before parsing
if (!to || !from)
return;
if (`${abi.type}:${abi.name}` == 'ERC20:transfer') {
const params = this.parseAbiParams(abi);
const { _to, _value } = params;
// Check that the params are valid before parsing
if (!_to || !_value)
return;
return {
type: 'ERC20:transfer',
to: web3_1.default.utils.toChecksumAddress(_to),
from: web3_1.default.utils.toChecksumAddress(from),
amount: web3_1.default.utils.fromWei(_value, 'wei'),
contractAddress: web3_1.default.utils.toChecksumAddress(to),
callStack
};
}
else if (`${abi.type}:${abi.name}` == 'ERC20:transferFrom') {
const params = this.parseAbiParams(abi);
const { _to, _from, _value } = params;
// Check that the params are valid before parsing
if (!_to || !_from || !_value)
return;
return {
type: 'ERC20:transfer',
to: web3_1.default.utils.toChecksumAddress(_to),
from: web3_1.default.utils.toChecksumAddress(_from),
amount: web3_1.default.utils.fromWei(_value, 'wei'),
contractAddress: web3_1.default.utils.toChecksumAddress(to),
callStack
};
}
else if (`${abi.type}:${abi.name}` == 'MULTISIG:submitTransaction') {
const params = this.parseAbiParams(abi);
const { destination, value } = params;
// Check that the params are valid before parsing
if (!destination || !value)
return;
return {
type: 'MULTISIG:submitTransaction',
to: web3_1.default.utils.toChecksumAddress(destination),
from: web3_1.default.utils.toChecksumAddress(from),
amount: web3_1.default.utils.fromWei(value, 'wei'),
contractAddress: web3_1.default.utils.toChecksumAddress(to),
callStack
};
}
else if (`${abi.type}:${abi.name}` == 'MULTISIG:confirmTransaction') {
return {
type: 'MULTISIG:confirmTransaction',
to: '0x0',
from: web3_1.default.utils.toChecksumAddress(from),
amount: '0',
contractAddress: web3_1.default.utils.toChecksumAddress(to),
callStack
};
}
return;
}
_getEffectForNativeTransfer(value, to, from, callStack) {
const effect = {
to: web3_1.default.utils.toChecksumAddress(to),
from: web3_1.default.utils.toChecksumAddress(from),
amount: web3_1.default.utils.fromWei(value, 'wei'),
callStack
};
return effect;
}
/**
* Receives any type of TX and returns a lean version without unused properties
* @param tx - transaction to leanify
*/
toLeanTransaction(tx) {
const removableProperties = ['data', 'internal', 'calls', 'abiType'];
for (let prop of removableProperties) {
if (tx[prop]) {
delete tx[prop];
}
}
return tx;
}
convertRawTx(chain, network, tx, block) {
if (!block) {
const txid = tx.hash || '';
const to = tx.to || '';
const from = tx.from || '';
const value = Number(tx.value);
const fee = Number(tx.gas) * Number(tx.gasPrice);
const abiType = this.abiDecode(tx.input);
const nonce = tx.nonce || 0;
const convertedTx = {
chain,
network,
blockHeight: (0, utils_1.valueOrDefault)(tx.blockNumber, -1),
blockHash: (0, utils_1.valueOrDefault)(tx.blockHash, undefined),
data: Buffer.from(tx.input || '0x'),
txid,
blockTime: new Date(),
blockTimeNormalized: new Date(),
fee,
transactionIndex: tx.transactionIndex || 0,
value,
wallets: [],
to,
from,
gasLimit: Number(tx.gas),
gasPrice: Number(tx.gasPrice),
nonce,
internal: [],
calls: []
};
if (abiType) {
convertedTx.abiType = abiType;
}
return convertedTx;
}
else {
const { hash: blockHash, time: blockTime, timeNormalized: blockTimeNormalized, height } = block;
const noBlockTx = this.convertRawTx(chain, network, tx);
return {
...noBlockTx,
blockHeight: height,
blockHash,
blockTime,
blockTimeNormalized
};
}
}
// Correct tx.data.toString() => 0xa9059cbb00000000000000000000000001503dfc5ad81bf630d83697e98601871bb211b60000000000000000000000000000000000000000000000000000000000002710
// Incorrect: tx.data.toString('hex') => 307861393035396362623030303030303030303030303030303030303030303030303031353033646663356164383162663633306438333639376539383630313837316262323131623630303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303032373130
_apiTransform(tx, options) {
let transaction = {
txid: tx.txid || '',
network: tx.network || '',
chain: tx.chain || '',
blockHeight: (0, utils_1.valueOrDefault)(tx.blockHeight, -1),
blockHash: tx.blockHash || '',
blockTime: tx.blockTime ? tx.blockTime.toISOString() : '',
blockTimeNormalized: tx.blockTimeNormalized ? tx.blockTimeNormalized.toISOString() : '',
fee: (0, utils_1.valueOrDefault)(tx.fee, -1),
value: (0, utils_1.valueOrDefault)(tx.value, -1),
gasLimit: (0, utils_1.valueOrDefault)(tx.gasLimit, -1),
gasPrice: (0, utils_1.valueOrDefault)(tx.gasPrice, -1),
nonce: (0, utils_1.valueOrDefault)(tx.nonce, 0),
to: tx.to || '',
from: tx.from || '',
effects: tx.effects || []
};
// Add non-lean properties if we aren't excluding them
const config = config_1.Config.chainConfig({ chain: tx.chain, network: tx.network });
if (config && !config.leanTransactionStorage) {
const dataStr = tx.data ? tx.data.toString() : '';
const decodedData = this.abiDecode(dataStr);
const nonLeanProperties = {
data: dataStr,
abiType: tx.abiType || (0, utils_1.valueOrDefault)(decodedData, undefined),
internal: tx.internal
? tx.internal.map(t => ({ ...t, decodedData: this.abiDecode(t?.action?.input || '0x') }))
: [],
calls: tx.calls ? tx.calls.map(t => ({ ...t, decodedData: this.abiDecode(t.input || '0x') })) : []
};
transaction = Object.assign(transaction, nonLeanProperties);
}
if (options && options.object) {
return transaction;
}
return JSON.stringify(transaction);
}
};
exports.EVMTransactionModel = EVMTransactionModel;
exports.EVMTransactionModel = EVMTransactionModel = __decorate([
Loggify_1.LoggifyClass
], EVMTransactionModel);
exports.EVMTransactionStorage = new EVMTransactionModel();
//# sourceMappingURL=transaction.js.map