Skip to content

Conversation

@rekmarks
Copy link
Member

@rekmarks rekmarks commented Jul 23, 2025

Explanation

Introduces JsonRpcEngineV2 and JsonRpcServer, intended to replace all existing usage of the existing JsonRpcEngine implementation. For the motivation behind this change, see #6088 and/or this ADR.

Implementation

In order to resolve the problems listed in the motivation, V2 engine is split in two:

  • JsonRpcEngineV2
    • Orchestrates middleware and composes sub-engines
    • JSON-RPC requests go in, result values (not JSON-RPC responses) come out
    • Re-throws middleware errors directly
  • JsonRpcServer
    • JSON-RPC requests go in, JSON-RPC responses come out
    • Accepts an errorHandler constructor parameter for capturing engine / middleware errors

See the updated package README.md for details.

Migration

While this PR is substantial, migrating our existing JSON-RPC pipelines will be a significant project involving multiple teams over many releases cycles. To facilitate this, the PR introduces a forwards-compatibility adapter, asV2Middleware(), and backwards-compatibility adapter, asLegacyMiddleware(), for the legacy and V2 engines, respectively. In addition, all V2 exports are exposed under the /v2 export path, making this update completely non-breaking (although all legacy exports are deprecated).

Note to reviewers

I recommend proceeding as follows:

  1. Read the readme
  2. JsonRpcEngineV2
  3. JsonRpcServer
  4. Compatibility adapter functions

References

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed, highlighting breaking changes as necessary
  • I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes
    • Migrating our RPC pipelines left as an exercise to the reader.

Note

Introduces a new v2 JSON-RPC engine and server, adds two-way adapters with the legacy engine, updates exports/docs, and adds supporting utilities, tests, and dependencies.

  • json-rpc-engine (v2):
    • Adds JsonRpcEngineV2 for middleware orchestration and JsonRpcServer for spec-compliant request/response handling.
    • Introduces MiddlewareContext, utility types/helpers (isRequest, isNotification, JsonRpcEngineError, stringify), and composition APIs (asMiddleware, handle).
  • Compatibility:
    • Adds asLegacyMiddleware (use V2 in legacy engine) and asV2Middleware (use legacy engine in V2).
    • Copies non-JSON-RPC properties between legacy requests and V2 context where appropriate.
  • Legacy engine updates:
    • Marks legacy JsonRpcEngine and related types as deprecated; uses shared stringify util; minor test/ESLint adjustments.
  • Exports/Packaging:
    • Adds exports entry "./v2" and Browserify shim v2.js.
    • Updates root index and new v2/index exports; adds src/README.md for legacy and v2/README.md.
  • Docs/Changelog:
    • Overhauls README.md with V2 usage and migration notes; CHANGELOG.md notes Added (V2) and Deprecated (legacy).
  • Dependencies:
    • Adds deep-freeze-strict, klona, and @types/deep-freeze-strict.
  • Tests:
    • Extensive new tests for V2 engine, server, adapters, context, and utils; updates legacy tests for minor behavior comments.

Written by Cursor Bugbot for commit 04fdf3b. This will update automatically on new commits. Configure here.

@rekmarks rekmarks force-pushed the rekm/json-rpc-engine-rewrite-next branch from f74dff2 to c393838 Compare July 23, 2025 18:10
rekmarks added 29 commits July 23, 2025 16:29
@rekmarks rekmarks enabled auto-merge (squash) October 13, 2025 18:21
Copy link
Contributor

@mcmire mcmire left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had one more comment about typing context, but I have finally gone through everything in this PR so I believe that is the only thing that's left.

};

export type JsonRpcMiddleware<
Request extends JsonRpcCall = JsonRpcCall,
Copy link
Contributor

@mcmire mcmire Oct 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Thought] In providing default values for these parameters are we expecting engineers to be using this type directly? If so, I wonder if that is the most ergonomic approach. I wonder if we can almost always have TypeScript infer this. Maybe not something we have to improve now, but just a thought.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inference at the time of engine construction generally works, although if you're defining a middleware function in one place for an engine that lives elsewhere, you probably want to use this type to ensure that your middleware function is valid. Do you have any ideas as to how we would make that more ergonomic?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be curious how far we could get with removing the default values. I was mainly thinking it would reduce the size of this file. But maybe it wouldn't, and perhaps you're right that in practice it wouldn't matter as developers would need to pass in these types anyway. Like I said, just a thought as I worked with this file a bit locally, not anything concrete to suggest here.


describe('requests', () => {
it('returns a result from the middleware', async () => {
const middleware: JsonRpcMiddleware<JsonRpcRequest> = jest.fn(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Thought] I wonder if we should do another pass through these tests at some point and convert these type assertions into satisfies? I do agree with Jongsun that in general we should be promoting use of satisfies at least for non-final types instead of type assertions, so that we can have TypeScript infer as much as it can. I'd at least want the tests to serve as examples and represent best practices for how authors should define middleware.

Using a generic Result forced each middleware function to have the same
return type. This makes the generic useless. Supporting this, almost all
of our uses of the equivalent generic on the legacy JsonRpcMiddlware
type are Json, any, or unknown. In consequence, we remove the Result
generic.

In addition, unfreezes the result returned from asLegacyMiddleware, a
problem that was surfaced by the type refactor. Adds a test to verify
this behavior.
@rekmarks rekmarks disabled auto-merge October 20, 2025 20:07

constructor({ middleware }: Options<Request>) {
// See .create() for why this is private.
private constructor({ middleware }: ConstructorOptions<Request, Context>) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to entirely protect this from usage we could consider a constructor guard: https://github.com/MetaMask/key-tree/blob/main/src/SLIP10Node.ts#L517-L535

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, right! I don't think we need that level of protection. It's not that the constructor is dangerous but rather that type inference won't work as well.

@rekmarks rekmarks enabled auto-merge (squash) October 24, 2025 15:50
@rekmarks rekmarks merged commit e4f2a09 into main Oct 24, 2025
255 checks passed
@rekmarks rekmarks deleted the rekm/json-rpc-engine-rewrite-next branch October 24, 2025 16:24
Gudahtt added a commit that referenced this pull request Oct 24, 2025
…r/multichain-transactions-controller

* origin/main: (35 commits)
  feat: `JsonRpcEngineV2` (#6176)
  Release 641.0.0 (#6940)
  feat: Add transaction emulation actions (#6935)
  Release/640.0.0 (#6934)
  fix(core-backend): control randomness to fix flaky test (#6936)
  chore: Add `@metamask-previews/*` to NPM age gate exceptions (#6937)
  Release/639.0.0 (#6931)
  feat: make getCryptoApproveTransactionParams synchronous (#6930)
  feat: add new actions to `KeyringController` (#6928)
  feat: add `getAccounts` to `AccountsController` (#6927)
  chore: remove `Monad Mainnet` single call balance contract and add into account v4 (#6929)
  Release/638.0.0 (#6923)
  fix: Downgrade `multiformats` to `^9.9.0` to avoid ESM-only dependency (#6920)
  Release/637.0.0 (#6919)
  feat(account-tree-controller): add callbacks for hidden and pinned data (#6910)
  Release 636.0.0 (#6918)
  fix(core-backend): reconnection logic (#6861)
  fix:  Tx state listener and signature coverage (#6906)
  Release/635.0.0 (#6917)
  fix(base-controller): add TypeScript declaration file for legacy module resolution (#6915)
  ...
Gudahtt added a commit that referenced this pull request Oct 24, 2025
…/preview

* origin/main:
  feat: `JsonRpcEngineV2` (#6176)
  Release 641.0.0 (#6940)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[json-rpc-engine] Rewrite JsonRpcEngine for safety, ergonomics, and debuggability

6 participants