diff --git a/packages/snaps-controllers/src/multichain/MultichainRoutingController.ts b/packages/snaps-controllers/src/multichain/MultichainRoutingController.ts index 974fd36a2c..6d7e6a14d6 100644 --- a/packages/snaps-controllers/src/multichain/MultichainRoutingController.ts +++ b/packages/snaps-controllers/src/multichain/MultichainRoutingController.ts @@ -21,7 +21,8 @@ import type { JsonRpcRequest, SnapId, } from '@metamask/snaps-sdk'; -import { HandlerType, type Caip2ChainId } from '@metamask/snaps-utils'; +import { HandlerType } from '@metamask/snaps-utils'; +import type { CaipChainId } from '@metamask/utils'; import { hasProperty } from '@metamask/utils'; import { getRunnableSnaps } from '../snaps'; @@ -45,11 +46,15 @@ type InternalAccount = { address: string; options: Record; methods: string[]; + metadata: { + name: string; + snap?: { id: SnapId; enabled: boolean; name: string }; + }; }; export type AccountsControllerListMultichainAccountsAction = { type: `AccountsController:listMultichainAccounts`; - handler: (chainId?: Caip2ChainId) => InternalAccount[]; + handler: (chainId?: CaipChainId) => InternalAccount[]; }; export type MultichainRoutingControllerActions = @@ -101,16 +106,75 @@ export class MultichainRoutingController extends BaseController< }); } - #getAccountSnapMethods(chainId: Caip2ChainId) { - const accounts = this.messagingSystem.call( - 'AccountsController:listMultichainAccounts', + async #resolveRequestAddress( + snapId: SnapId, + chainId: CaipChainId, + request: JsonRpcRequest, + ) { + const result = (await this.messagingSystem.call( + 'SnapController:handleRequest', + { + snapId, + origin: 'metamask', + request: { + method: '', + params: { + chainId, + request, + }, + }, + handler: HandlerType.OnProtocolRequest, // TODO: Export and request format + }, + )) as { address: string } | null; + return result?.address; + } + + async #getAccountSnap( + protocolSnapId: SnapId, + chainId: CaipChainId, + request: JsonRpcRequest, + ) { + const accounts = this.messagingSystem + .call('AccountsController:listMultichainAccounts', chainId) + .filter( + (account) => + account.metadata.snap?.enabled && + account.methods.includes(request.method), + ); + + // If no accounts can service the request, return null. + if (accounts.length === 0) { + return null; + } + + // Attempt to resolve the address that should be used for signing. + const address = await this.#resolveRequestAddress( + protocolSnapId, chainId, + request, ); - return accounts.flatMap((account) => account.methods); + if (!address) { + throw rpcErrors.invalidParams(); + } + + // TODO: Decide what happens if we have more than one possible account. + const selectedAccount = accounts.find( + (account) => account.address.toLowerCase() === address.toLowerCase(), + ); + + if (!selectedAccount) { + throw rpcErrors.invalidParams(); + } + + return { + address: selectedAccount.address, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + snapId: selectedAccount.metadata.snap!.id, + }; } - #getProtocolSnaps(chainId: Caip2ChainId, method: string) { + #getProtocolSnaps(chainId: CaipChainId) { const allSnaps = this.messagingSystem.call('SnapController:getAll'); const filteredSnaps = getRunnableSnaps(allSnaps); @@ -122,9 +186,7 @@ export class MultichainRoutingController extends BaseController< if (permissions && hasProperty(permissions, SnapEndowments.Protocol)) { const permission = permissions[SnapEndowments.Protocol]; const chains = getProtocolCaveatChainIds(permission); - const methods = getProtocolCaveatRpcMethods(permission); - // TODO: This may need to be more complicated depending on the decided format. - if (chains?.includes(chainId) && methods?.includes(method)) { + if (chains?.includes(chainId)) { accumulator.push({ snapId: snap.id, permission, @@ -141,28 +203,49 @@ export class MultichainRoutingController extends BaseController< request, }: { origin: string; - chainId: Caip2ChainId; + chainId: CaipChainId; request: JsonRpcRequest; }) { // TODO: Determine if the request is already validated here? const { method } = request; + const protocolSnaps = this.#getProtocolSnaps(chainId); + if (protocolSnaps.length === 0) { + throw rpcErrors.methodNotFound(); + } + // If the RPC request can be serviced by an account Snap, route it there. - const accountMethods = this.#getAccountSnapMethods(chainId); - if (accountMethods.includes(method)) { - // TODO: Determine how to call the AccountsRouter - return null; + const accountSnap = await this.#getAccountSnap( + protocolSnaps[0].snapId, + chainId, + request, + ); + if (accountSnap) { + return this.messagingSystem.call('SnapController:handleRequest', { + snapId: accountSnap.snapId, + origin: 'metamask', // TODO: Determine origin of these requests? + request, + handler: HandlerType.OnKeyringRequest, + }); } // If the RPC request cannot be serviced by an account Snap, // but has a protocol Snap available, route it there. - const protocolSnaps = this.#getProtocolSnaps(chainId, method); - const snapId = protocolSnaps[0]?.snapId; - if (snapId) { + // TODO: This may need to be more complicated depending on the decided format. + const protocolSnap = protocolSnaps.find((snap) => + getProtocolCaveatRpcMethods(snap.permission)?.includes(method), + ); + if (protocolSnap) { return this.messagingSystem.call('SnapController:handleRequest', { - snapId, + snapId: protocolSnap.snapId, origin: 'metamask', // TODO: Determine origin of these requests? - request, + request: { + method: '', + params: { + request, + chainId, + }, + }, handler: HandlerType.OnProtocolRequest, }); } diff --git a/packages/snaps-controllers/src/multichain/index.ts b/packages/snaps-controllers/src/multichain/index.ts new file mode 100644 index 0000000000..6c07180225 --- /dev/null +++ b/packages/snaps-controllers/src/multichain/index.ts @@ -0,0 +1 @@ +export * from './MultichainRoutingController'; diff --git a/packages/snaps-sdk/src/types/handlers/protocol.ts b/packages/snaps-sdk/src/types/handlers/protocol.ts index 601b6cf926..4d0c86d992 100644 --- a/packages/snaps-sdk/src/types/handlers/protocol.ts +++ b/packages/snaps-sdk/src/types/handlers/protocol.ts @@ -1,4 +1,9 @@ -import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; +import type { + CaipChainId, + Json, + JsonRpcParams, + JsonRpcRequest, +} from '@metamask/utils'; /** * The `onProtocolRequest` handler, which is called when a Snap receives a @@ -9,6 +14,7 @@ import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; * @param args - The request arguments. * @param args.origin - The origin of the request. This can be the ID of another * Snap, or the URL of a website. + * @param args.chainId - The chain ID of the request. * @param args.request - The protocol request sent to the Snap. This includes * the method name and parameters. * @returns The response to the protocol request. This must be a @@ -19,5 +25,6 @@ export type OnProtocolRequestHandler< Params extends JsonRpcParams = JsonRpcParams, > = (args: { origin: string; + chainId: CaipChainId; request: JsonRpcRequest; }) => Promise;