This repository was archived by the owner on Nov 9, 2023. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 57
Expand file tree
/
Copy pathJsonRpcEngine.ts
More file actions
488 lines (430 loc) · 13.1 KB
/
Copy pathJsonRpcEngine.ts
File metadata and controls
488 lines (430 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
import SafeEventEmitter from '@metamask/safe-event-emitter';
import { errorCodes, EthereumRpcError, serializeError } from 'eth-rpc-errors';
type Maybe<T> = Partial<T> | null | undefined;
export type Json =
| boolean
| number
| string
| null
| { [property: string]: Json }
| Json[];
/**
* A String specifying the version of the JSON-RPC protocol.
* MUST be exactly "2.0".
*/
export type JsonRpcVersion = '2.0';
/**
* An identifier established by the Client that MUST contain a String, Number,
* or NULL value if included. If it is not included it is assumed to be a
* notification. The value SHOULD normally not be Null and Numbers SHOULD
* NOT contain fractional parts.
*/
export type JsonRpcId = number | string | void;
export interface JsonRpcError {
code: number;
message: string;
data?: unknown;
stack?: string;
}
export interface JsonRpcRequest<T> {
jsonrpc: JsonRpcVersion;
method: string;
id: JsonRpcId;
params?: T;
}
export interface JsonRpcNotification<T> {
jsonrpc: JsonRpcVersion;
method: string;
params?: T;
}
interface JsonRpcResponseBase {
jsonrpc: JsonRpcVersion;
id: JsonRpcId;
}
export interface JsonRpcSuccess<T> extends JsonRpcResponseBase {
result: Maybe<T>;
}
export interface JsonRpcFailure extends JsonRpcResponseBase {
error: JsonRpcError;
}
export type JsonRpcResponse<T> = JsonRpcSuccess<T> | JsonRpcFailure;
export interface PendingJsonRpcResponse<T> extends JsonRpcResponseBase {
result?: T;
error?: Error | JsonRpcError;
}
export type JsonRpcEngineCallbackError = Error | JsonRpcError | null;
type MaybePromise<T> = Promise<T> | T;
export type JsonRpcEngineReturnHandler = () => MaybePromise<void>;
export type JsonRpcEngineEndCallback = (
error?: JsonRpcEngineCallbackError
) => void;
export type JsonRpcMiddleware<T, U> = (
req: JsonRpcRequest<T>,
res: PendingJsonRpcResponse<U>,
end: JsonRpcEngineEndCallback
) => MaybePromise<void | JsonRpcEngineReturnHandler>;
/**
* A JSON-RPC request and response processor.
* Give it a stack of middleware, pass it requests, and get back responses.
*/
export class JsonRpcEngine extends SafeEventEmitter {
private _middleware: JsonRpcMiddleware<unknown, unknown>[];
constructor() {
super();
this._middleware = [];
}
/**
* Add a middleware function to the engine's middleware stack.
*
* @param middleware - The middleware function to add.
*/
push<T, U>(middleware: JsonRpcMiddleware<T, U>): void {
this._middleware.push(middleware as JsonRpcMiddleware<unknown, unknown>);
}
/**
* Handle a JSON-RPC request, and return a response.
*
* @param request - The request to handle.
* @param callback - An error-first callback that will receive the response.
*/
handle<T, U>(
request: JsonRpcRequest<T>,
callback: (error: unknown, response: JsonRpcResponse<U>) => void,
): void;
/**
* Handle an array of JSON-RPC requests, and return an array of responses.
*
* @param request - The requests to handle.
* @param callback - An error-first callback that will receive the array of
* responses.
*/
handle<T, U>(
requests: JsonRpcRequest<T>[],
callback: (error: unknown, responses: JsonRpcResponse<U>[]) => void,
): void;
/**
* Handle a JSON-RPC request, and return a response.
*
* @param request - The request to handle.
* @returns A promise that resolves with the response, or rejects with an
* error.
*/
handle<T, U>(request: JsonRpcRequest<T>): Promise<JsonRpcResponse<U>>;
/**
* Handle an array of JSON-RPC requests, and return an array of responses.
*
* @param request - The requests to handle.
* @returns A promise that resolves with the array of responses, or rejects
* with an error.
*/
handle<T, U>(requests: JsonRpcRequest<T>[]): Promise<JsonRpcResponse<U>[]>;
handle(req: unknown, cb?: any) {
if (cb && typeof cb !== 'function') {
throw new Error('"callback" must be a function if provided.');
}
if (Array.isArray(req)) {
if (cb) {
return this._handleBatch(req, cb);
}
return this._handleBatch(req);
}
if (cb) {
return this._handle(req as JsonRpcRequest<unknown>, cb);
}
return this._promiseHandle(req as JsonRpcRequest<unknown>);
}
/**
* Returns this engine as a middleware function that can be pushed to other
* engines.
*
* @returns This engine as a middleware function.
*/
asMiddleware(): JsonRpcMiddleware<unknown, unknown> {
return async (req, res, end) => {
try {
const [
middlewareError,
isComplete,
returnHandlers,
] = await JsonRpcEngine._runAllMiddleware(req, res, this._middleware);
if (isComplete) {
await JsonRpcEngine._runReturnHandlers(returnHandlers);
return end(middlewareError as JsonRpcEngineCallbackError);
}
return async () => {
await JsonRpcEngine._runReturnHandlers(returnHandlers);
};
} catch (error) {
return end(error);
}
};
}
/**
* Like _handle, but for batch requests.
*/
private _handleBatch(
reqs: JsonRpcRequest<unknown>[],
): Promise<JsonRpcResponse<unknown>[]>;
/**
* Like _handle, but for batch requests.
*/
private _handleBatch(
reqs: JsonRpcRequest<unknown>[],
cb: (error: unknown, responses?: JsonRpcResponse<unknown>[]) => void,
): Promise<void>;
private async _handleBatch(
reqs: JsonRpcRequest<unknown>[],
cb?: (error: unknown, responses?: JsonRpcResponse<unknown>[]) => void,
): Promise<JsonRpcResponse<unknown>[] | void> {
// The order here is important
try {
// 2. Wait for all requests to finish, or throw on some kind of fatal
// error
const responses = await Promise.all(
// 1. Begin executing each request in the order received
reqs.map(this._promiseHandle.bind(this)),
);
// 3. Return batch response
if (cb) {
return cb(null, responses);
}
return responses;
} catch (error) {
if (cb) {
return cb(error);
}
throw error;
}
}
/**
* A promise-wrapped _handle.
*/
private _promiseHandle(
req: JsonRpcRequest<unknown>,
): Promise<JsonRpcResponse<unknown>> {
return new Promise((resolve) => {
this._handle(req, (_err, res) => {
// There will always be a response, and it will always have any error
// that is caught and propagated.
resolve(res);
});
});
}
/**
* Ensures that the request object is valid, processes it, and passes any
* error and the response object to the given callback.
*
* Does not reject.
*/
private async _handle(
callerReq: JsonRpcRequest<unknown>,
cb: (error: unknown, response: JsonRpcResponse<unknown>) => void,
): Promise<void> {
if (
!callerReq ||
Array.isArray(callerReq) ||
typeof callerReq !== 'object'
) {
const error = new EthereumRpcError(
errorCodes.rpc.invalidRequest,
`Requests must be plain objects. Received: ${typeof callerReq}`,
{ request: callerReq },
);
return cb(error, { id: undefined, jsonrpc: '2.0', error });
}
if (typeof callerReq.method !== 'string') {
const error = new EthereumRpcError(
errorCodes.rpc.invalidRequest,
`Must specify a string method. Received: ${typeof callerReq.method}`,
{ request: callerReq },
);
return cb(error, { id: callerReq.id, jsonrpc: '2.0', error });
}
const req: JsonRpcRequest<unknown> = { ...callerReq };
const res: PendingJsonRpcResponse<unknown> = {
id: req.id,
jsonrpc: req.jsonrpc,
};
let error: JsonRpcEngineCallbackError = null;
try {
await this._processRequest(req, res);
} catch (_error) {
// A request handler error, a re-thrown middleware error, or something
// unexpected.
error = _error;
}
if (error) {
// Ensure no result is present on an errored response
delete res.result;
if (!res.error) {
res.error = serializeError(error);
}
}
return cb(error, res as JsonRpcResponse<unknown>);
}
/**
* For the given request and response, runs all middleware and their return
* handlers, if any, and ensures that internal request processing semantics
* are satisfied.
*/
private async _processRequest(
req: JsonRpcRequest<unknown>,
res: PendingJsonRpcResponse<unknown>,
): Promise<void> {
const [
error,
isComplete,
returnHandlers,
] = await JsonRpcEngine._runAllMiddleware(req, res, this._middleware);
// Throw if "end" was not called, or if the response has neither a result
// nor an error.
JsonRpcEngine._checkForCompletion(req, res, isComplete);
// The return handlers should run even if an error was encountered during
// middleware processing.
await JsonRpcEngine._runReturnHandlers(returnHandlers);
// Now we re-throw the middleware processing error, if any, to catch it
// further up the call chain.
if (error) {
throw error;
}
}
/**
* Serially executes the given stack of middleware.
*
* @returns An array of any error encountered during middleware execution,
* a boolean indicating whether the request was completed, and an array of
* middleware-defined return handlers.
*/
private static async _runAllMiddleware(
req: JsonRpcRequest<unknown>,
res: PendingJsonRpcResponse<unknown>,
middlewareStack: JsonRpcMiddleware<unknown, unknown>[],
): Promise<
[
unknown, // error
boolean, // isComplete
JsonRpcEngineReturnHandler[],
]
> {
const returnHandlers: JsonRpcEngineReturnHandler[] = [];
let error = null;
let isComplete = false;
// Go down stack of middleware, call and collect optional returnHandlers
for (const middleware of middlewareStack) {
[error, isComplete] = await JsonRpcEngine._runMiddleware(
req,
res,
middleware,
returnHandlers,
);
if (isComplete) {
break;
}
}
return [error, isComplete, returnHandlers.reverse()];
}
/**
* Runs an individual middleware.
*
* @returns An array of any error encountered during middleware exection,
* and a boolean indicating whether the request should end.
*/
private static async _runMiddleware(
req: JsonRpcRequest<unknown>,
res: PendingJsonRpcResponse<unknown>,
middleware: JsonRpcMiddleware<unknown, unknown>,
returnHandlers: JsonRpcEngineReturnHandler[],
): Promise<[unknown, boolean]> {
const [
middlewareCallbackPromise,
resolve,
] = getDeferredPromise<[unknown, boolean]>();
let ended = false;
const end: JsonRpcEngineEndCallback = (err?: unknown) => {
const error = err || res.error;
if (error) {
res.error = serializeError(error);
}
// True indicates that the request should end
ended = true;
resolve([error, true]);
};
try {
const returnHandler = await middleware(req, res, end);
// If the request is already ended, there's nothing to do.
if (!ended) {
if (res.error) {
end(res.error);
} else {
if (returnHandler) {
if (typeof returnHandler !== 'function') {
end(
new EthereumRpcError(
errorCodes.rpc.internal,
`JsonRpcEngine: return handlers must be functions. ` +
`Received "${typeof returnHandler}" for request:\n${jsonify(
req,
)}`,
{ request: req },
),
);
}
returnHandlers.push(returnHandler);
}
// False indicates that the request should not end
resolve([null, false]);
}
}
} catch (error) {
end(error);
}
return middlewareCallbackPromise;
}
/**
* Serially executes array of return handlers. The request and response are
* assumed to be in their scope.
*/
private static async _runReturnHandlers(
handlers: JsonRpcEngineReturnHandler[],
): Promise<void> {
for (const handler of handlers) {
await handler();
}
}
/**
* Throws an error if the response has neither a result nor an error, or if
* the "isComplete" flag is falsy.
*/
private static _checkForCompletion(
req: JsonRpcRequest<unknown>,
res: PendingJsonRpcResponse<unknown>,
isComplete: boolean,
): void {
if (!('result' in res) && !('error' in res)) {
throw new EthereumRpcError(
errorCodes.rpc.internal,
`JsonRpcEngine: Response has no error or result for request:\n${jsonify(
req,
)}`,
{ request: req },
);
}
if (!isComplete) {
throw new EthereumRpcError(
errorCodes.rpc.internal,
`JsonRpcEngine: Nothing ended request:\n${jsonify(req)}`,
{ request: req },
);
}
}
}
function jsonify(request: JsonRpcRequest<unknown>): string {
return JSON.stringify(request, null, 2);
}
function getDeferredPromise<T>(): [ Promise<T>, (value: T) => void] {
let resolve: any;
const promise: Promise<T> = new Promise((_resolve) => {
resolve = _resolve;
});
return [promise, resolve];
}