Skip to content

Commit 1894123

Browse files
authored
feat: Add context parameter to InternalProvider and JsonRpcServer methods (#7061)
## Explanation Adds a `MiddlewareContext` parameter to `InternalProvider.request()` and `JsonRpcServer.handle()`. This is essentially a missing feature from the `JsonRpcEngineV2` implementation / migration, since callers are no longer able to add non-JSON-RPC properties to request objects and instead need to use the `context` object. As part of this, permit specifying a plain object as the context to avoid forcing callers to import `MiddlewareContext` whenever they want to make a JSON-RPC call with some particular context. Also opportunistically fixes a bug with the static `isInstance` methods of V2 engine classes, where we wouldn't walk the prototype chain when checking for the symbol property. ## References - Related to #6327 ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/contributing.md#updating-changelogs), highlighting breaking changes as necessary - ~I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes~ - These changes are non-breaking, and will in any event be exhaustively covered via preview builds through #7065. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds a context option to `InternalProvider.request()`/`JsonRpcServer.handle()`, enables passing plain-object contexts across the V2 engine, makes `PollingBlockTracker` context-generic, and narrows Network Controller’s `Provider` type; updates tests/docs accordingly. > > - **JSON-RPC Engine (v2)**: > - Add `HandleOptions` with `context` (accepts `MiddlewareContext` or plain object) and forward it through `handle()` and `JsonRpcServer.handle()`. > - Enhance `MiddlewareContext` (construct from KeyValues object, `isInstance`) and export `InferKeyValues`; add shared `isInstance` util; minor server refactor (static request coercion). > - Update README and tests for context passing and utils. > - **eth-json-rpc-provider**: > - Make `InternalProvider` generic over `Context`; `request()` accepts `{ context }` and forwards to engine. > - Update tests and CHANGELOG. > - **eth-block-tracker**: > - Genericize `PollingBlockTracker` with `Context`; type provider as `InternalProvider<Context>`; update CHANGELOG. > - **Network Controller**: > - Narrow `Provider` to `InternalProvider<MiddlewareContext<{ origin: string; skipCache: boolean } & Record<string, unknown>>>`. > - Type updates in `create-network-client`, tests, and helpers; update CHANGELOG. > - **Tests/Utilities**: > - Update fakes (`FakeProvider`, `FakeBlockTracker`) and consumers to new generic/context types. > - Bridge controller test now uses `@metamask/network-controller` `Provider`. > - **Deps**: > - Add `@metamask/network-controller` to root `package.json`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit c853283. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 90b2523 commit 1894123

27 files changed

+374
-59
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"@metamask/eth-block-tracker": "^14.0.0",
6464
"@metamask/eth-json-rpc-provider": "^5.0.1",
6565
"@metamask/json-rpc-engine": "^10.1.1",
66+
"@metamask/network-controller": "^25.0.0",
6667
"@metamask/utils": "^11.8.1",
6768
"@ts-bridge/cli": "^0.6.4",
6869
"@types/jest": "^27.4.1",

packages/bridge-controller/src/utils/balance.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@ import { BigNumber } from '@ethersproject/bignumber';
22
import { AddressZero } from '@ethersproject/constants';
33
import { Contract } from '@ethersproject/contracts';
44
import { Web3Provider } from '@ethersproject/providers';
5-
import type { InternalProvider } from '@metamask/eth-json-rpc-provider';
65
import { abiERC20 } from '@metamask/metamask-eth-abis';
6+
import type { Provider } from '@metamask/network-controller';
77

88
import * as balanceUtils from './balance';
99
import { fetchTokenBalance } from './balance';
1010
import { FakeProvider } from '../../../../tests/fake-provider';
1111

1212
declare global {
1313
// eslint-disable-next-line no-var
14-
var ethereumProvider: InternalProvider;
14+
var ethereumProvider: Provider;
1515
}
1616

1717
jest.mock('@ethersproject/contracts', () => {

packages/eth-block-tracker/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Add `Context` generic parameter to `PollingBlockTracker` ([#7061](https://github.com/MetaMask/core/pull/7061))
13+
- This enables passing providers with different context types to the block tracker.
14+
1015
### Changed
1116

1217
- **BREAKING:** Use `InternalProvider` instead of `SafeEventEmitterProvider` ([#6796](https://github.com/MetaMask/core/pull/6796))

packages/eth-block-tracker/src/PollingBlockTracker.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import type { InternalProvider } from '@metamask/eth-json-rpc-provider';
2+
import type {
3+
ContextConstraint,
4+
MiddlewareContext,
5+
} from '@metamask/json-rpc-engine/v2';
26
import SafeEventEmitter from '@metamask/safe-event-emitter';
37
import {
48
createDeferredPromise,
@@ -17,8 +21,10 @@ const sec = 1000;
1721

1822
const blockTrackerEvents: (string | symbol)[] = ['sync', 'latest'];
1923

20-
export type PollingBlockTrackerOptions = {
21-
provider?: InternalProvider;
24+
export type PollingBlockTrackerOptions<
25+
Context extends ContextConstraint = MiddlewareContext,
26+
> = {
27+
provider?: InternalProvider<Context>;
2228
pollingInterval?: number;
2329
retryTimeout?: number;
2430
keepEventLoopActive?: boolean;
@@ -33,7 +39,9 @@ type ExtendedJsonRpcRequest = {
3339

3440
type InternalListener = (value: string) => void;
3541

36-
export class PollingBlockTracker
42+
export class PollingBlockTracker<
43+
Context extends ContextConstraint = MiddlewareContext,
44+
>
3745
extends SafeEventEmitter
3846
implements BlockTracker
3947
{
@@ -49,7 +57,7 @@ export class PollingBlockTracker
4957

5058
private _pollingTimeout?: ReturnType<typeof setTimeout>;
5159

52-
private readonly _provider: InternalProvider;
60+
private readonly _provider: InternalProvider<Context>;
5361

5462
private readonly _pollingInterval: number;
5563

@@ -65,7 +73,7 @@ export class PollingBlockTracker
6573

6674
#pendingFetch?: Omit<DeferredPromise<string>, 'resolve'>;
6775

68-
constructor(opts: PollingBlockTrackerOptions = {}) {
76+
constructor(opts: PollingBlockTrackerOptions<Context> = {}) {
6977
// parse + validate args
7078
if (!opts.provider) {
7179
throw new Error('PollingBlockTracker - no provider specified.');

packages/eth-json-rpc-provider/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Add `providerFromMiddlewareV2` ([#7001](https://github.com/MetaMask/core/pull/7001))
1313
- This accepts the new middleware from `@metamask/json-rpc-engine/v2`.
14+
- Add `context` option to `InternalProvider.request()` ([#7061](https://github.com/MetaMask/core/pull/7061))
15+
- Enables passing a `MiddlewareContext` to the JSON-RPC server.
1416

1517
### Changed
1618

packages/eth-json-rpc-provider/src/internal-provider.test.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { Web3Provider } from '@ethersproject/providers';
22
import EthQuery from '@metamask/eth-query';
33
import EthJsQuery from '@metamask/ethjs-query';
44
import { asV2Middleware, JsonRpcEngine } from '@metamask/json-rpc-engine';
5-
import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine/v2';
5+
import type {
6+
JsonRpcMiddleware,
7+
MiddlewareContext,
8+
} from '@metamask/json-rpc-engine/v2';
69
import { JsonRpcEngineV2 } from '@metamask/json-rpc-engine/v2';
710
import { providerErrors, rpcErrors } from '@metamask/rpc-errors';
811
import { type JsonRpcRequest, type Json } from '@metamask/utils';
@@ -16,7 +19,9 @@ import {
1619

1720
jest.mock('uuid');
1821

19-
type ResultParam = Json | ((req?: JsonRpcRequest) => Json);
22+
type ResultParam =
23+
| Json
24+
| ((req?: JsonRpcRequest, context?: MiddlewareContext) => Json);
2025

2126
const createLegacyEngine = (method: string, result: ResultParam) => {
2227
const engine = new JsonRpcEngine();
@@ -33,9 +38,11 @@ const createLegacyEngine = (method: string, result: ResultParam) => {
3338
const createV2Engine = (method: string, result: ResultParam) => {
3439
return JsonRpcEngineV2.create<JsonRpcMiddleware<JsonRpcRequest>>({
3540
middleware: [
36-
({ request, next }) => {
41+
({ request, next, context }) => {
3742
if (request.method === method) {
38-
return typeof result === 'function' ? result(request) : result;
43+
return typeof result === 'function'
44+
? result(request as JsonRpcRequest, context)
45+
: result;
3946
}
4047
return next();
4148
},
@@ -245,6 +252,29 @@ describe.each([
245252
expect(response.result).toBe(42);
246253
});
247254

255+
it('forwards the context to the JSON-RPC handler', async () => {
256+
const rpcHandler = createRpcHandler('test', (request, context) => {
257+
// @ts-expect-error - Intentional type abuse.
258+
// eslint-disable-next-line jest/no-conditional-in-test
259+
return context?.assertGet('foo') ?? request.foo;
260+
});
261+
const provider = new InternalProvider({ engine: rpcHandler });
262+
263+
const request = {
264+
id: 1,
265+
jsonrpc: '2.0' as const,
266+
method: 'test',
267+
};
268+
269+
const result = await provider.request(request, {
270+
context: {
271+
foo: 'bar',
272+
},
273+
});
274+
275+
expect(result).toBe('bar');
276+
});
277+
248278
it('handles a successful EIP-1193 object request', async () => {
249279
let req: JsonRpcRequest | undefined;
250280
const rpcHandler = createRpcHandler('test', (request) => {

packages/eth-json-rpc-provider/src/internal-provider.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { asV2Middleware, type JsonRpcEngine } from '@metamask/json-rpc-engine';
2-
import type {
3-
ContextConstraint,
4-
MiddlewareContext,
2+
import {
3+
type HandleOptions,
4+
type ContextConstraint,
5+
type MiddlewareContext,
6+
JsonRpcEngineV2,
57
} from '@metamask/json-rpc-engine/v2';
6-
import { JsonRpcEngineV2 } from '@metamask/json-rpc-engine/v2';
78
import type { JsonRpcSuccess } from '@metamask/utils';
89
import {
910
type Json,
@@ -37,16 +38,18 @@ type Options<
3738
* This provider loosely follows conventions that pre-date EIP-1193.
3839
* It is not compliant with any Ethereum provider standard.
3940
*/
40-
export class InternalProvider {
41-
readonly #engine: JsonRpcEngineV2<JsonRpcRequest, MiddlewareContext>;
41+
export class InternalProvider<
42+
Context extends ContextConstraint = MiddlewareContext,
43+
> {
44+
readonly #engine: JsonRpcEngineV2<JsonRpcRequest, Context>;
4245

4346
/**
4447
* Construct a InternalProvider from a JSON-RPC server or legacy engine.
4548
*
4649
* @param options - Options.
4750
* @param options.engine - The JSON-RPC engine used to process requests.
4851
*/
49-
constructor({ engine }: Options) {
52+
constructor({ engine }: Options<JsonRpcRequest, Context>) {
5053
this.#engine =
5154
'push' in engine
5255
? JsonRpcEngineV2.create({
@@ -59,14 +62,17 @@ export class InternalProvider {
5962
* Send a provider request asynchronously.
6063
*
6164
* @param eip1193Request - The request to send.
65+
* @param options - The options for the request operation.
66+
* @param options.context - The context to include with the request.
6267
* @returns The JSON-RPC response.
6368
*/
6469
async request<Params extends JsonRpcParams, Result extends Json>(
6570
eip1193Request: Eip1193Request<Params>,
71+
options?: HandleOptions<Context>,
6672
): Promise<Result> {
6773
const jsonRpcRequest =
6874
convertEip1193RequestToJsonRpcRequest(eip1193Request);
69-
return (await this.#handle<Result>(jsonRpcRequest)).result;
75+
return (await this.#handle<Result>(jsonRpcRequest, options)).result;
7076
}
7177

7278
/**
@@ -118,12 +124,14 @@ export class InternalProvider {
118124

119125
readonly #handle = async <Result extends Json>(
120126
jsonRpcRequest: JsonRpcRequest,
127+
options?: HandleOptions<Context>,
121128
): Promise<JsonRpcSuccess<Result>> => {
122129
const { id, jsonrpc } = jsonRpcRequest;
123-
// This typecast is technicaly unsafe, but we need it to preserve the provider's
124-
// public interface, which allows you to typecast results.
130+
// The `result` typecast is unsafe, but we need it to preserve the provider's
131+
// public interface, which allows you to unsafely typecast results.
125132
const result = (await this.#engine.handle(
126133
jsonRpcRequest,
134+
options,
127135
)) as unknown as Result;
128136

129137
return {

packages/json-rpc-engine/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12-
- Add `JsonRpcEngineV2` ([#6176](https://github.com/MetaMask/core/pull/6176), [#6971](https://github.com/MetaMask/core/pull/6971), [#6975](https://github.com/MetaMask/core/pull/6975), [#6990](https://github.com/MetaMask/core/pull/6990), [#6991](https://github.com/MetaMask/core/pull/6991), [#7001](https://github.com/MetaMask/core/pull/7001), [#7032](https://github.com/MetaMask/core/pull/7032))
12+
- Add `JsonRpcEngineV2` ([#6176](https://github.com/MetaMask/core/pull/6176), [#6971](https://github.com/MetaMask/core/pull/6971), [#6975](https://github.com/MetaMask/core/pull/6975), [#6990](https://github.com/MetaMask/core/pull/6990), [#6991](https://github.com/MetaMask/core/pull/6991), [#7032](https://github.com/MetaMask/core/pull/7032), [#7001](https://github.com/MetaMask/core/pull/7001), [#7061](https://github.com/MetaMask/core/pull/7061))
1313
- This is a complete rewrite of `JsonRpcEngine`, intended to replace the original implementation. See the readme for details.
1414

1515
## [10.1.1]

packages/json-rpc-engine/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,33 @@ const engine = JsonRpcEngineV2.create({
444444
});
445445
```
446446

447+
#### Passing the context to `handle()`
448+
449+
You can pass a `MiddlewareContext` instance directly to `handle()`:
450+
451+
```ts
452+
const context = new MiddlewareContext();
453+
context.set('foo', 'bar');
454+
const result = await engine.handle(
455+
{ id: '1', jsonrpc: '2.0', method: 'hello' },
456+
{ context },
457+
);
458+
console.log(result); // 'bar'
459+
```
460+
461+
You can also pass a plain object as a shorthand for a `MiddlewareContext` instance:
462+
463+
```ts
464+
const context = { foo: 'bar' };
465+
const result = await engine.handle(
466+
{ id: '1', jsonrpc: '2.0', method: 'hello' },
467+
{ context },
468+
);
469+
console.log(result); // 'bar'
470+
```
471+
472+
This works the same way for `JsonRpcServer.handle()`.
473+
447474
#### Constraining context keys and values
448475

449476
The context exposes a generic parameter `KeyValues`, which determines the keys and values

packages/json-rpc-engine/src/v2/JsonRpcEngineV2.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,24 @@ describe('JsonRpcEngineV2', () => {
371371
expect(result).toBe('bar');
372372
});
373373

374+
it('accepts an initial context as a KeyValues object', async () => {
375+
const initialContext = { foo: 'bar' } as const;
376+
const middleware: JsonRpcMiddleware<
377+
JsonRpcRequest,
378+
string,
379+
MiddlewareContext<Record<string, string>>
380+
> = ({ context }) => context.assertGet('foo');
381+
const engine = JsonRpcEngineV2.create({
382+
middleware: [middleware],
383+
});
384+
385+
const result = await engine.handle(makeRequest(), {
386+
context: initialContext,
387+
});
388+
389+
expect(result).toBe('bar');
390+
});
391+
374392
it('accepts middleware with different context types', async () => {
375393
const middleware1: JsonRpcMiddleware<
376394
JsonRpcCall,

0 commit comments

Comments
 (0)