Skip to content

Commit 491f06d

Browse files
Support creating and using multichain API sessions
1 parent 307272d commit 491f06d

14 files changed

Lines changed: 312 additions & 30 deletions

File tree

packages/examples/packages/multichain-provider/snap.manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"url": "https://github.com/MetaMask/snaps.git"
88
},
99
"source": {
10-
"shasum": "CJiAtl5nGAZkZbPg4iyLxboK9t1qFs2glulvFYQoW8c=",
10+
"shasum": "h+Tn09Nkej5V97sNnoVXIBlZ+qCoRHQcvUVwwtIbGTU=",
1111
"location": {
1212
"npm": {
1313
"filePath": "dist/bundle.js",

packages/examples/packages/multichain-provider/src/index.test.ts

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,31 @@ describe('onRpcRequest', () => {
2020
});
2121
});
2222

23+
describe('createSession', () => {
24+
it('returns the established session', async () => {
25+
const { request } = await installSnap();
26+
27+
const response = await request({
28+
method: 'createSession',
29+
});
30+
31+
expect(response).toRespondWith({
32+
sessionScopes: {
33+
'eip155:1': {
34+
accounts: [],
35+
methods: expect.any(Array),
36+
notifications: expect.any(Array),
37+
},
38+
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': {
39+
accounts: [],
40+
methods: expect.any(Array),
41+
notifications: expect.any(Array),
42+
},
43+
},
44+
});
45+
});
46+
});
47+
2348
describe('getChainId', () => {
2449
const MOCK_CHAIN_ID = '0x01'; // Ethereum Mainnet
2550

@@ -38,6 +63,8 @@ describe('onRpcRequest', () => {
3863
it('returns the addresses granted access to by the user', async () => {
3964
const { request } = await installSnap();
4065

66+
await request({ method: 'createSession' });
67+
4168
const response = await request({
4269
method: 'getAccounts',
4370
});
@@ -53,9 +80,11 @@ describe('onRpcRequest', () => {
5380
const MOCK_SIGNATURE =
5481
'0x16f672a12220dc4d9e27671ef580cfc1397a9a4d5ee19eadea46c0f350b2f72a4922be7c1f16ed9b03ef1d3351eac469e33accf5a36194b1d88923701c2b163f1b';
5582

56-
it('returns a signature', async () => {
83+
it('returns a signature for Ethereum', async () => {
5784
const { request, mockJsonRpc } = await installSnap();
5885

86+
await request({ method: 'createSession' });
87+
5988
// We can mock the signature request with the response we want.
6089
mockJsonRpc({
6190
method: 'personal_sign',
@@ -64,7 +93,7 @@ describe('onRpcRequest', () => {
6493

6594
const response = await request({
6695
method: 'signMessage',
67-
params: { message: 'foo' },
96+
params: { scope: 'eip155:1', message: 'foo' },
6897
});
6998

7099
expect(response).toRespondWith(MOCK_SIGNATURE);
@@ -75,9 +104,11 @@ describe('onRpcRequest', () => {
75104
const MOCK_SIGNATURE =
76105
'0x01b37713300d99fecf0274bcb0dfb586a23d56c4bf2ed700c5ecf4ada7a2a14825e7b1212b1cc49c9440c375337561f2b7a6e639ba25be6a6f5a16f60e6931d31c';
77106

78-
it('returns a signature', async () => {
107+
it('returns a signature for Ethereum', async () => {
79108
const { request, mockJsonRpc } = await installSnap();
80109

110+
await request({ method: 'createSession' });
111+
81112
// We can mock the signature request with the response we want.
82113
mockJsonRpc({
83114
method: 'eth_signTypedData_v4',
@@ -86,10 +117,27 @@ describe('onRpcRequest', () => {
86117

87118
const response = await request({
88119
method: 'signTypedData',
89-
params: { message: 'foo' },
120+
params: { scope: 'eip155:1', message: 'foo' },
90121
});
91122

92123
expect(response).toRespondWith(MOCK_SIGNATURE);
93124
});
94125
});
126+
127+
describe('getGenesisHash', () => {
128+
it('returns a hash', async () => {
129+
const { request } = await installSnap();
130+
131+
await request({ method: 'createSession' });
132+
133+
const response = await request({
134+
method: 'getGenesisHash',
135+
params: { scope: 'eip155:1' },
136+
});
137+
138+
expect(response).toRespondWith(
139+
'0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3',
140+
);
141+
});
142+
});
95143
});

packages/examples/packages/multichain-provider/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => {
119119
return await scopeModule.signMessage(accounts[0], params.message);
120120
}
121121

122+
case 'getGenesisHash':
123+
return await scopeModule.getGenesisHash();
124+
122125
default:
123126
throw new MethodNotFoundError({ method: request.method });
124127
}

packages/snaps-simulation/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"test:watch": "jest --watch"
5656
},
5757
"dependencies": {
58+
"@metamask/chain-agnostic-permission": "^1.4.0",
5859
"@metamask/json-rpc-engine": "^10.1.0",
5960
"@metamask/json-rpc-middleware-stream": "^8.0.8",
6061
"@metamask/key-tree": "^10.1.1",

packages/snaps-simulation/src/controllers.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import {
2+
caip25CaveatBuilder,
3+
Caip25CaveatType,
4+
} from '@metamask/chain-agnostic-permission';
15
import { Messenger } from '@metamask/messenger';
26
import type {
37
CaveatSpecificationConstraint,
@@ -145,6 +149,11 @@ function getPermissionController(options: GetControllersOptions) {
145149
return new PermissionController({
146150
messenger,
147151
caveatSpecifications: {
152+
// @ts-expect-error Missing args temporarily.
153+
[Caip25CaveatType]: caip25CaveatBuilder({
154+
findNetworkClientIdByChainId: (chainId) => chainId,
155+
isNonEvmScopeSupported: (_scope) => true,
156+
}),
148157
...snapsCaveatsSpecifications,
149158
...snapsEndowmentCaveatSpecifications,
150159
},

packages/snaps-simulation/src/methods/specifications.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { caip25EndowmentBuilder } from '@metamask/chain-agnostic-permission';
12
import type { GenericPermissionController } from '@metamask/permission-controller';
23
import {
34
endowmentPermissionBuilders,
@@ -81,6 +82,8 @@ export function getPermissionSpecifications({
8182
options,
8283
}: GetPermissionSpecificationsOptions) {
8384
return {
85+
[caip25EndowmentBuilder.targetName]:
86+
caip25EndowmentBuilder.specificationBuilder({}),
8487
...buildSnapEndowmentSpecifications(EXCLUDED_SNAP_ENDOWMENTS),
8588
...buildSnapRestrictedMethodSpecifications(EXCLUDED_SNAP_PERMISSIONS, {
8689
// Shared hooks.

packages/snaps-simulation/src/middleware/internal-methods/middleware.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import type { Hex, Json, JsonRpcParams } from '@metamask/utils';
44

55
import { getAccountsHandler } from './accounts';
66
import { getChainIdHandler } from './chain-id';
7-
import { getSessionHandler } from './multichain';
7+
import {
8+
createSessionHandler,
9+
getSessionHandler,
10+
invokeMethodHandler,
11+
} from './multichain';
812
import { getNetworkVersionHandler } from './net-version';
913
import { getSwitchEthereumChainHandler } from './switch-ethereum-chain';
1014
import type { ApplicationState } from '../../store';
@@ -39,7 +43,9 @@ const methodHandlers = {
3943
eth_chainId: getChainIdHandler,
4044
net_version: getNetworkVersionHandler,
4145
wallet_switchEthereumChain: getSwitchEthereumChainHandler,
46+
wallet_createSession: createSessionHandler,
4247
wallet_getSession: getSessionHandler,
48+
wallet_invokeMethod: invokeMethodHandler,
4349
/* eslint-enable @typescript-eslint/naming-convention */
4450
};
4551

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { getSessionScopes } from '@metamask/chain-agnostic-permission';
2+
import type {
3+
JsonRpcEngineEndCallback,
4+
JsonRpcEngineNextCallback,
5+
} from '@metamask/json-rpc-engine';
6+
import type { RequestedPermissions } from '@metamask/permission-controller';
7+
import { rpcErrors } from '@metamask/rpc-errors';
8+
import {
9+
isObject,
10+
type JsonRpcRequest,
11+
type PendingJsonRpcResponse,
12+
} from '@metamask/utils';
13+
14+
export type CreateSessionHandlerHooks = {
15+
grantPermissions: (permissions: RequestedPermissions) => void;
16+
};
17+
18+
/**
19+
* A handler that implements a simplified version of `wallet_createSession`.
20+
*
21+
* @param request - Incoming JSON-RPC request.
22+
* @param response - The outgoing JSON-RPC response, modified to return the
23+
* result.
24+
* @param _next - The `json-rpc-engine` middleware next handler.
25+
* @param end - The `json-rpc-engine` middleware end handler.
26+
* @param hooks - The method hooks.
27+
* @returns The JSON-RPC response.
28+
*/
29+
export async function createSessionHandler(
30+
request: JsonRpcRequest,
31+
response: PendingJsonRpcResponse,
32+
_next: JsonRpcEngineNextCallback,
33+
end: JsonRpcEngineEndCallback,
34+
hooks: CreateSessionHandlerHooks,
35+
) {
36+
if (!isObject(request.params)) {
37+
return end(rpcErrors.invalidParams({ data: { request } }));
38+
}
39+
40+
const caveat = {
41+
requiredScopes: request.params.requiredScopes ?? {},
42+
optionalScopes: request.params.optionalScopes ?? {},
43+
sessionProperties: {},
44+
isMultichainOrigin: true,
45+
};
46+
47+
const permissions = {
48+
'endowment:caip25': {
49+
caveats: [
50+
{
51+
type: 'authorizedScopes',
52+
value: caveat,
53+
},
54+
],
55+
},
56+
};
57+
58+
// @ts-expect-error Ignore for now.
59+
hooks.grantPermissions(permissions);
60+
61+
// @ts-expect-error Ignore for now.
62+
const sessionScopes = getSessionScopes(caveat, {
63+
getNonEvmSupportedMethods: () => [],
64+
});
65+
66+
response.result = { sessionScopes };
67+
return end();
68+
}

packages/snaps-simulation/src/middleware/internal-methods/multichain/get-session.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import {
2+
Caip25CaveatType,
3+
Caip25EndowmentPermissionName,
4+
} from '@metamask/chain-agnostic-permission';
15
import type {
26
JsonRpcEngineEndCallback,
37
JsonRpcEngineNextCallback,
@@ -14,7 +18,7 @@ export type GetSessionHandlerHooks = {
1418
};
1519

1620
/**
17-
* A handler that implements `wallet_getSession`.
21+
* A handler that implements a simplified version of `wallet_getSession`.
1822
*
1923
* @param _request - Incoming JSON-RPC request. Ignored for this specific
2024
* handler.
@@ -32,6 +36,14 @@ export async function getSessionHandler(
3236
end: JsonRpcEngineEndCallback,
3337
hooks: GetSessionHandlerHooks,
3438
) {
35-
response.result = hooks.getCaveat('endowment:caip25', 'authorizedScopes');
39+
try {
40+
const caveat = hooks.getCaveat(
41+
Caip25EndowmentPermissionName,
42+
Caip25CaveatType,
43+
);
44+
response.result = { sessionScopes: caveat?.value ?? {} };
45+
} catch {
46+
response.result = { sessionScopes: {} };
47+
}
3648
return end();
3749
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1+
export * from './create-session';
12
export * from './get-session';
3+
export * from './invoke-method';

0 commit comments

Comments
 (0)