// // Copyright 2021 Vulcanize, Inc. // import assert from 'assert'; import { Cache } from '@cerc-io/cache'; import { EthClient as EthClientInterface, EthFullTransaction } from '@cerc-io/util'; import ethQueries from './eth-queries'; import { padKey } from './utils'; import { GraphQLClient, GraphQLConfig } from './graphql-client'; export interface Config extends GraphQLConfig { cache: Cache | undefined; } interface Vars { blockHash: string; blockNumber?: string; contract?: string; slot?: string; addresses?: string[]; } export class EthClient implements EthClientInterface { _graphqlClient: GraphQLClient; _cache: Cache | undefined; constructor (config: Config) { const { gqlEndpoint, gqlSubscriptionEndpoint, cache } = config; assert(gqlEndpoint, 'Missing gql endpoint'); this._graphqlClient = new GraphQLClient({ gqlEndpoint, gqlSubscriptionEndpoint }); this._cache = cache; } async getStorageAt ({ blockHash, contract, slot }: { blockHash: string, contract: string, slot: string }): Promise<{ value: string, proof: { data: string } }> { slot = `0x${padKey(slot)}`; console.time(`time:eth-client#getStorageAt-${JSON.stringify({ blockHash, contract, slot })}`); const result = await this._getCachedOrFetch('getStorageAt', { blockHash, contract, slot }); console.timeEnd(`time:eth-client#getStorageAt-${JSON.stringify({ blockHash, contract, slot })}`); const { getStorageAt: { value, cid, ipldBlock } } = result; return { value, proof: { // TODO: Return proof only if requested. data: JSON.stringify({ blockHash, account: { address: contract, storage: { cid, ipldBlock } } }) } }; } async getBlockWithTransactions ({ blockNumber, blockHash }: { blockNumber?: number, blockHash?: string }): Promise { console.time(`time:eth-client#getBlockWithTransactions-${JSON.stringify({ blockNumber, blockHash })}`); const result = await this._graphqlClient.query( ethQueries.getBlockWithTransactions, { blockNumber: blockNumber?.toString(), blockHash } ); console.timeEnd(`time:eth-client#getBlockWithTransactions-${JSON.stringify({ blockNumber, blockHash })}`); return result; } async getBlocks ({ blockNumber, blockHash }: { blockNumber?: number, blockHash?: string }): Promise { console.time(`time:eth-client#getBlocks-${JSON.stringify({ blockNumber, blockHash })}`); const result = await this._graphqlClient.query( ethQueries.getBlocks, { blockNumber: blockNumber?.toString(), blockHash } ); console.timeEnd(`time:eth-client#getBlocks-${JSON.stringify({ blockNumber, blockHash })}`); return result; } async getFullBlocks ({ blockNumber, blockHash }: { blockNumber?: number, blockHash?: string }): Promise { console.time(`time:eth-client#getFullBlocks-${JSON.stringify({ blockNumber, blockHash })}`); const { allEthHeaderCids } = await this._graphqlClient.query( ethQueries.getFullBlocks, { blockNumber: blockNumber?.toString(), blockHash } ); console.timeEnd(`time:eth-client#getFullBlocks-${JSON.stringify({ blockNumber, blockHash })}`); return this.addBlockSize(allEthHeaderCids.nodes); } // TODO: Get block size from ipld-eth-server private addBlockSize (nodes: any[]): any[] { // Populate block size with 0 value return nodes.map(obj => { return { ...obj, size: '0' }; }); } async getFullTransaction (txHash: string, blockNumber?: number): Promise { console.time(`time:eth-client#getFullTransaction-${JSON.stringify({ txHash, blockNumber })}`); const result = await this._graphqlClient.query( ethQueries.getFullTransaction, { txHash, blockNumber: blockNumber?.toString() } ); console.timeEnd(`time:eth-client#getFullTransaction-${JSON.stringify({ txHash, blockNumber })}`); return result; } async getBlockByHash (blockHash?: string): Promise { console.time(`time:eth-client#getBlockByHash-${blockHash}`); const result = await this._graphqlClient.query(ethQueries.getBlockByHash, { blockHash }); console.timeEnd(`time:eth-client#getBlockByHash-${blockHash}`); return { block: { ...result.block, number: parseInt(result.block.number, 16), timestamp: parseInt(result.block.timestamp, 16) } }; } // TODO: Support filtering logs using topics async getLogs (vars: Vars): Promise { console.time(`time:eth-client#getLogs-${JSON.stringify(vars)}`); const result = await this._getCachedOrFetch('getLogs', vars); console.timeEnd(`time:eth-client#getLogs-${JSON.stringify(vars)}`); const { getLogs } = result; return { logs: getLogs }; } // TODO: Implement // async getLogsForBlockRange(): Promise {} async _getCachedOrFetch (queryName: keyof typeof ethQueries, vars: Vars): Promise { const keyObj = { queryName, vars }; // Check if request cached in db, if cache is enabled. if (this._cache) { const [value, found] = await this._cache.get(keyObj) || [undefined, false]; if (found) { return value; } } // Result not cached or cache disabled, need to perform an upstream GQL query. const result = await this._graphqlClient.query(ethQueries[queryName], vars); // Cache the result and return it, if cache is enabled. if (this._cache) { await this._cache.put(keyObj, result); } return result; } }