Skip to content

Commit 16a5727

Browse files
committed
Allow createFetchMiddleware to take an RPC service
The `RpcService` class from `@metamask/network-controller` now incorporates the vast majority of logic contained in `createFetchMiddleware`. Using this class not only allows us to remove a lot of code from this package, but it also allows us to automatically cut over to a failover node when the network goes down.
1 parent 1105076 commit 16a5727

File tree

8 files changed

+1092
-29
lines changed

8 files changed

+1092
-29
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Changed
9+
- Deprecate passing an RPC endpoint to `createFetchMiddleware` in favor of passing an RPC service
10+
- The new, recommended method signature is now `createFetchMiddleware({ rpcService: AbstractRpcService; originHttpHeaderKey?: string; })`, where `AbstractRpcService` matches the same interface from `@metamask/network-controller`
11+
- This allows us to support automatic failover to a secondary node when the network goes down
12+
- The existing method signature `createFetchMiddleware({ btoa: typeof btoa; fetch: typeof fetch; rpcUrl: string; originHttpHeaderKey?: string; })` will be removed in a future major version
813

914
## [15.1.2]
1015
### Changed

jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ module.exports = {
2222
collectCoverage: true,
2323

2424
// An array of glob patterns indicating a set of files for which coverage information should be collected
25-
collectCoverageFrom: ['./src/**/*.ts'],
25+
collectCoverageFrom: ['./src/**/*.ts', '!./src/**/*.test-d.ts'],
2626

2727
// The directory where Jest should output its coverage files
2828
coverageDirectory: 'coverage',

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"lint:fix": "yarn lint:eslint --fix && yarn lint:misc --write",
2525
"lint:misc": "prettier '**/*.json' '**/*.md' '!CHANGELOG.md' '**/*.yml' '!.yarnrc.yml' --ignore-path .gitignore --no-error-on-unmatched-pattern",
2626
"prepack": "./scripts/prepack.sh",
27-
"test": "jest",
27+
"test": "jest && yarn test:types",
28+
"test:types": "tsd --files 'src/**/*.test-d.ts'",
2829
"test:watch": "jest --watch"
2930
},
3031
"dependencies": {
@@ -43,6 +44,7 @@
4344
"devDependencies": {
4445
"@jest/globals": "^27.5.1",
4546
"@lavamoat/allow-scripts": "^3.0.4",
47+
"@metamask-previews/controller-utils": "11.4.5-preview-3abe3688",
4648
"@metamask/auto-changelog": "^3.1.0",
4749
"@metamask/eslint-config": "^12.1.0",
4850
"@metamask/eslint-config-jest": "^12.1.0",
@@ -68,6 +70,7 @@
6870
"rimraf": "^3.0.2",
6971
"ts-jest": "^27.1.4",
7072
"ts-node": "^10.7.0",
73+
"tsd": "^0.31.2",
7174
"typescript": "~4.8.4"
7275
},
7376
"packageManager": "yarn@3.2.1",

src/fetch.test.ts

Lines changed: 215 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1+
import { JsonRpcEngine } from '@metamask/json-rpc-engine';
2+
import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils';
3+
14
import { createFetchConfigFromReq } from '.';
5+
import { createFetchMiddleware } from './fetch';
6+
import type { AbstractRpcService } from './types';
27

38
/**
49
* Generate a base64-encoded string from a binary string. This should be equivalent to
@@ -12,7 +17,178 @@ function btoa(stringToEncode: string) {
1217
return Buffer.from(stringToEncode).toString('base64');
1318
}
1419

15-
describe('fetch', () => {
20+
describe('createFetchMiddleware', () => {
21+
it('calls the RPC service with the correct request headers and body when no `originHttpHeaderKey` option given', async () => {
22+
const rpcService = buildRpcService();
23+
const requestSpy = jest.spyOn(rpcService, 'request');
24+
const middleware = createFetchMiddleware({
25+
rpcService,
26+
});
27+
28+
const engine = new JsonRpcEngine();
29+
engine.push(middleware);
30+
await engine.handle({
31+
id: 1,
32+
jsonrpc: '2.0',
33+
method: 'eth_chainId',
34+
params: [],
35+
});
36+
37+
expect(requestSpy).toHaveBeenCalledWith(
38+
{
39+
id: 1,
40+
jsonrpc: '2.0',
41+
method: 'eth_chainId',
42+
params: [],
43+
},
44+
{
45+
headers: {},
46+
},
47+
);
48+
});
49+
50+
it('includes the `origin` from the given request in the request headers under the given `originHttpHeaderKey`', async () => {
51+
const rpcService = buildRpcService();
52+
const requestSpy = jest.spyOn(rpcService, 'request');
53+
const middleware = createFetchMiddleware({
54+
rpcService,
55+
options: {
56+
originHttpHeaderKey: 'X-Dapp-Origin',
57+
},
58+
});
59+
60+
const engine = new JsonRpcEngine();
61+
engine.push(middleware);
62+
// Type assertion: this isn't a "proper" request as it includes `origin`,
63+
// but that's intentional.
64+
await engine.handle({
65+
id: 1,
66+
jsonrpc: '2.0',
67+
method: 'eth_chainId',
68+
params: [],
69+
origin: 'somedapp.com',
70+
} as JsonRpcRequest);
71+
72+
expect(requestSpy).toHaveBeenCalledWith(
73+
{
74+
id: 1,
75+
jsonrpc: '2.0',
76+
method: 'eth_chainId',
77+
params: [],
78+
},
79+
{
80+
headers: {
81+
'X-Dapp-Origin': 'somedapp.com',
82+
},
83+
},
84+
);
85+
});
86+
87+
describe('if the request to the service returns a successful JSON-RPC response', () => {
88+
it('includes the `result` field from the RPC service in its own response', async () => {
89+
const rpcService = buildRpcService();
90+
jest.spyOn(rpcService, 'request').mockResolvedValue({
91+
id: 1,
92+
jsonrpc: '2.0',
93+
result: 'the result',
94+
});
95+
const middleware = createFetchMiddleware({
96+
rpcService,
97+
});
98+
99+
const engine = new JsonRpcEngine();
100+
engine.push(middleware);
101+
const result = await engine.handle({
102+
id: 1,
103+
jsonrpc: '2.0',
104+
method: 'eth_chainId',
105+
params: [],
106+
});
107+
108+
expect(result).toStrictEqual({
109+
id: 1,
110+
jsonrpc: '2.0',
111+
result: 'the result',
112+
});
113+
});
114+
});
115+
116+
describe('if the request to the service returns a unsuccessful JSON-RPC response', () => {
117+
it('includes the `error` field from the service in a new internal JSON-RPC error', async () => {
118+
const rpcService = buildRpcService();
119+
jest.spyOn(rpcService, 'request').mockResolvedValue({
120+
id: 1,
121+
jsonrpc: '2.0',
122+
error: {
123+
code: -1000,
124+
message: 'oops',
125+
},
126+
});
127+
const middleware = createFetchMiddleware({
128+
rpcService,
129+
});
130+
131+
const engine = new JsonRpcEngine();
132+
engine.push(middleware);
133+
const result = await engine.handle({
134+
id: 1,
135+
jsonrpc: '2.0',
136+
method: 'eth_chainId',
137+
params: [],
138+
});
139+
140+
expect(result).toMatchObject({
141+
id: 1,
142+
jsonrpc: '2.0',
143+
error: {
144+
code: -32603,
145+
message: 'Internal JSON-RPC error.',
146+
stack: expect.stringContaining('Internal JSON-RPC error.'),
147+
data: {
148+
code: -1000,
149+
message: 'oops',
150+
cause: null,
151+
},
152+
},
153+
});
154+
});
155+
});
156+
157+
describe('if the request to the service throws', () => {
158+
it('includes the message and stack of the error in a new JSON-RPC error', async () => {
159+
const rpcService = buildRpcService();
160+
jest.spyOn(rpcService, 'request').mockRejectedValue(new Error('oops'));
161+
const middleware = createFetchMiddleware({
162+
rpcService,
163+
});
164+
165+
const engine = new JsonRpcEngine();
166+
engine.push(middleware);
167+
const result = await engine.handle({
168+
id: 1,
169+
jsonrpc: '2.0',
170+
method: 'eth_chainId',
171+
params: [],
172+
});
173+
174+
expect(result).toMatchObject({
175+
id: 1,
176+
jsonrpc: '2.0',
177+
error: {
178+
code: -32603,
179+
data: {
180+
cause: {
181+
message: 'oops',
182+
stack: expect.stringContaining('Error: oops'),
183+
},
184+
},
185+
},
186+
});
187+
});
188+
});
189+
});
190+
191+
describe('createFetchConfigFromReq', () => {
16192
it('should create a fetch config from a request', async () => {
17193
const req = {
18194
id: 1,
@@ -65,3 +241,41 @@ describe('fetch', () => {
65241
});
66242
});
67243
});
244+
245+
/**
246+
* Constructs a fake RPC service for use as a failover in tests.
247+
*
248+
* @returns The fake failover service.
249+
*/
250+
function buildRpcService(): AbstractRpcService {
251+
const onRetry: AbstractRpcService['onRetry'] = (_listener) => {
252+
return {
253+
dispose() {
254+
// do nothing
255+
},
256+
};
257+
};
258+
const onBreak: AbstractRpcService['onBreak'] = (_listener) => {
259+
return {
260+
dispose() {
261+
// do nothing
262+
},
263+
};
264+
};
265+
const onDegraded: AbstractRpcService['onDegraded'] = (_listener) => {
266+
// do nothing
267+
};
268+
269+
return {
270+
async request<Params extends JsonRpcParams, Result extends Json>(
271+
_jsonRpcRequest: JsonRpcRequest<Params>,
272+
_fetchOptions?: RequestInit,
273+
) {
274+
// FIXME: This isn't right, but whatever
275+
return 'ok' as Result;
276+
},
277+
onRetry,
278+
onBreak,
279+
onDegraded,
280+
};
281+
}

0 commit comments

Comments
 (0)