Skip to content

Commit 44902d8

Browse files
feat: Support more chains when simulating Ethereum provider
1 parent db7a1ca commit 44902d8

File tree

13 files changed

+145
-137
lines changed

13 files changed

+145
-137
lines changed

packages/snaps-simulation/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@
5555
"test:watch": "jest --watch"
5656
},
5757
"dependencies": {
58-
"@metamask/eth-json-rpc-middleware": "^17.0.1",
5958
"@metamask/json-rpc-engine": "^10.1.0",
6059
"@metamask/json-rpc-middleware-stream": "^8.0.8",
6160
"@metamask/key-tree": "^10.1.1",
@@ -70,6 +69,7 @@
7069
"@metamask/superstruct": "^3.2.1",
7170
"@metamask/utils": "^11.9.0",
7271
"@reduxjs/toolkit": "^1.9.5",
72+
"ethers": "^6.16.0",
7373
"fast-deep-equal": "^3.1.3",
7474
"immer": "^9.0.21",
7575
"mime": "^3.0.0",

packages/snaps-simulation/src/constants.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,6 @@ export const DEFAULT_LOCALE = 'en';
2424
*/
2525
export const DEFAULT_CURRENCY = 'usd';
2626

27-
/**
28-
* The default JSON-RPC endpoint for Ethereum requests.
29-
*/
30-
export const DEFAULT_JSON_RPC_ENDPOINT = 'https://cloudflare-eth.com/';
31-
3227
/**
3328
* The types of inputs that can be used in the `typeInField` interface action.
3429
*/
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { Hex } from '@metamask/utils';
2+
import type { SagaIterator } from 'redux-saga';
3+
import { put } from 'redux-saga/effects';
4+
5+
import { setChain } from '../../store';
6+
import type { RunSagaFunction } from '../../store';
7+
8+
/**
9+
* Set the current chain ID in state.
10+
*
11+
* @param chainId - The chain ID.
12+
* @yields Puts the chain ID in the store.
13+
* @returns `null`.
14+
*/
15+
function* setCurrentChainImplementation(chainId: Hex): SagaIterator<void> {
16+
yield put(setChain(chainId));
17+
}
18+
19+
/**
20+
* Get a method that can be used to set the current chain.
21+
*
22+
* @param runSaga - A function to run a saga outside the usual Redux flow.
23+
* @returns A method that can be used to set the current chain.
24+
*/
25+
export function getSetCurrentChainImplementation(runSaga: RunSagaFunction) {
26+
return async (...args: Parameters<typeof setCurrentChainImplementation>) => {
27+
await runSaga(setCurrentChainImplementation, ...args).toPromise();
28+
};
29+
}

packages/snaps-simulation/src/methods/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './chain';
12
export * from './end-trace';
23
export * from './get-entropy-sources';
34
export * from './get-mnemonic';

packages/snaps-simulation/src/middleware/engine.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { createFetchMiddleware } from '@metamask/eth-json-rpc-middleware';
21
import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine';
32
import { JsonRpcEngine } from '@metamask/json-rpc-engine';
43
import type { RestrictedMethodParameters } from '@metamask/permission-controller';
@@ -7,7 +6,7 @@ import type { Json } from '@metamask/utils';
76

87
import { createInternalMethodsMiddleware } from './internal-methods';
98
import { createMockMiddleware } from './mock';
10-
import { DEFAULT_JSON_RPC_ENDPOINT } from '../constants';
9+
import { createProviderMiddleware } from './provider';
1110
import type {
1211
PermittedMiddlewareHooks,
1312
RestrictedMiddlewareHooks,
@@ -33,15 +32,13 @@ export type CreateJsonRpcEngineOptions = {
3332
* @param options.restrictedHooks - Any hooks used by the middleware handlers.
3433
* @param options.permittedHooks - Any hooks used by the middleware handlers.
3534
* @param options.permissionMiddleware - The permission middleware to use.
36-
* @param options.endpoint - The JSON-RPC endpoint to use for Ethereum requests.
3735
* @returns A JSON-RPC engine.
3836
*/
3937
export function createJsonRpcEngine({
4038
store,
4139
restrictedHooks,
4240
permittedHooks,
4341
permissionMiddleware,
44-
endpoint = DEFAULT_JSON_RPC_ENDPOINT,
4542
}: CreateJsonRpcEngineOptions) {
4643
const engine = new JsonRpcEngine();
4744
engine.push(createMockMiddleware(store));
@@ -52,13 +49,7 @@ export function createJsonRpcEngine({
5249
engine.push(createSnapsMethodMiddleware(true, permittedHooks));
5350

5451
engine.push(permissionMiddleware);
55-
engine.push(
56-
createFetchMiddleware({
57-
btoa: globalThis.btoa,
58-
fetch: globalThis.fetch,
59-
rpcUrl: endpoint,
60-
}),
61-
);
52+
engine.push(createProviderMiddleware(store));
6253

6354
return engine;
6455
}

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine';
22
import { logError } from '@metamask/snaps-utils';
3-
import type { Json, JsonRpcParams } from '@metamask/utils';
3+
import type { Hex, Json, JsonRpcParams } from '@metamask/utils';
44

55
import { getAccountsHandler } from './accounts';
66
import { getChainIdHandler } from './chain-id';
@@ -15,6 +15,13 @@ export type InternalMethodsMiddlewareHooks = {
1515
* @returns The user's secret recovery phrase.
1616
*/
1717
getMnemonic: () => Promise<Uint8Array>;
18+
19+
/**
20+
* A hook that sets the current chain ID.
21+
*
22+
* @param chainId - The chain ID.
23+
*/
24+
setCurrentChain: (chainId: Hex) => Promise<void>;
1825
};
1926

2027
const methodHandlers = {

packages/snaps-simulation/src/middleware/internal-methods/switch-ethereum-chain.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,41 @@ import type {
22
JsonRpcEngineEndCallback,
33
JsonRpcEngineNextCallback,
44
} from '@metamask/json-rpc-engine';
5-
import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils';
5+
import {
6+
assert,
7+
type Hex,
8+
type JsonRpcRequest,
9+
type PendingJsonRpcResponse,
10+
} from '@metamask/utils';
11+
12+
export type SwitchEthereumChainHooks = {
13+
setCurrentChain: (chain: Hex) => Promise<void>;
14+
};
615

716
/**
817
* A mock handler for the `wallet_switchEthereumChain` method that always
918
* returns `null`.
1019
*
11-
* @param _request - Incoming JSON-RPC request. This is ignored for this
12-
* specific handler.
20+
* @param request - Incoming JSON-RPC request.
1321
* @param response - The outgoing JSON-RPC response, modified to return the
1422
* result.
1523
* @param _next - The `json-rpc-engine` middleware next handler.
1624
* @param end - The `json-rpc-engine` middleware end handler.
25+
* @param hooks - The method hooks.
1726
* @returns The response.
1827
*/
1928
export async function getSwitchEthereumChainHandler(
20-
_request: JsonRpcRequest,
29+
request: JsonRpcRequest,
2130
response: PendingJsonRpcResponse,
2231
_next: JsonRpcEngineNextCallback,
2332
end: JsonRpcEngineEndCallback,
33+
hooks: SwitchEthereumChainHooks,
2434
) {
35+
const castRequest = request as JsonRpcRequest<[{ chainId: Hex }]>;
36+
37+
assert(castRequest.params?.[0].chainId, 'No chain ID passed.');
38+
await hooks.setCurrentChain(castRequest.params[0].chainId);
39+
2540
response.result = null;
2641
return end();
2742
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine';
2+
import { createAsyncMiddleware } from '@metamask/json-rpc-engine';
3+
import type { Json, JsonRpcParams } from '@metamask/utils';
4+
import { hexToBigInt } from '@metamask/utils';
5+
import { InfuraProvider } from 'ethers';
6+
7+
import type { Store, getChainId } from '../store';
8+
9+
/**
10+
* Create a middleware that uses a JSON-RPC provider to respond to RPC requests.
11+
*
12+
* @param store - The Redux store.
13+
* @returns A middleware that responds to JSON-RPC requests.
14+
*/
15+
export function createProviderMiddleware(
16+
store: Store,
17+
): JsonRpcMiddleware<JsonRpcParams, Json> {
18+
return createAsyncMiddleware(async (request, response) => {
19+
const chainId = getChainId(store.getState());
20+
const provider = new InfuraProvider(hexToBigInt(chainId));
21+
22+
const result = await provider.send(request.method, request.params ?? []);
23+
response.result = result;
24+
});
25+
}

packages/snaps-simulation/src/simulation.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import type {
3030
} from '@metamask/snaps-sdk';
3131
import type { FetchedSnapFiles, Snap } from '@metamask/snaps-utils';
3232
import { logError } from '@metamask/snaps-utils';
33-
import type { CaipAssetType, Json } from '@metamask/utils';
33+
import type { CaipAssetType, Hex, Json } from '@metamask/utils';
3434
import type { Duplex } from 'readable-stream';
3535
import { pipeline } from 'readable-stream';
3636
import type { SagaIterator } from 'redux-saga';
@@ -54,6 +54,7 @@ import {
5454
getTrackErrorImplementation,
5555
getEndTraceImplementation,
5656
getStartTraceImplementation,
57+
getSetCurrentChainImplementation,
5758
} from './methods/hooks';
5859
import { getGetMnemonicSeedImplementation } from './methods/hooks/get-mnemonic-seed';
5960
import { createJsonRpcEngine } from './middleware';
@@ -155,6 +156,13 @@ export type RestrictedMiddlewareHooks = {
155156
* @returns The metadata for the given Snap.
156157
*/
157158
getSnap: (snapId: string) => Snap;
159+
160+
/**
161+
* A hook that sets the current chain ID.
162+
*
163+
* @param chainId - The chain ID.
164+
*/
165+
setCurrentChain: (chainId: Hex) => Promise<void>;
158166
};
159167

160168
export type PermittedMiddlewareHooks = {
@@ -373,7 +381,7 @@ export async function installSnap<
373381
registerActions(controllerMessenger, runSaga, options, snapId);
374382

375383
// Set up controllers and JSON-RPC stack.
376-
const restrictedHooks = getRestrictedHooks(options);
384+
const restrictedHooks = getRestrictedHooks(options, runSaga);
377385
const permittedHooks = getPermittedHooks(
378386
snapId,
379387
snapFiles,
@@ -457,10 +465,12 @@ export async function installSnap<
457465
* Get the hooks for the simulation.
458466
*
459467
* @param options - The simulation options.
468+
* @param runSaga - The run saga function.
460469
* @returns The hooks for the simulation.
461470
*/
462471
export function getRestrictedHooks(
463472
options: SimulationOptions,
473+
runSaga: RunSagaFunction,
464474
): RestrictedMiddlewareHooks {
465475
return {
466476
getMnemonic: getGetMnemonicImplementation(options.secretRecoveryPhrase),
@@ -470,6 +480,7 @@ export function getRestrictedHooks(
470480
getIsLocked: () => false,
471481
getClientCryptography: () => ({}),
472482
getSnap: getGetSnapImplementation(true),
483+
setCurrentChain: getSetCurrentChainImplementation(runSaga),
473484
};
474485
}
475486

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { Hex } from '@metamask/utils';
2+
import type { PayloadAction } from '@reduxjs/toolkit';
3+
import { createSlice } from '@reduxjs/toolkit';
4+
5+
import type { ApplicationState } from './store';
6+
7+
export type ChainState = {
8+
chainId: Hex;
9+
};
10+
11+
/**
12+
* The initial chain state.
13+
*/
14+
const INITIAL_STATE: ChainState = {
15+
chainId: '0x1',
16+
};
17+
18+
export const chainSlice = createSlice({
19+
name: 'chain',
20+
initialState: INITIAL_STATE,
21+
reducers: {
22+
setChain: (state, action: PayloadAction<Hex>) => {
23+
state.chainId = action.payload;
24+
},
25+
},
26+
});
27+
28+
export const { setChain } = chainSlice.actions;
29+
30+
/**
31+
* Get the chain ID from the state.
32+
*
33+
* @param state - The application state.
34+
* @returns The chain ID.
35+
*/
36+
export const getChainId = (state: ApplicationState) => state.chain.chainId;

0 commit comments

Comments
 (0)