Skip to content

Conversation

@FrederikBolding
Copy link
Member

@FrederikBolding FrederikBolding commented Dec 1, 2025

Adds support for using the multichain API in Snaps. This is accomplished by setting up a separate provider and substream called metamask-multichain-provider. When snap.request is called with a multichain request, this provider and substream is used. The clients will need to route to the proper JSON-RPC pipeline based on the substream and verify that the Snap has the proper permission (TBD).

Additionally this PR adds a example Snap for usage of this API, that can leverage Ethereum and Solana APIs at once. It also adds limited simulation support for the multichain API. The simulation framework implements a basic version of the multichain API where sessions are tracked and requests are routed to supported EVM providers. There are no underlying providers for non-EVM request, but they can be mocked.

TODO:

  • wallet_revokeSession support
  • RPC method parameter validation
  • Figure out how to enforce the permission
  • Tests

@socket-security
Copy link

socket-security bot commented Dec 1, 2025

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​metamask/​chain-agnostic-permission@​1.4.01001007895100
Addedpackages%2Fexamples%2Fpackages%2Fmultichain-provider@​0.0.0-use.local100100100100100

View full report

import { getMockOptions } from '../test-utils/options';

const MOCK_HOOKS: RestrictedMiddlewareHooks = {
const MOCK_HOOKS = {
Copy link
Member

Choose a reason for hiding this comment

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

Why'd you remove the type? Can it be replaces by satisfies?

Copy link
Member Author

Choose a reason for hiding this comment

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

To make it easier to modify the mocks in each test without casting. They seem to be inferred correctly when passed in

options,
}: GetPermissionSpecificationsOptions) {
return {
[caip25EndowmentBuilder.targetName]:
Copy link
Member

Choose a reason for hiding this comment

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

Should probably be in buildSnapEndowmentSpecifications.

Copy link
Member Author

Choose a reason for hiding this comment

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

No, this is the multichain endowment that is specified in the clients, which is granted when using wallet_createSession. We don't want that to be part of the Snap endowments.

optionalScopes: request.params.optionalScopes ?? {},
sessionProperties: {},
isMultichainOrigin: true,
} as Caip25CaveatValue;
Copy link
Member

Choose a reason for hiding this comment

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

Is this assertion needed?

}

// TODO: Struct?
const { request: wrappedRequest, scope } = request.params as any;
Copy link
Member

Choose a reason for hiding this comment

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

Just leaving a comment so we don't forget about replacing any here.

const mergedScopes = {
...value.requiredScopes,
...value.optionalScopes,
} as Record<CaipChainId, InternalScopeObject & { methods: string[] }>;
Copy link
Member

Choose a reason for hiding this comment

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

Is this type assertion needed?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, this is slightly different from the proper types because we store methods so that we can recreate the session scopes. In the clients these values are not stored in the permission, but instead recreated whenever requested.

Copy link
Member Author

Choose a reason for hiding this comment

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

I chose this way of doing it to let the Snap register any non-EVM action as long as they mock it

const nonEvmMethods = Object.keys(mergedScopes).reduce<
Record<CaipChainId, string[]>
>((accumulator, scope) => {
const castScope = scope as CaipChainId | 'wallet';
Copy link
Member

Choose a reason for hiding this comment

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

Is this type assertion needed?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, because Object.keys just returns string[]

return new PermissionController({
messenger,
caveatSpecifications: {
[Caip25CaveatType]: caip25CaveatBuilder({
Copy link
Member

Choose a reason for hiding this comment

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

How come this isn't part of snapsEndowmentCaveatSpecifications?

Copy link
Member Author

Choose a reason for hiding this comment

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

* @param request - The JSON-RPC request.
* @returns The JSON-RPC response.
*/
export async function invokeMethod<ReturnType>(
Copy link
Member

Choose a reason for hiding this comment

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

This could be moved to the Module class, so it can be used like this.invokeMethod, removing the need to provide the scope.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point. I feel like I had a justification for this at some point, but I can't think of one now.

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.

3 participants