Skip to content

Commit f0e8f91

Browse files
wip: Add MultichainRoutingController
1 parent 90b8e43 commit f0e8f91

File tree

1 file changed

+165
-0
lines changed

1 file changed

+165
-0
lines changed
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import type {
2+
RestrictedControllerMessenger,
3+
ControllerGetStateAction,
4+
ControllerStateChangeEvent,
5+
} from '@metamask/base-controller';
6+
import { BaseController } from '@metamask/base-controller';
7+
import type {
8+
Caveat,
9+
GetPermissions,
10+
ValidPermission,
11+
} from '@metamask/permission-controller';
12+
import { rpcErrors } from '@metamask/rpc-errors';
13+
import { SnapEndowments } from '@metamask/snaps-rpc-methods';
14+
import type {
15+
EmptyObject,
16+
Json,
17+
JsonRpcRequest,
18+
SnapId,
19+
} from '@metamask/snaps-sdk';
20+
import { HandlerType, type Caip2ChainId } from '@metamask/snaps-utils';
21+
import { hasProperty } from '@metamask/utils';
22+
23+
import { getRunnableSnaps } from '../snaps';
24+
import type { GetAllSnaps, HandleSnapRequest } from '../snaps';
25+
26+
export type MultichainRoutingControllerGetStateAction =
27+
ControllerGetStateAction<
28+
typeof controllerName,
29+
MultichainRoutingControllerState
30+
>;
31+
export type MultichainRoutingControllerStateChangeEvent =
32+
ControllerStateChangeEvent<
33+
typeof controllerName,
34+
MultichainRoutingControllerState
35+
>;
36+
37+
// Since the AccountsController depends on snaps-controllers we manually type this
38+
type InternalAccount = {
39+
id: string;
40+
type: string;
41+
address: string;
42+
options: Record<string, Json>;
43+
methods: string[];
44+
};
45+
46+
export type AccountsControllerListMultichainAccountsAction = {
47+
type: `AccountsController:listMultichainAccounts`;
48+
handler: (chainId?: Caip2ChainId) => InternalAccount[];
49+
};
50+
51+
export type MultichainRoutingControllerActions =
52+
| GetAllSnaps
53+
| HandleSnapRequest
54+
| GetPermissions
55+
| AccountsControllerListMultichainAccountsAction
56+
| MultichainRoutingControllerGetStateAction;
57+
58+
export type MultichainRoutingControllerEvents =
59+
MultichainRoutingControllerStateChangeEvent;
60+
61+
export type MultichainRoutingControllerMessenger =
62+
RestrictedControllerMessenger<
63+
typeof controllerName,
64+
MultichainRoutingControllerActions,
65+
MultichainRoutingControllerEvents,
66+
MultichainRoutingControllerActions['type'],
67+
MultichainRoutingControllerEvents['type']
68+
>;
69+
70+
export type MultichainRoutingControllerArgs = {
71+
messenger: MultichainRoutingControllerMessenger;
72+
state?: MultichainRoutingControllerState;
73+
};
74+
75+
export type MultichainRoutingControllerState = EmptyObject;
76+
77+
type SnapWithPermission = {
78+
snapId: SnapId;
79+
permission: ValidPermission<string, Caveat<string, Json>>;
80+
};
81+
82+
const controllerName = 'MultichainRoutingController';
83+
84+
export class MultichainRoutingController extends BaseController<
85+
typeof controllerName,
86+
MultichainRoutingControllerState,
87+
MultichainRoutingControllerMessenger
88+
> {
89+
constructor({ messenger, state }: MultichainRoutingControllerArgs) {
90+
super({
91+
messenger,
92+
metadata: {},
93+
name: controllerName,
94+
state: {
95+
...state,
96+
},
97+
});
98+
}
99+
100+
#getAccountSnapMethods(chainId: Caip2ChainId) {
101+
const accounts = this.messagingSystem.call(
102+
'AccountsController:listMultichainAccounts',
103+
chainId,
104+
);
105+
106+
return accounts.flatMap((account) => account.methods);
107+
}
108+
109+
#getProtocolSnaps(_chainId: Caip2ChainId, _method: string) {
110+
const allSnaps = this.messagingSystem.call('SnapController:getAll');
111+
const filteredSnaps = getRunnableSnaps(allSnaps);
112+
113+
return filteredSnaps.reduce<SnapWithPermission[]>((accumulator, snap) => {
114+
const permissions = this.messagingSystem.call(
115+
'PermissionController:getPermissions',
116+
snap.id,
117+
);
118+
// TODO: Protocol Snap export
119+
// TODO: Filter based on chain ID and method
120+
if (permissions && hasProperty(permissions, SnapEndowments.Rpc)) {
121+
accumulator.push({
122+
snapId: snap.id,
123+
permission: permissions[SnapEndowments.Rpc],
124+
});
125+
}
126+
127+
return accumulator;
128+
}, []);
129+
}
130+
131+
async handleRequest({
132+
chainId,
133+
request,
134+
}: {
135+
origin: string;
136+
chainId: Caip2ChainId;
137+
request: JsonRpcRequest;
138+
}) {
139+
// TODO: Determine if the request is already validated here?
140+
const { method } = request;
141+
142+
// If the RPC request can be serviced by an account Snap, route it there.
143+
const accountMethods = this.#getAccountSnapMethods(chainId);
144+
if (accountMethods.includes(method)) {
145+
// TODO: Determine how to call the AccountsRouter
146+
return null;
147+
}
148+
149+
// If the RPC request cannot be serviced by an account Snap,
150+
// but has a protocol Snap available, route it there.
151+
const protocolSnaps = this.#getProtocolSnaps(chainId, method);
152+
const snapId = protocolSnaps[0]?.snapId;
153+
if (snapId) {
154+
return this.messagingSystem.call('SnapController:handleRequest', {
155+
snapId,
156+
origin: 'metamask', // TODO: Determine origin of these requests?
157+
request,
158+
handler: HandlerType.OnRpcRequest, // TODO: Protocol Snap export
159+
});
160+
}
161+
162+
// If no compatible account or protocol Snaps were found, throw.
163+
throw rpcErrors.methodNotFound();
164+
}
165+
}

0 commit comments

Comments
 (0)