Add ETH RPC API to watcher server (#535)

* Add ETH RPC API to watcher server

* Add eth_call API handler

* Add error handling to eth_call handler

* Parse block tag in eth_call handler

* Add a flag to enable ETH RPC server

* Fix lint errors

* Update block tag parsing
This commit is contained in:
prathamesh0 2024-09-13 12:44:00 +05:30 committed by GitHub
parent ea5ff93e21
commit b46d8816b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 241 additions and 6 deletions

View File

@ -32,7 +32,8 @@ import {
readParty,
UpstreamConfig,
fillBlocks,
createGQLLogger
createGQLLogger,
createEthRPCHandlers
} from '@cerc-io/util';
import { TypeSource } from '@graphql-tools/utils';
import type {
@ -285,6 +286,7 @@ export class ServerCmd {
const jobQueue = this._baseCmd.jobQueue;
const indexer = this._baseCmd.indexer;
const eventWatcher = this._baseCmd.eventWatcher;
const ethProvider = this._baseCmd.ethProvider;
assert(config);
assert(jobQueue);
@ -317,9 +319,11 @@ export class ServerCmd {
const gqlLogger = createGQLLogger(config.server.gql.logDir);
const resolvers = await createResolvers(indexer, eventWatcher, gqlLogger);
const ethRPCHandlers = await createEthRPCHandlers(indexer, ethProvider);
// Create an Express app
const app: Application = express();
const server = await createAndStartServer(app, typeDefs, resolvers, config.server, paymentsManager);
const server = await createAndStartServer(app, typeDefs, resolvers, ethRPCHandlers, config.server, paymentsManager);
await startGQLMetricsServer(config);

View File

@ -2,7 +2,7 @@
import assert from 'assert';
import { DeepPartial, FindConditions, FindManyOptions } from 'typeorm';
import { providers } from 'ethers';
import { ethers } from 'ethers';
import {
IndexerInterface,
@ -28,6 +28,8 @@ import { GetStorageAt, getStorageValue, MappingKey, StorageLayout } from '@cerc-
export class Indexer implements IndexerInterface {
_getStorageAt: GetStorageAt;
_storageLayoutMap: Map<string, StorageLayout> = new Map();
_contractMap: Map<string, ethers.utils.Interface> = new Map();
eventSignaturesMap: Map<string, string[]> = new Map();
constructor (ethClient: EthClient, storageLayoutMap?: Map<string, StorageLayout>) {
@ -50,6 +52,10 @@ export class Indexer implements IndexerInterface {
return this._storageLayoutMap;
}
get contractMap (): Map<string, ethers.utils.Interface> {
return this._contractMap;
}
async init (): Promise<void> {
return undefined;
}

View File

@ -36,6 +36,7 @@
"it-length-prefixed": "^8.0.4",
"it-pipe": "^2.0.5",
"it-pushable": "^3.1.2",
"jayson": "^4.1.2",
"js-yaml": "^4.1.0",
"json-bigint": "^1.0.0",
"lodash": "^4.17.21",

View File

@ -252,6 +252,9 @@ export interface ServerConfig {
// Flag to specify whether RPC endpoint supports block hash as block tag parameter
// https://ethereum.org/en/developers/docs/apis/json-rpc/#default-block
rpcSupportsBlockHashParam: boolean;
// Enable ETH JSON RPC server at /rpc
enableEthRPCServer: boolean;
}
export interface FundingAmountsConfig {

View File

@ -0,0 +1,134 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { utils } from 'ethers';
import { JsonRpcProvider } from '@ethersproject/providers';
import { IndexerInterface } from './types';
const CODE_INVALID_PARAMS = -32602;
const CODE_INTERNAL_ERROR = -32603;
const CODE_SERVER_ERROR = -32000;
const ERROR_CONTRACT_MAP_NOT_SET = 'Contract map not set';
const ERROR_CONTRACT_ABI_NOT_FOUND = 'Contract ABI not found';
const ERROR_CONTRACT_INSUFFICIENT_PARAMS = 'Insufficient params';
const ERROR_CONTRACT_NOT_RECOGNIZED = 'Contract not recognized';
const ERROR_CONTRACT_METHOD_NOT_FOUND = 'Contract method not found';
const ERROR_METHOD_NOT_IMPLEMENTED = 'Method not implemented';
const ERROR_INVALID_BLOCK_TAG = 'Invalid block tag';
const ERROR_BLOCK_NOT_FOUND = 'Block not found';
const DEFAULT_BLOCK_TAG = 'latest';
class ErrorWithCode extends Error {
code: number;
constructor (code: number, message: string) {
super(message);
this.code = code;
}
}
export const createEthRPCHandlers = async (
indexer: IndexerInterface,
ethProvider: JsonRpcProvider
): Promise<any> => {
return {
eth_blockNumber: async (args: any, callback: any) => {
const syncStatus = await indexer.getSyncStatus();
const result = syncStatus ? `0x${syncStatus.latestProcessedBlockNumber.toString(16)}` : '0x';
callback(null, result);
},
eth_call: async (args: any, callback: any) => {
try {
if (!indexer.contractMap) {
throw new ErrorWithCode(CODE_INTERNAL_ERROR, ERROR_CONTRACT_MAP_NOT_SET);
}
if (args.length === 0) {
throw new ErrorWithCode(CODE_INVALID_PARAMS, ERROR_CONTRACT_INSUFFICIENT_PARAMS);
}
const { to, data } = args[0];
const blockTag = args.length > 1 ? args[1] : DEFAULT_BLOCK_TAG;
const blockHash = await parseBlockTag(indexer, ethProvider, blockTag);
const watchedContract = indexer.getWatchedContracts().find(contract => contract.address === to);
if (!watchedContract) {
throw new ErrorWithCode(CODE_INVALID_PARAMS, ERROR_CONTRACT_NOT_RECOGNIZED);
}
const contractInterface = indexer.contractMap.get(watchedContract.kind);
if (!contractInterface) {
throw new ErrorWithCode(CODE_INTERNAL_ERROR, ERROR_CONTRACT_ABI_NOT_FOUND);
}
// Slice out method signature from data
const functionSelector = data.slice(0, 10);
// Find the matching function from the ABI
const functionFragment = contractInterface.getFunction(functionSelector);
if (!functionFragment) {
throw new ErrorWithCode(CODE_INVALID_PARAMS, ERROR_CONTRACT_METHOD_NOT_FOUND);
}
// Decode the data based on the matched function
const decodedData = contractInterface.decodeFunctionData(functionFragment, data);
const functionName = functionFragment.name;
const indexerMethod = (indexer as any)[functionName].bind(indexer);
if (!indexerMethod) {
throw new ErrorWithCode(CODE_SERVER_ERROR, ERROR_METHOD_NOT_IMPLEMENTED);
}
const result = await indexerMethod(blockHash, to, ...decodedData);
const encodedResult = contractInterface.encodeFunctionResult(functionFragment, Array.isArray(result.value) ? result.value : [result.value]);
callback(null, encodedResult);
} catch (error: any) {
let callBackError;
if (error instanceof ErrorWithCode) {
callBackError = { code: error.code, message: error.message };
} else {
callBackError = { code: CODE_SERVER_ERROR, message: error.message };
}
callback(callBackError);
}
},
eth_getLogs: async (args: any, callback: any) => {
// TODO: Implement
}
};
};
const parseBlockTag = async (indexer: IndexerInterface, ethProvider: JsonRpcProvider, blockTag: string): Promise<string> => {
if (utils.isHexString(blockTag)) {
// Return value if hex string is of block hash length
if (utils.hexDataLength(blockTag) === 32) {
return blockTag;
}
// Treat hex value as a block number
const block = await ethProvider.getBlock(blockTag);
if (block === null) {
throw new ErrorWithCode(CODE_INVALID_PARAMS, ERROR_BLOCK_NOT_FOUND);
}
return block.hash;
}
if (blockTag === DEFAULT_BLOCK_TAG) {
const syncStatus = await indexer.getSyncStatus();
if (!syncStatus) {
throw new ErrorWithCode(CODE_INTERNAL_ERROR, 'SyncStatus not found');
}
return syncStatus.latestProcessedBlockHash;
}
throw new ErrorWithCode(CODE_INVALID_PARAMS, ERROR_INVALID_BLOCK_TAG);
};

View File

@ -28,3 +28,4 @@ export * from './eth';
export * from './consensus';
export * from './validate-config';
export * from './logger';
export * from './eth-rpc-handlers';

View File

@ -11,6 +11,8 @@ import debug from 'debug';
import responseCachePlugin from 'apollo-server-plugin-response-cache';
import { InMemoryLRUCache } from '@apollo/utils.keyvaluecache';
import queue from 'express-queue';
import jayson from 'jayson';
import { json as jsonParser } from 'body-parser';
import { TypeSource } from '@graphql-tools/utils';
import { makeExecutableSchema } from '@graphql-tools/schema';
@ -22,11 +24,13 @@ import { PaymentsManager, paymentsPlugin } from './payments';
const log = debug('vulcanize:server');
const DEFAULT_GQL_PATH = '/graphql';
const ETH_RPC_PATH = '/rpc';
export const createAndStartServer = async (
app: Application,
typeDefs: TypeSource,
resolvers: any,
ethRPCHandlers: any,
serverConfig: ServerConfig,
paymentsManager?: PaymentsManager
): Promise<ApolloServer> => {
@ -98,8 +102,25 @@ export const createAndStartServer = async (
path: gqlPath
});
if (serverConfig.enableEthRPCServer) {
// Create a JSON-RPC server to handle ETH RPC calls
const rpcServer = jayson.Server(ethRPCHandlers);
// Mount the JSON-RPC server to ETH_RPC_PATH
app.use(
ETH_RPC_PATH,
jsonParser(),
// TODO: Handle GET requests as well to match Geth's behaviour
rpcServer.middleware()
);
}
httpServer.listen(port, host, () => {
log(`Server is listening on ${host}:${port}${server.graphqlPath}`);
log(`GQL server is listening on http://${host}:${port}${server.graphqlPath}`);
if (serverConfig.enableEthRPCServer) {
log(`ETH JSON RPC server is listening on http://${host}:${port}${ETH_RPC_PATH}`);
}
});
return server;

View File

@ -3,7 +3,7 @@
//
import { Connection, DeepPartial, EntityTarget, FindConditions, FindManyOptions, ObjectLiteral, QueryRunner } from 'typeorm';
import { Transaction } from 'ethers';
import { ethers, Transaction } from 'ethers';
import { MappingKey, StorageLayout } from '@cerc-io/solidity-mapper';
@ -161,6 +161,7 @@ export interface IndexerInterface {
readonly serverConfig: ServerConfig
readonly upstreamConfig: UpstreamConfig
readonly storageLayoutMap: Map<string, StorageLayout>
readonly contractMap: Map<string, ethers.utils.Interface>
// eslint-disable-next-line no-use-before-define
readonly graphWatcher?: GraphWatcherInterface
init (): Promise<void>

View File

@ -3635,6 +3635,13 @@
dependencies:
"@types/node" "*"
"@types/connect@^3.4.33":
version "3.4.38"
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858"
integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==
dependencies:
"@types/node" "*"
"@types/cors@2.8.12":
version "2.8.12"
resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080"
@ -3848,6 +3855,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b"
integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==
"@types/node@^12.12.54":
version "12.20.55"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240"
integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==
"@types/node@^12.12.6":
version "12.20.13"
resolved "https://registry.npmjs.org/@types/node/-/node-12.20.13.tgz"
@ -3954,6 +3966,13 @@
resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.5.tgz#74fef9ffbaa198eb8b588be029f38b00299caa2c"
integrity sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==
"@types/ws@^7.4.4":
version "7.4.7"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702"
integrity sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==
dependencies:
"@types/node" "*"
"@types/ws@^8.5.3":
version "8.5.4"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.4.tgz#bb10e36116d6e570dd943735f86c933c1587b8a5"
@ -4151,7 +4170,7 @@
resolved "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz"
integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==
JSONStream@^1.0.4:
JSONStream@^1.0.4, JSONStream@^1.3.5:
version "1.3.5"
resolved "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz"
integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==
@ -7398,6 +7417,18 @@ es6-object-assign@^1.1.0:
resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.1.0.tgz#c2c3582656247c39ea107cb1e6652b6f9f24523c"
integrity sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==
es6-promise@^4.0.3:
version "4.2.8"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
es6-promisify@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203"
integrity sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==
dependencies:
es6-promise "^4.0.3"
es6-symbol@^3.1.1, es6-symbol@~3.1.3:
version "3.1.3"
resolved "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz"
@ -8293,6 +8324,11 @@ extsprintf@^1.2.0:
resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.0.tgz"
integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
eyes@^0.1.8:
version "0.1.8"
resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0"
integrity sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==
fake-merkle-patricia-tree@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/fake-merkle-patricia-tree/-/fake-merkle-patricia-tree-1.0.1.tgz"
@ -10422,6 +10458,11 @@ isobject@^3.0.0, isobject@^3.0.1:
resolved "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz"
integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
isomorphic-ws@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc"
integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==
isstream@~0.1.2:
version "0.1.2"
resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz"
@ -10651,6 +10692,24 @@ jackspeak@^2.3.5:
optionalDependencies:
"@pkgjs/parseargs" "^0.11.0"
jayson@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/jayson/-/jayson-4.1.2.tgz#443c26a8658703e0b2e881117b09395d88b6982e"
integrity sha512-5nzMWDHy6f+koZOuYsArh2AXs73NfWYVlFyJJuCedr93GpY+Ku8qq10ropSXVfHK+H0T6paA88ww+/dV+1fBNA==
dependencies:
"@types/connect" "^3.4.33"
"@types/node" "^12.12.54"
"@types/ws" "^7.4.4"
JSONStream "^1.3.5"
commander "^2.20.3"
delay "^5.0.0"
es6-promisify "^5.0.0"
eyes "^0.1.8"
isomorphic-ws "^4.0.1"
json-stringify-safe "^5.0.1"
uuid "^8.3.2"
ws "^7.5.10"
js-sdsl@^4.1.4:
version "4.4.0"
resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.4.0.tgz#8b437dbe642daa95760400b602378ed8ffea8430"
@ -17029,6 +17088,11 @@ ws@^7.4.6:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
ws@^7.5.10:
version "7.5.10"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9"
integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
ws@^8.11.0, ws@^8.12.1, ws@^8.4.0:
version "8.13.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"