noob-ethereum
Version:
A simple Ethereum library
354 lines (353 loc) • 15.9 kB
JavaScript
;
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 (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__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.Provider = void 0;
const range_1 = __importDefault(require("lodash/range"));
const http_1 = require("../../abstract/http");
const dotenv = __importStar(require("dotenv"));
dotenv.config();
const export_1 = require("../utils/export");
const constants_1 = require("../../constants");
const index_1 = require("../../index");
const config = {
headers: {
'Content-Type': 'application/json',
},
};
class Provider extends http_1.HttpClient {
constructor(url) {
super(url);
this.config = config;
}
static init(url) {
if (!this.instance) {
this.instance = new Provider(url);
}
return this.instance;
}
/**
* Fetch raw block by number via JSON-RPC
* @param {string} blockNumber - 12396599
* @param {boolean} verbose - true
* @returns {Promise<IRawBlock>}
*/
async getBlockByNumber(blockNumber, verbose = false) {
var _a;
// @ts-ignore
if (isNaN(blockNumber) || blockNumber === '') {
throw new Error('User supplied invalid string as block number');
}
if (+blockNumber < 0) {
throw new Error('User supplied block number that does not exist');
}
const res = await this.instance.post('', {
jsonrpc: '2.0',
method: 'eth_getBlockByNumber',
params: [index_1.utils.hexify(blockNumber), verbose],
id: 0,
}, this.config);
if ((_a = res.data.error) === null || _a === void 0 ? void 0 : _a.code) {
throw new Error(res.data.error.code);
}
const { result } = res.data;
return result;
}
async getLogs(address, topics, blockHash) {
var _a;
const res = await this.instance.post('', {
jsonrpc: '2.0',
method: 'eth_getLogs',
params: [
{
address,
topics,
blockHash,
},
],
id: 0,
}, this.config);
if ((_a = res.data.error) === null || _a === void 0 ? void 0 : _a.code) {
throw new Error(res.data.error.code);
}
const { result } = res.data;
return result;
}
/**
* Fetch latest raw block via JSON-RPC
* @param {boolean} verbose - true
* @returns {Promise<IRawBlock>}
*/
async getLatestBlock(verbose = false) {
const res = await this.instance.post('', {
jsonrpc: '2.0',
method: 'eth_getBlockByNumber',
params: ['latest', verbose],
id: 0,
}, this.config);
const { result } = res.data;
return result;
}
/**
* Fetch transaction receipt by transaction hash
* @param {string} hash - transaction hash
* @returns {Promise<IRawBlock>}
*/
async getTransactionReceipt(hash) {
var _a;
const res = await this.instance.post('', {
jsonrpc: '2.0',
method: 'eth_getTransactionReceipt',
params: [hash],
id: 1,
}, this.config);
if ((_a = res.data.error) === null || _a === void 0 ? void 0 : _a.code) {
throw new Error(res.data.error.code);
}
const { result } = res.data;
return result;
}
/**
* Generate JSON file of latest block
* @param {boolean} verbose - flag to specify fetching full tx objects or just their hashes
* @param {string} path - optional parameter to specify path from project root where to save JSON file
*/
async seedLatestBlock(verbose = false, path = 'src/seeder/blocks/1559') {
const block = await this.getLatestBlock(verbose);
const blockNumber = parseInt(block.number, 16);
(0, export_1.exportToJSONFile)(block, blockNumber.toString(), path);
}
/**
* Generate JSON file for a specific block
* @param {number} num - decimal number of a block
* @param {boolean} verbose - flag to specify fetching full tx objects or just their hashes
* @param {string} path - optional parameter to specify path from project root where to save JSON file
*/
async seedBlockByNumber(verbose = false, num, path) {
if (!num)
throw new Error('No block number specified');
const block = await this.getBlockByNumber(num, verbose);
if (!path) {
path = num >= constants_1.LONDON_HARDFORK_BLOCK ? 'src/seeder/blocks/1559' : 'src/seeder/blocks/legacy';
}
(0, export_1.exportToJSONFile)(block, num.toString(), path);
}
async _prepareBlockRangeQuery(starting, total) {
const currentHead = index_1.utils.decimal((await this.getLatestBlock(false)).number);
const startBlock = index_1.utils.decimal((await this.getBlockByNumber(starting, true)).number);
if (total === 'latest') {
return (0, range_1.default)(startBlock, currentHead, 1);
}
if (startBlock + total > currentHead) {
throw new Error('Range provided includes blocks that have not been added to the chain yet!');
}
return (0, range_1.default)(startBlock, startBlock + total, 1);
}
async _fetchFullTransactionBodies(startingBlock, total) {
const blockNumberArr = await this._prepareBlockRangeQuery(startingBlock, total);
const txHashArr = [];
const fetchBlockClosure = async (n, i) => {
const { transactions } = await this.getBlockByNumber(n, true);
txHashArr[i] = [];
transactions.map((t) => {
txHashArr[i].push(t.toString());
});
};
await Promise.all(blockNumberArr.map((n, i) => fetchBlockClosure(n, i)));
return txHashArr;
}
/* Fetch array block transactions in tuple form over a range of blocks (tuple includes stringified array of block number, transaction index, transaction hash) */
async _fetchTransactionsOverBlockRange(startingBlock, total) {
const blockNumberArr = await this._prepareBlockRangeQuery(startingBlock, total);
const txHashArr = [];
const fetchBlockClosure = async (n, i) => {
const { transactions } = await this.getBlockByNumber(n, true);
txHashArr[i] = [];
transactions.map((t) => {
txHashArr[i].push([n, index_1.utils.decimal(t.transactionIndex), t.hash].toString());
});
};
await Promise.all(blockNumberArr.map((n, i) => fetchBlockClosure(n, i)));
return txHashArr;
}
/* Go over a specified number of blocks are return transaction tuples that match the from and to transacction fields */
async _fetchTransactionsOverBlocksByInteraction(startingBlock, total, from, to) {
console.log('block:', startingBlock, 'total:', total);
const blockNumberArr = await this._prepareBlockRangeQuery(startingBlock, total);
let txHashArr = [];
const fetchBlockClosure = async (n, i) => {
const { transactions } = await this.getBlockByNumber(n, true);
txHashArr[i] = [];
transactions.map((t) => {
if (t.from === from && t.to === to) {
txHashArr[i].push([n, index_1.utils.decimal(t.transactionIndex), t.hash].toString());
}
});
};
await Promise.all(blockNumberArr.map((n, i) => fetchBlockClosure(n, i)));
// Filter all the blocks that did not have those interactions
txHashArr = txHashArr.filter((arr) => arr.length !== 0);
return txHashArr;
}
// Process and handle millions of requests for ALL block transactions
async fetchTransactionsOverBlockRange(startingBlock, total, limit) {
const CONCURRENT_LIMIT = limit;
const start = Date.now();
let result = [];
let params = await this._prepareBlockRangeQuery(startingBlock, total);
const blockTotal = params.length;
let finalGroup = [];
let progress = 0;
const concGroupSize = Math.floor(params.length / CONCURRENT_LIMIT);
if (params.length % CONCURRENT_LIMIT !== 0) {
const sliceParam = -params.length % CONCURRENT_LIMIT;
finalGroup = [...params.slice(sliceParam)];
params = [...params.slice(0, sliceParam)];
console.log('params.length:', params.length);
console.log('finalGroup.length:', finalGroup.length);
}
for (let i = 0; i < params.length; i += CONCURRENT_LIMIT) {
const arr = await this._fetchTransactionsOverBlockRange(startingBlock + i, CONCURRENT_LIMIT);
result = result.concat(arr);
progress += CONCURRENT_LIMIT;
console.log('Blocks downloaded:', `${progress}/${blockTotal}`, '| Progress:', ((100 * progress) / blockTotal).toFixed(1) + '%' + ' | ' + 'elapsed time: ', index_1.utils.minutes(Date.now() - start));
}
if (finalGroup.length > 0) {
const arr = await this._fetchTransactionsOverBlockRange(finalGroup[0], finalGroup.length);
result = result.concat(arr);
progress += finalGroup.length;
console.log('Blocks downloaded:', `${progress}/${blockTotal}`, '| Progress:', ((100 * progress) / blockTotal).toFixed(1) + '%' + ' | ' + 'elapsed time: ', index_1.utils.minutes(Date.now() - start));
}
console.log('group length:', concGroupSize);
console.log('final group length:', finalGroup.length);
return result;
}
async fetchTransactionsOverBlocksByInteraction(startingBlock, total, limit, from, to) {
const CONCURRENT_LIMIT = limit;
const start = Date.now();
let result = [];
let params = await this._prepareBlockRangeQuery(startingBlock, total);
const blockTotal = params.length;
let finalGroup = [];
let progress = 0;
const concGroupSize = Math.floor(params.length / CONCURRENT_LIMIT);
if (params.length % CONCURRENT_LIMIT !== 0) {
const sliceParam = -params.length % CONCURRENT_LIMIT;
finalGroup = [...params.slice(sliceParam)];
params = [...params.slice(0, sliceParam)];
console.log('params.length:', params.length);
console.log('finalGroup.length:', finalGroup.length);
}
for (let i = 0; i < params.length; i += CONCURRENT_LIMIT) {
const arr = await this._fetchTransactionsOverBlocksByInteraction(startingBlock + i, CONCURRENT_LIMIT, from, to);
result = result.concat(arr);
progress += CONCURRENT_LIMIT;
console.log('Blocks downloaded:', `${progress}/${blockTotal}`, '| Progress:', ((100 * progress) / blockTotal).toFixed(1) + '%' + ' | ' + 'elapsed time: ', index_1.utils.minutes(Date.now() - start));
}
if (finalGroup.length > 0) {
const arr = await this._fetchTransactionsOverBlocksByInteraction(finalGroup[0], finalGroup.length, from, to);
result = result.concat(arr);
progress += finalGroup.length;
console.log('Blocks downloaded:', `${progress}/${blockTotal}`, '| Progress:', ((100 * progress) / blockTotal).toFixed(1) + '%' + ' | ' + 'elapsed time: ', index_1.utils.minutes(Date.now() - start));
}
console.log('group length:', concGroupSize);
console.log('final group length:', finalGroup.length);
return result;
}
async fetchMultipleRequests(paramsArr) {
const result = [];
const requestClosure = async (hash, i) => {
const res = await this.getTransactionReceipt(hash);
const standardized = this._standardizeTransactionReceipt(res);
result[i] = [];
result[i].push(standardized);
};
await Promise.all(paramsArr.map((hash, i) => requestClosure(hash, i)));
return result;
}
async fetchBatchReceipts(batchArr, limit) {
let result = [];
const CONCURRENT_LIMIT = limit;
const start = Date.now();
const total = batchArr.length;
let arr = batchArr.flat(Infinity).map((x) => {
const tuple = x.split(',');
return tuple[tuple.length - 1];
});
let progress = 0;
let finalRequestBatch = [];
if (arr.length % CONCURRENT_LIMIT !== 0) {
const param = -arr.length % CONCURRENT_LIMIT;
finalRequestBatch = [...arr.slice(param)];
arr = [...arr.slice(0, param)];
console.log(param);
console.log(CONCURRENT_LIMIT);
console.log('params.length:', arr.length);
console.log('finalGroup.length:', finalRequestBatch.length);
}
for (let i = 0; i < arr.length; i += CONCURRENT_LIMIT) {
const res = await this.fetchMultipleRequests(arr.slice(i, i + CONCURRENT_LIMIT));
result = [
...result,
...res.flat(2).map((obj, i) => {
obj.batchNumber = progress + i + 1;
return obj;
}),
];
progress += CONCURRENT_LIMIT;
console.log('Receipts downloaded:', `${progress}/${total}`, '| Progress:', ((100 * progress) / total).toFixed(1) + '%' + ' | ' + 'elapsed time: ', index_1.utils.minutes(Date.now() - start));
}
if (finalRequestBatch.length > 0) {
const res = await this.fetchMultipleRequests(finalRequestBatch);
result = [
...result,
...res.flat(2).map((obj, i) => {
obj.batchNumber = progress + i + 1;
return obj;
}),
];
progress += finalRequestBatch.length;
console.log('Receipts downloaded:', `${progress}/${total}`, '| Progress:', ((100 * progress) / total).toFixed(1) + '%' + ' | ' + 'elapsed time: ', index_1.utils.minutes(Date.now() - start));
}
console.log('batches scraped:', total);
console.log('group length:', CONCURRENT_LIMIT);
console.log('final group length:', finalRequestBatch.length);
return result;
}
_standardizeTransactionReceipt(receipt) {
const effectiveGasPrice = index_1.utils.toGwei(receipt.effectiveGasPrice, 'wei');
const gasUsed = parseInt(receipt.gasUsed, 16);
return {
transactionHash: receipt.transactionHash,
blockNumber: parseInt(receipt.blockNumber, 16),
effectiveGasPrice,
gasUsed,
publicationCost: index_1.utils.gweiToEther(gasUsed * effectiveGasPrice),
};
}
}
exports.Provider = Provider;