UNPKG

bitcore-node

Version:

A blockchain indexing node with extended capabilities using bitcore

585 lines 26.2 kB
"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