Skip to content

Commit b46d881

Browse files
authored
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
1 parent ea5ff93 commit b46d881

File tree

9 files changed

+241
-6
lines changed

9 files changed

+241
-6
lines changed

packages/cli/src/server.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ import {
3232
readParty,
3333
UpstreamConfig,
3434
fillBlocks,
35-
createGQLLogger
35+
createGQLLogger,
36+
createEthRPCHandlers
3637
} from '@cerc-io/util';
3738
import { TypeSource } from '@graphql-tools/utils';
3839
import type {
@@ -285,6 +286,7 @@ export class ServerCmd {
285286
const jobQueue = this._baseCmd.jobQueue;
286287
const indexer = this._baseCmd.indexer;
287288
const eventWatcher = this._baseCmd.eventWatcher;
289+
const ethProvider = this._baseCmd.ethProvider;
288290

289291
assert(config);
290292
assert(jobQueue);
@@ -317,9 +319,11 @@ export class ServerCmd {
317319
const gqlLogger = createGQLLogger(config.server.gql.logDir);
318320
const resolvers = await createResolvers(indexer, eventWatcher, gqlLogger);
319321

322+
const ethRPCHandlers = await createEthRPCHandlers(indexer, ethProvider);
323+
320324
// Create an Express app
321325
const app: Application = express();
322-
const server = await createAndStartServer(app, typeDefs, resolvers, config.server, paymentsManager);
326+
const server = await createAndStartServer(app, typeDefs, resolvers, ethRPCHandlers, config.server, paymentsManager);
323327

324328
await startGQLMetricsServer(config);
325329

packages/graph-node/test/utils/indexer.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import assert from 'assert';
44
import { DeepPartial, FindConditions, FindManyOptions } from 'typeorm';
5-
import { providers } from 'ethers';
5+
import { ethers } from 'ethers';
66

77
import {
88
IndexerInterface,
@@ -28,6 +28,8 @@ import { GetStorageAt, getStorageValue, MappingKey, StorageLayout } from '@cerc-
2828
export class Indexer implements IndexerInterface {
2929
_getStorageAt: GetStorageAt;
3030
_storageLayoutMap: Map<string, StorageLayout> = new Map();
31+
_contractMap: Map<string, ethers.utils.Interface> = new Map();
32+
3133
eventSignaturesMap: Map<string, string[]> = new Map();
3234

3335
constructor (ethClient: EthClient, storageLayoutMap?: Map<string, StorageLayout>) {
@@ -50,6 +52,10 @@ export class Indexer implements IndexerInterface {
5052
return this._storageLayoutMap;
5153
}
5254

55+
get contractMap (): Map<string, ethers.utils.Interface> {
56+
return this._contractMap;
57+
}
58+
5359
async init (): Promise<void> {
5460
return undefined;
5561
}

packages/util/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"it-length-prefixed": "^8.0.4",
3737
"it-pipe": "^2.0.5",
3838
"it-pushable": "^3.1.2",
39+
"jayson": "^4.1.2",
3940
"js-yaml": "^4.1.0",
4041
"json-bigint": "^1.0.0",
4142
"lodash": "^4.17.21",

packages/util/src/config.ts

+3
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,9 @@ export interface ServerConfig {
252252
// Flag to specify whether RPC endpoint supports block hash as block tag parameter
253253
// https://ethereum.org/en/developers/docs/apis/json-rpc/#default-block
254254
rpcSupportsBlockHashParam: boolean;
255+
256+
// Enable ETH JSON RPC server at /rpc
257+
enableEthRPCServer: boolean;
255258
}
256259

257260
export interface FundingAmountsConfig {

packages/util/src/eth-rpc-handlers.ts

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/* eslint-disable @typescript-eslint/no-unused-vars */
2+
import { utils } from 'ethers';
3+
4+
import { JsonRpcProvider } from '@ethersproject/providers';
5+
6+
import { IndexerInterface } from './types';
7+
8+
const CODE_INVALID_PARAMS = -32602;
9+
const CODE_INTERNAL_ERROR = -32603;
10+
const CODE_SERVER_ERROR = -32000;
11+
12+
const ERROR_CONTRACT_MAP_NOT_SET = 'Contract map not set';
13+
const ERROR_CONTRACT_ABI_NOT_FOUND = 'Contract ABI not found';
14+
const ERROR_CONTRACT_INSUFFICIENT_PARAMS = 'Insufficient params';
15+
const ERROR_CONTRACT_NOT_RECOGNIZED = 'Contract not recognized';
16+
const ERROR_CONTRACT_METHOD_NOT_FOUND = 'Contract method not found';
17+
const ERROR_METHOD_NOT_IMPLEMENTED = 'Method not implemented';
18+
const ERROR_INVALID_BLOCK_TAG = 'Invalid block tag';
19+
const ERROR_BLOCK_NOT_FOUND = 'Block not found';
20+
21+
const DEFAULT_BLOCK_TAG = 'latest';
22+
23+
class ErrorWithCode extends Error {
24+
code: number;
25+
constructor (code: number, message: string) {
26+
super(message);
27+
this.code = code;
28+
}
29+
}
30+
31+
export const createEthRPCHandlers = async (
32+
indexer: IndexerInterface,
33+
ethProvider: JsonRpcProvider
34+
): Promise<any> => {
35+
return {
36+
eth_blockNumber: async (args: any, callback: any) => {
37+
const syncStatus = await indexer.getSyncStatus();
38+
const result = syncStatus ? `0x${syncStatus.latestProcessedBlockNumber.toString(16)}` : '0x';
39+
40+
callback(null, result);
41+
},
42+
43+
eth_call: async (args: any, callback: any) => {
44+
try {
45+
if (!indexer.contractMap) {
46+
throw new ErrorWithCode(CODE_INTERNAL_ERROR, ERROR_CONTRACT_MAP_NOT_SET);
47+
}
48+
49+
if (args.length === 0) {
50+
throw new ErrorWithCode(CODE_INVALID_PARAMS, ERROR_CONTRACT_INSUFFICIENT_PARAMS);
51+
}
52+
53+
const { to, data } = args[0];
54+
const blockTag = args.length > 1 ? args[1] : DEFAULT_BLOCK_TAG;
55+
56+
const blockHash = await parseBlockTag(indexer, ethProvider, blockTag);
57+
58+
const watchedContract = indexer.getWatchedContracts().find(contract => contract.address === to);
59+
if (!watchedContract) {
60+
throw new ErrorWithCode(CODE_INVALID_PARAMS, ERROR_CONTRACT_NOT_RECOGNIZED);
61+
}
62+
63+
const contractInterface = indexer.contractMap.get(watchedContract.kind);
64+
if (!contractInterface) {
65+
throw new ErrorWithCode(CODE_INTERNAL_ERROR, ERROR_CONTRACT_ABI_NOT_FOUND);
66+
}
67+
68+
// Slice out method signature from data
69+
const functionSelector = data.slice(0, 10);
70+
71+
// Find the matching function from the ABI
72+
const functionFragment = contractInterface.getFunction(functionSelector);
73+
if (!functionFragment) {
74+
throw new ErrorWithCode(CODE_INVALID_PARAMS, ERROR_CONTRACT_METHOD_NOT_FOUND);
75+
}
76+
77+
// Decode the data based on the matched function
78+
const decodedData = contractInterface.decodeFunctionData(functionFragment, data);
79+
80+
const functionName = functionFragment.name;
81+
const indexerMethod = (indexer as any)[functionName].bind(indexer);
82+
if (!indexerMethod) {
83+
throw new ErrorWithCode(CODE_SERVER_ERROR, ERROR_METHOD_NOT_IMPLEMENTED);
84+
}
85+
86+
const result = await indexerMethod(blockHash, to, ...decodedData);
87+
const encodedResult = contractInterface.encodeFunctionResult(functionFragment, Array.isArray(result.value) ? result.value : [result.value]);
88+
89+
callback(null, encodedResult);
90+
} catch (error: any) {
91+
let callBackError;
92+
if (error instanceof ErrorWithCode) {
93+
callBackError = { code: error.code, message: error.message };
94+
} else {
95+
callBackError = { code: CODE_SERVER_ERROR, message: error.message };
96+
}
97+
98+
callback(callBackError);
99+
}
100+
},
101+
102+
eth_getLogs: async (args: any, callback: any) => {
103+
// TODO: Implement
104+
}
105+
};
106+
};
107+
108+
const parseBlockTag = async (indexer: IndexerInterface, ethProvider: JsonRpcProvider, blockTag: string): Promise<string> => {
109+
if (utils.isHexString(blockTag)) {
110+
// Return value if hex string is of block hash length
111+
if (utils.hexDataLength(blockTag) === 32) {
112+
return blockTag;
113+
}
114+
115+
// Treat hex value as a block number
116+
const block = await ethProvider.getBlock(blockTag);
117+
if (block === null) {
118+
throw new ErrorWithCode(CODE_INVALID_PARAMS, ERROR_BLOCK_NOT_FOUND);
119+
}
120+
121+
return block.hash;
122+
}
123+
124+
if (blockTag === DEFAULT_BLOCK_TAG) {
125+
const syncStatus = await indexer.getSyncStatus();
126+
if (!syncStatus) {
127+
throw new ErrorWithCode(CODE_INTERNAL_ERROR, 'SyncStatus not found');
128+
}
129+
130+
return syncStatus.latestProcessedBlockHash;
131+
}
132+
133+
throw new ErrorWithCode(CODE_INVALID_PARAMS, ERROR_INVALID_BLOCK_TAG);
134+
};

packages/util/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,4 @@ export * from './eth';
2828
export * from './consensus';
2929
export * from './validate-config';
3030
export * from './logger';
31+
export * from './eth-rpc-handlers';

packages/util/src/server.ts

+22-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import debug from 'debug';
1111
import responseCachePlugin from 'apollo-server-plugin-response-cache';
1212
import { InMemoryLRUCache } from '@apollo/utils.keyvaluecache';
1313
import queue from 'express-queue';
14+
import jayson from 'jayson';
15+
import { json as jsonParser } from 'body-parser';
1416

1517
import { TypeSource } from '@graphql-tools/utils';
1618
import { makeExecutableSchema } from '@graphql-tools/schema';
@@ -22,11 +24,13 @@ import { PaymentsManager, paymentsPlugin } from './payments';
2224
const log = debug('vulcanize:server');
2325

2426
const DEFAULT_GQL_PATH = '/graphql';
27+
const ETH_RPC_PATH = '/rpc';
2528

2629
export const createAndStartServer = async (
2730
app: Application,
2831
typeDefs: TypeSource,
2932
resolvers: any,
33+
ethRPCHandlers: any,
3034
serverConfig: ServerConfig,
3135
paymentsManager?: PaymentsManager
3236
): Promise<ApolloServer> => {
@@ -98,8 +102,25 @@ export const createAndStartServer = async (
98102
path: gqlPath
99103
});
100104

105+
if (serverConfig.enableEthRPCServer) {
106+
// Create a JSON-RPC server to handle ETH RPC calls
107+
const rpcServer = jayson.Server(ethRPCHandlers);
108+
109+
// Mount the JSON-RPC server to ETH_RPC_PATH
110+
app.use(
111+
ETH_RPC_PATH,
112+
jsonParser(),
113+
// TODO: Handle GET requests as well to match Geth's behaviour
114+
rpcServer.middleware()
115+
);
116+
}
117+
101118
httpServer.listen(port, host, () => {
102-
log(`Server is listening on ${host}:${port}${server.graphqlPath}`);
119+
log(`GQL server is listening on http://${host}:${port}${server.graphqlPath}`);
120+
121+
if (serverConfig.enableEthRPCServer) {
122+
log(`ETH JSON RPC server is listening on http://${host}:${port}${ETH_RPC_PATH}`);
123+
}
103124
});
104125

105126
return server;

packages/util/src/types.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//
44

55
import { Connection, DeepPartial, EntityTarget, FindConditions, FindManyOptions, ObjectLiteral, QueryRunner } from 'typeorm';
6-
import { Transaction } from 'ethers';
6+
import { ethers, Transaction } from 'ethers';
77

88
import { MappingKey, StorageLayout } from '@cerc-io/solidity-mapper';
99

@@ -161,6 +161,7 @@ export interface IndexerInterface {
161161
readonly serverConfig: ServerConfig
162162
readonly upstreamConfig: UpstreamConfig
163163
readonly storageLayoutMap: Map<string, StorageLayout>
164+
readonly contractMap: Map<string, ethers.utils.Interface>
164165
// eslint-disable-next-line no-use-before-define
165166
readonly graphWatcher?: GraphWatcherInterface
166167
init (): Promise<void>

0 commit comments

Comments
 (0)