UNPKG

argosjs

Version:

Ethereum smart-contract events visualiser

691 lines (690 loc) 36.1 kB
"use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; result["default"] = mod; return result; }; Object.defineProperty(exports, "__esModule", { value: true }); var ethers_1 = require("ethers"); var neo4j_driver_1 = require("neo4j-driver"); var DatabaseFactory_1 = require("../factory/DatabaseFactory"); var errors = __importStar(require("../utils/error")); var strategy_1 = require("../utils/strategy"); var Watcher_1 = require("./Watcher"); var EthereumWatcher = /** @class */ (function (_super) { __extends(EthereumWatcher, _super); /** * Create a watcher for Ethereum blockchain * @param {string} contractAddr the address of the verified contract * @param {string} abi the ABI of the verified contract * @param {number} providerType the Etherscan API Token * @param {DatabaseConstructorType} dbType the database servcice constructor * @param {ProviderConfig} providerConfig the loaded config file * @param {boolean} clearDB retrieve from genesis block instead of the latest in DB (db cleared) * @param {string} exportDir export dir * @returns {EthereumWatcher} Ethereum instance */ function EthereumWatcher(contractAddr, abi, providerType, dbType, providerConfig, clearDB, exportDir) { var _this = _super.call(this) || this; _this._contractAddr = contractAddr; _this._dbType = dbType; _this._dbService = DatabaseFactory_1.DatabaseFactory.createDbInstance(_this._dbType); _this._clearDB = clearDB; _this._config = providerConfig; _this._provider = _this.getProvider(providerType, providerConfig); _this._contract = new ethers_1.ethers.Contract(_this._contractAddr, abi, _this._provider); _this._exportDir = exportDir; _this._event = undefined; _this._timeout = _this._config.timeout; _this._logSizePerOp = _this._config.logSizePerOp || 900; console.log("Ethereum Watcher initiated!"); return _this; } /** * Watch event with particular model * @param {string} eventName name of the event * @param {Neode.SchemaObject} dbModel the model loaded via require() * @param {number} fromDate timestamp * @param {number} toDate timestamp */ EthereumWatcher.prototype.watchEvents = function (eventName, fromDate, toDate) { return __awaiter(this, void 0, void 0, function () { var fromBlock, _a, toBlock, _b, startTime, endTime, elapsedMilli, elapsedSeconds, elapsedMinutes, roundMinutes, roundSeconds, roundMilis; var _this = this; return __generator(this, function (_c) { switch (_c.label) { case 0: if (!fromDate) return [3 /*break*/, 2]; return [4 /*yield*/, this.timeToBlock(fromDate)]; case 1: _a = _c.sent(); return [3 /*break*/, 3]; case 2: _a = 0; _c.label = 3; case 3: fromBlock = _a; if (!toDate) return [3 /*break*/, 5]; return [4 /*yield*/, this.timeToBlock(toDate)]; case 4: _b = _c.sent(); return [3 /*break*/, 7]; case 5: return [4 /*yield*/, this._latestBlockNo]; case 6: _b = _c.sent(); _c.label = 7; case 7: toBlock = _b; this._event = this._contract.interface.events[eventName]; console.log("Start logging " + eventName + " events"); startTime = new Date(); if (!this._clearDB) return [3 /*break*/, 9]; console.log("Update cache..."); return [4 /*yield*/, this.getEvents(eventName, fromBlock, toBlock)]; case 8: _c.sent(); return [3 /*break*/, 11]; case 9: console.log("Reload from cache..."); return [4 /*yield*/, this.importCSV() .then(function () { return __awaiter(_this, void 0, void 0, function () { var latestInDB, earliestInDB, upperBlock, lowerBlock; return __generator(this, function (_a) { switch (_a.label) { case 0: console.log("Cached reloaded, updating..."); this._clearDB = false; return [4 /*yield*/, this._dbService.executeQuery({ query: "MATCH ()-[r]->() RETURN max(r.blockheight) as result" })]; case 1: latestInDB = _a.sent(); return [4 /*yield*/, this._dbService.executeQuery({ query: "MATCH ()-[r]->() RETURN min(r.blockheight) as result" })]; case 2: earliestInDB = _a.sent(); upperBlock = latestInDB ? parseInt(latestInDB[0].get("result")) : undefined; lowerBlock = earliestInDB ? parseInt(earliestInDB[0].get("result")) : undefined; if (!(lowerBlock && upperBlock)) return [3 /*break*/, 11]; if (!(fromBlock > upperBlock || toBlock < lowerBlock)) return [3 /*break*/, 4]; console.log("The required range is outside DB's range"); return [4 /*yield*/, this.getEvents(eventName, fromBlock, toBlock)]; case 3: _a.sent(); _a.label = 4; case 4: if (!(fromBlock >= lowerBlock && toBlock <= upperBlock)) return [3 /*break*/, 6]; console.log("The required range is inside DB's range"); return [4 /*yield*/, this.getEvents(eventName, fromBlock, toBlock)]; case 5: _a.sent(); _a.label = 6; case 6: if (!(fromBlock !== 0 && fromBlock < lowerBlock)) return [3 /*break*/, 8]; console.log("The required fromBlock is earlier than DB's earliest block"); return [4 /*yield*/, this.getEvents(eventName, fromBlock, lowerBlock)]; case 7: _a.sent(); _a.label = 8; case 8: if (!(toBlock > upperBlock)) return [3 /*break*/, 10]; console.log("The required toBlock is more recent than DB's latest block"); return [4 /*yield*/, this.getEvents(eventName, upperBlock, toBlock)]; case 9: _a.sent(); _a.label = 10; case 10: return [3 /*break*/, 12]; case 11: errors.throwError({ type: "ERROR_WATCHER_WATCHEVENTS" /* ERROR_WATCHER_WATCHEVENTS */, reason: "Could not find block range...", level: "warn", dump: { fromBlock: fromBlock, toBlock: toBlock } }); _a.label = 12; case 12: return [2 /*return*/]; } }); }); }) .catch(function (error) { return __awaiter(_this, void 0, void 0, function () { var _a; return __generator(this, function (_b) { switch (_b.label) { case 0: _a = error.name; switch (_a) { case "ERROR_WATCHER_WATCHEVENTS" /* ERROR_WATCHER_WATCHEVENTS */: return [3 /*break*/, 1]; } return [3 /*break*/, 3]; case 1: console.log("Cache not loaded or corrupted, updating..."); this._clearDB = true; return [4 /*yield*/, this.getEvents(eventName, fromBlock, toBlock).catch(function (error) { throw error; })]; case 2: _b.sent(); return [3 /*break*/, 4]; case 3: throw error; case 4: return [2 /*return*/]; } }); }); })]; case 10: _c.sent(); _c.label = 11; case 11: endTime = new Date(); elapsedMilli = endTime.getTime() - startTime.getTime(); elapsedSeconds = elapsedMilli / 1000; elapsedMinutes = elapsedSeconds / 60; roundMinutes = Math.trunc(elapsedMinutes); roundSeconds = Math.trunc(elapsedSeconds - roundMinutes * 60); roundMilis = Math.trunc(elapsedMilli - roundMinutes * 60000 - roundSeconds * 1000); console.log("Finished in " + roundMinutes + " mins " + roundSeconds + " s " + roundMilis + " ms"); return [2 /*return*/]; } }); }); }; /** * Convert timestamp to blocknumber * @param {Date} date */ EthereumWatcher.prototype.timeToBlock = function (date) { return __awaiter(this, void 0, void 0, function () { var latestBlockNo, upperBlock, lowerBlock, result; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this._provider.getBlockNumber()]; case 1: latestBlockNo = _a.sent(); return [4 /*yield*/, this._provider.getBlock(latestBlockNo)]; case 2: upperBlock = _a.sent(); this._latestBlockNo = latestBlockNo; this._latestTime = upperBlock.timestamp; if (!(this._blockTime === undefined)) return [3 /*break*/, 4]; return [4 /*yield*/, this._provider.getBlock(1)]; case 3: lowerBlock = _a.sent(); // How many time does it take to make one block (on average) in s this._blockTime = (upperBlock.timestamp - lowerBlock.timestamp) / (latestBlockNo - 1); _a.label = 4; case 4: result = latestBlockNo - Math.floor((upperBlock.timestamp - date.getTime() / 1000) / this._blockTime); // console.log(blockTime, result); return [2 /*return*/, result]; } }); }); }; /** * Tell the Watcher to clear the database before the next operation * @param clearFlag */ EthereumWatcher.prototype.setClearDBFlag = function (clearFlag) { this._clearDB = clearFlag; }; /** * Assemble a selection of data out of a log part * @param {ethers.providers.Log[]} logPart the extracted log part */ EthereumWatcher.prototype.getLogData = function (logPart) { return __awaiter(this, void 0, void 0, function () { var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: this.checkTimeout(); return [4 /*yield*/, Promise.all(logPart.map(function (logEntry) { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.extractData(logEntry)]; case 1: return [2 /*return*/, _a.sent()]; } }); }); }))]; case 1: return [2 /*return*/, _a.sent()]; } }); }); }; /** * Check for timeout */ EthereumWatcher.prototype.checkTimeout = function () { // Stops on timeout var elapsedTime = new Date().getTime() - this._startTime.getTime(); if (this._timeout > 0 && elapsedTime > this._timeout) { errors.throwError({ type: "ERROR_WATCHER_GETEVENTS" /* ERROR_WATCHER_GETEVENTS */, reason: "Timeout while getting events.", level: "error", dump: { elapsedTime: elapsedTime + " ms", timeoutLimit: this._timeout + " ms" } }); } }; /** * Get events from provider and store data to database * @param {string} eventName * @param {number} fromBlock * @param {number} toBlock */ EthereumWatcher.prototype.getEvents = function (eventName, fromBlock, toBlock) { return __awaiter(this, void 0, void 0, function () { var self, start, end, nbEvents, steps, eidss, temp; return __generator(this, function (_a) { switch (_a.label) { case 0: if (!this._clearDB) return [3 /*break*/, 2]; return [4 /*yield*/, this._dbService.dbClearAll()]; case 1: _a.sent(); _a.label = 2; case 2: self = this; start = fromBlock; end = toBlock; nbEvents = 0; steps = 0; this._startTime = new Date(); _a.label = 3; case 3: if (!(start !== end)) return [3 /*break*/, 8]; eidss = void 0; _a.label = 4; case 4: if (!(eidss === undefined)) return [3 /*break*/, 6]; return [4 /*yield*/, self.getEventPatch(eventName, start, end).catch(function (error) { // In case of ERROR_WATCHER_PROVIDER_GETLOGS, proceed to dichotomy if (error.name !== "ERROR_WATCHER_PROVIDER_GETLOGS" /* ERROR_WATCHER_PROVIDER_GETLOGS */) { throw error; } })]; case 5: temp = _a.sent(); if (temp) { eidss = temp; } else { // Lower the blockrange end = start + Math.floor((end - start) / 2); // Stops if too many events in 1 block if (start === end) { errors.throwError({ type: "ERROR_WATCHER_GETEVENTS" /* ERROR_WATCHER_GETEVENTS */, level: "error", reason: "Too many events in 1 block! ", }); } this.checkTimeout(); steps += 1; } return [3 /*break*/, 4]; case 6: return [4 /*yield*/, this.sendToDB(eidss)]; case 7: _a.sent(); start = (end === toBlock) ? end : end + 1; end = toBlock; steps += 1; nbEvents += eidss.length; return [3 /*break*/, 3]; case 8: console.log(nbEvents + " event(s) detected!", "required " + steps + " steps"); return [2 /*return*/]; } }); }); }; /** * Load the strategies to the watcher * @param strategies the user-defined strategy to extract and persists data */ EthereumWatcher.prototype.setStrategies = function (strategies) { var _this = this; this._strategies = strategies; this._nbContractCall = Object.keys(this._strategies.DataExtractionStrategy).filter(function (iteration) { var strategy = _this._strategies.DataExtractionStrategy[parseInt(iteration)].strategy; return strategy ? true : false; }).length; }; /** * Convert blockNo to timestamp * @param blockNo */ EthereumWatcher.prototype.blockToTime = function (blockNo) { var howManyBlocks = this._latestBlockNo - blockNo; var howManyTime = howManyBlocks * this._blockTime; return Math.ceil(this._latestTime - howManyTime); }; /** * Export to CSV */ EthereumWatcher.prototype.exportCSV = function () { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/, this._dbService.exportCSV(this._exportDir + this._contractAddr)]; }); }); }; /** * Import from CSV */ EthereumWatcher.prototype.importCSV = function () { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this._dbService.dbClearAll()]; case 1: _a.sent(); return [2 /*return*/, this._dbService.importCSV(this._contractAddr)]; } }); }); }; /** * Call the right provider for the contract * @param {ProviderEnum} providerType the provider type * @param {ProviderConfig} config the loaded config file * @returns {ethers.providers.BaseProvider} a provider */ EthereumWatcher.prototype.getProvider = function (providerType, config) { switch (providerType) { case 0 /* defaultProvider */: default: return ethers_1.ethers.getDefaultProvider(config.default.network); case 1 /* EtherscanProvider */: return new ethers_1.ethers.providers.EtherscanProvider(ethers_1.ethers.utils.getNetwork(config.etherscan.network), config.etherscan.api); case 2 /* InfuraProvider */: console.log("Infura"); return new ethers_1.ethers.providers.InfuraProvider(ethers_1.ethers.utils.getNetwork(config.infura.network), config.infura.projectId); case 3 /* JsonRpcProvider */: console.log("JSON-RPC"); var urlString = (config.jsonrpc.username === "") ? config.jsonrpc.url : { url: config.jsonrpc.url, user: config.jsonrpc.username, password: config.jsonrpc.password, allowInsecure: config.jsonrpc.allowInsecure }; return new ethers_1.ethers.providers.JsonRpcProvider(urlString, ethers_1.ethers.utils.getNetwork(config.jsonrpc.network)); case 4 /* Web3Provider */: return new ethers_1.ethers.providers.Web3Provider({ host: config.web3.host }); case 5 /* IpcProvider */: return new ethers_1.ethers.providers.IpcProvider(config.ipc.path, ethers_1.ethers.utils.getNetwork(config.ipc.network)); } }; /** * Persist processed data to the database * @param eidss the processed data */ EthereumWatcher.prototype.sendToDB = function (eidss) { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this._dbService.persistDataToDB(eidss, this._strategies.PersistenceStrategy)]; case 1: _a.sent(); return [4 /*yield*/, this.exportCSV()]; case 2: _a.sent(); console.log("Database updated!"); return [2 /*return*/]; } }); }); }; /** * Extract data from log entry * @param {ethers.providers.Log} logEntry a log entry * @returns {EventInfoDataStruct} the required information to build a db node */ EthereumWatcher.prototype.extractData = function (logEntry) { return __awaiter(this, void 0, void 0, function () { var data, DES, nbIterations, eids, _loop_1, this_1, i; return __generator(this, function (_a) { switch (_a.label) { case 0: this.checkTimeout(); data = this._event.decode(logEntry.data, logEntry.topics); DES = this._strategies.DataExtractionStrategy; nbIterations = Object.keys(DES).length; eids = {}; eids.blockheight = logEntry.blockNumber; eids.eventTime = neo4j_driver_1.v1.types.DateTime.fromStandardDate(new Date(this.blockToTime(eids.blockheight) * 1000)); _loop_1 = function (i) { var strategy, result, s, _a, args, r; return __generator(this, function (_b) { switch (_b.label) { case 0: strategy = Object.keys(DES[i])[1]; result = void 0; _a = strategy; switch (_a) { case "contractCall": return [3 /*break*/, 1]; case "fromData": return [3 /*break*/, 5]; } return [3 /*break*/, 8]; case 1: s = DES[i][strategy]; args = Object.keys(s.args).map(function (arg) { var argProcess = s.args[arg]; return argProcess ? argProcess(data[arg]) : data[arg]; }); return [4 /*yield*/, this_1.contractCall(s.funcName, args, s.ignoreError)]; case 2: r = _b.sent(); result = r ? r[s.resAttr] : r; if (!r) return [3 /*break*/, 4]; return [4 /*yield*/, this_1.processData(s, result)]; case 3: result = _b.sent(); _b.label = 4; case 4: return [3 /*break*/, 9]; case 5: s = DES[i][strategy]; result = data[s.attrName]; if (!result) return [3 /*break*/, 7]; return [4 /*yield*/, this_1.processData(s, result)]; case 6: result = _b.sent(); _b.label = 7; case 7: return [3 /*break*/, 9]; case 8: errors.throwError({ type: "ERROR_WATCHER_EXTRACT_DATA" /* ERROR_WATCHER_EXTRACT_DATA */, level: "error", reason: "Unable to find strategy for " + DES[i] + "." }); _b.label = 9; case 9: eids[DES[i].propName] = result; return [2 /*return*/]; } }); }; this_1 = this; i = 0; _a.label = 1; case 1: if (!(i < nbIterations)) return [3 /*break*/, 4]; return [5 /*yield**/, _loop_1(i)]; case 2: _a.sent(); _a.label = 3; case 3: i++; return [3 /*break*/, 1]; case 4: return [2 /*return*/, eids]; } }); }); }; /** * Call contract method * @param funcName method name * @param args method args */ EthereumWatcher.prototype.contractCall = function (funcName, args, ignoreError) { var contractFuncs = this._contract.functions; if (!Object.keys(contractFuncs).includes(funcName)) { errors.throwError({ type: "ERROR_WATCHER_CONTRACT_CALL" /* ERROR_WATCHER_CONTRACT_CALL */, level: "error", reason: "This contract does not contain function " + funcName }); } var func = this._contract.functions[funcName]; var process = func.apply(this._contract, args); return process.catch(function (error) { if (!ignoreError) { throw error; } }); }; /** * Apply process function after extracting data. By default, the data will be processed as String * @param strategy DES strategy * @param init unformatted/raw data */ EthereumWatcher.prototype.processData = function (strategy, init) { return __awaiter(this, void 0, void 0, function () { var process, contractFuncs, result; return __generator(this, function (_a) { switch (_a.label) { case 0: process = strategy.process || strategy_1.defaultDataProcess; contractFuncs = this._contract.functions; return [4 /*yield*/, process(init, contractFuncs) .catch(function (reason) { errors.throwError({ type: "ERROR_WATCHER_PROCESS_DATA" /* ERROR_WATCHER_PROCESS_DATA */, reason: "Could not apply Process function. " + reason.message, level: "error", dump: { usingStrategy: strategy, initValue: init } }); })]; case 1: result = _a.sent(); return [2 /*return*/, result]; } }); }); }; /** * Get the smaller batch of events * @param {string} eventName * @param {number} fromBlock * @param {number} toBlock * @returns {Promise<ethers.providers.Log[]>} */ EthereumWatcher.prototype.getEventPatch = function (eventName, fromBlock, toBlock) { return __awaiter(this, void 0, void 0, function () { var logs, eidss; return __generator(this, function (_a) { switch (_a.label) { case 0: this.checkTimeout(); console.log("Getting events '" + eventName + "' from block #" + fromBlock + " to block #" + toBlock); return [4 /*yield*/, this._provider.getLogs({ fromBlock: fromBlock, toBlock: toBlock, address: this._contractAddr, topics: [this._event.topic] }).catch(function (reason) { errors.throwError({ type: "ERROR_WATCHER_PROVIDER_GETLOGS" /* ERROR_WATCHER_PROVIDER_GETLOGS */, level: "warn", reason: "Provider could not get logs. This is due to the provider's restriction on data." + reason.message }); })]; case 1: logs = _a.sent(); if (!logs) return [3 /*break*/, 4]; if (!(logs.length >= this._logSizePerOp * this._nbContractCall)) return [3 /*break*/, 2]; errors.throwError({ type: "ERROR_WATCHER_PROVIDER_GETLOGS" /* ERROR_WATCHER_PROVIDER_GETLOGS */, level: "warn", reason: "Exceeded the amount of logSize permitted per operation. Consider lower the 'logSizePerOp' in the config.", dump: { logLength: logs.length, maxLength: this._logSizePerOp } }); return [3 /*break*/, 4]; case 2: return [4 /*yield*/, this.getLogData(logs) .catch(function (reason) { errors.throwError({ type: "ERROR_WATCHER_GETLOGDATA" /* ERROR_WATCHER_GETLOGDATA */, level: "error", reason: "Could not get the log data" + reason.message, dump: { fromBlock: fromBlock, toBlock: toBlock } }); })]; case 3: eidss = _a.sent(); return [2 /*return*/, eidss ? eidss : undefined]; case 4: return [2 /*return*/, undefined]; } }); }); }; return EthereumWatcher; }(Watcher_1.Watcher)); exports.EthereumWatcher = EthereumWatcher; exports.default = EthereumWatcher;