Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/examples/packages/ethers-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
},
"dependencies": {
"@metamask/snaps-sdk": "workspace:^",
"ethers": "^6.3.0"
"ethers": "^6.16.0"
},
"devDependencies": {
"@jest/globals": "^29.5.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/examples/packages/ethers-js/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/MetaMask/snaps.git"
},
"source": {
"shasum": "JvI40BHRDgrfeB75rQhlRoaHO7csK0diC0RrBhW4jxw=",
"shasum": "z6v9zCXkSPMKSydCdc98zmr9xKkXRzMb2WARZE2DWuo=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
3 changes: 2 additions & 1 deletion packages/snaps-simulation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,13 @@
"test:watch": "jest --watch"
},
"dependencies": {
"@metamask/eth-json-rpc-middleware": "^17.0.1",
"@metamask/json-rpc-engine": "^10.1.0",
"@metamask/json-rpc-middleware-stream": "^8.0.8",
"@metamask/key-tree": "^10.1.1",
"@metamask/messenger": "^0.3.0",
"@metamask/permission-controller": "^12.1.1",
"@metamask/phishing-controller": "^16.1.0",
"@metamask/rpc-errors": "^7.0.3",
"@metamask/snaps-controllers": "workspace:^",
"@metamask/snaps-execution-environments": "workspace:^",
"@metamask/snaps-rpc-methods": "workspace:^",
Expand All @@ -70,6 +70,7 @@
"@metamask/superstruct": "^3.2.1",
"@metamask/utils": "^11.9.0",
"@reduxjs/toolkit": "^1.9.5",
"ethers": "^6.16.0",
"fast-deep-equal": "^3.1.3",
"immer": "^9.0.21",
"mime": "^3.0.0",
Expand Down
5 changes: 0 additions & 5 deletions packages/snaps-simulation/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,6 @@ export const DEFAULT_LOCALE = 'en';
*/
export const DEFAULT_CURRENCY = 'usd';

/**
* The default JSON-RPC endpoint for Ethereum requests.
*/
export const DEFAULT_JSON_RPC_ENDPOINT = 'https://cloudflare-eth.com/';

/**
* The types of inputs that can be used in the `typeInField` interface action.
*/
Expand Down
29 changes: 29 additions & 0 deletions packages/snaps-simulation/src/methods/hooks/chain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Hex } from '@metamask/utils';
import type { SagaIterator } from 'redux-saga';
import { put } from 'redux-saga/effects';

import { setChain } from '../../store';
import type { RunSagaFunction } from '../../store';

/**
* Set the current chain ID in state.
*
* @param chainId - The chain ID.
* @yields Puts the chain ID in the store.
* @returns `null`.
*/
function* setCurrentChainImplementation(chainId: Hex): SagaIterator<void> {
yield put(setChain(chainId));
}

/**
* Get a method that can be used to set the current chain.
*
* @param runSaga - A function to run a saga outside the usual Redux flow.
* @returns A method that can be used to set the current chain.
*/
export function getSetCurrentChainImplementation(runSaga: RunSagaFunction) {
return async (...args: Parameters<typeof setCurrentChainImplementation>) => {
await runSaga(setCurrentChainImplementation, ...args).toPromise();
};
}
1 change: 1 addition & 0 deletions packages/snaps-simulation/src/methods/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './chain';
export * from './end-trace';
export * from './get-entropy-sources';
export * from './get-mnemonic';
Expand Down
13 changes: 2 additions & 11 deletions packages/snaps-simulation/src/middleware/engine.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { createFetchMiddleware } from '@metamask/eth-json-rpc-middleware';
import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine';
import { JsonRpcEngine } from '@metamask/json-rpc-engine';
import type { RestrictedMethodParameters } from '@metamask/permission-controller';
Expand All @@ -7,7 +6,7 @@ import type { Json } from '@metamask/utils';

import { createInternalMethodsMiddleware } from './internal-methods';
import { createMockMiddleware } from './mock';
import { DEFAULT_JSON_RPC_ENDPOINT } from '../constants';
import { createProviderMiddleware } from './provider';
import type {
PermittedMiddlewareHooks,
RestrictedMiddlewareHooks,
Expand All @@ -33,15 +32,13 @@ export type CreateJsonRpcEngineOptions = {
* @param options.restrictedHooks - Any hooks used by the middleware handlers.
* @param options.permittedHooks - Any hooks used by the middleware handlers.
* @param options.permissionMiddleware - The permission middleware to use.
* @param options.endpoint - The JSON-RPC endpoint to use for Ethereum requests.
* @returns A JSON-RPC engine.
*/
export function createJsonRpcEngine({
store,
restrictedHooks,
permittedHooks,
permissionMiddleware,
endpoint = DEFAULT_JSON_RPC_ENDPOINT,
}: CreateJsonRpcEngineOptions) {
const engine = new JsonRpcEngine();
engine.push(createMockMiddleware(store));
Expand All @@ -52,13 +49,7 @@ export function createJsonRpcEngine({
engine.push(createSnapsMethodMiddleware(true, permittedHooks));

engine.push(permissionMiddleware);
engine.push(
createFetchMiddleware({
btoa: globalThis.btoa,
fetch: globalThis.fetch,
rpcUrl: endpoint,
}),
);
engine.push(createProviderMiddleware(store));

return engine;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine';
import { logError } from '@metamask/snaps-utils';
import type { Json, JsonRpcParams } from '@metamask/utils';
import type { Hex, Json, JsonRpcParams } from '@metamask/utils';

import { getAccountsHandler } from './accounts';
import { getChainIdHandler } from './chain-id';
Expand All @@ -15,6 +15,13 @@ export type InternalMethodsMiddlewareHooks = {
* @returns The user's secret recovery phrase.
*/
getMnemonic: () => Promise<Uint8Array>;

/**
* A hook that sets the current chain ID.
*
* @param chainId - The chain ID.
*/
setCurrentChain: (chainId: Hex) => Promise<void>;
};

const methodHandlers = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,24 @@ describe('getSwitchEthereumChainHandler', () => {
jsonrpc: '2.0' as const,
id: 1,
};
const hooks = { setCurrentChain: jest.fn().mockResolvedValue(undefined) };
const chainId = '0xaa36a7';

await getSwitchEthereumChainHandler(
{
jsonrpc: '2.0',
id: 1,
method: 'wallet_switchEthereumChain',
params: [],
params: [{ chainId }],
},
result,
jest.fn(),
end,
hooks,
);

expect(end).toHaveBeenCalled();
expect(result.result).toBeNull();
expect(hooks.setCurrentChain).toHaveBeenCalledWith(chainId);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,41 @@ import type {
JsonRpcEngineEndCallback,
JsonRpcEngineNextCallback,
} from '@metamask/json-rpc-engine';
import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils';
import {
assert,
type Hex,
type JsonRpcRequest,
type PendingJsonRpcResponse,
} from '@metamask/utils';

export type SwitchEthereumChainHooks = {
setCurrentChain: (chain: Hex) => Promise<void>;
};

/**
* A mock handler for the `wallet_switchEthereumChain` method that always
* returns `null`.
*
* @param _request - Incoming JSON-RPC request. This is ignored for this
* specific handler.
* @param request - Incoming JSON-RPC request.
* @param response - The outgoing JSON-RPC response, modified to return the
* result.
* @param _next - The `json-rpc-engine` middleware next handler.
* @param end - The `json-rpc-engine` middleware end handler.
* @param hooks - The method hooks.
* @returns The response.
*/
export async function getSwitchEthereumChainHandler(
_request: JsonRpcRequest,
request: JsonRpcRequest,
response: PendingJsonRpcResponse,
_next: JsonRpcEngineNextCallback,
end: JsonRpcEngineEndCallback,
hooks: SwitchEthereumChainHooks,
) {
const castRequest = request as JsonRpcRequest<[{ chainId: Hex }]>;

assert(castRequest.params?.[0].chainId, 'No chain ID passed.');
await hooks.setCurrentChain(castRequest.params[0].chainId);

response.result = null;
return end();
}
39 changes: 39 additions & 0 deletions packages/snaps-simulation/src/middleware/provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine';
import { createAsyncMiddleware } from '@metamask/json-rpc-engine';
import { rpcErrors } from '@metamask/rpc-errors';
import type { Json, JsonRpcParams } from '@metamask/utils';
import { hasProperty, hexToBigInt } from '@metamask/utils';
import { InfuraProvider } from 'ethers';

import type { Store } from '../store';
import { getChainId } from '../store';

/**
* Create a middleware that uses a JSON-RPC provider to respond to RPC requests.
*
* @param store - The Redux store.
* @returns A middleware that responds to JSON-RPC requests.
*/
export function createProviderMiddleware(
store: Store,
): JsonRpcMiddleware<JsonRpcParams, Json> {
return createAsyncMiddleware(async (request, response) => {
try {
const chainId = getChainId(store.getState());
const provider = new InfuraProvider(hexToBigInt(chainId));

const result = await provider.send(request.method, request.params ?? []);
response.result = result;
} catch (error) {
if (hasProperty(error, 'info') && hasProperty(error.info, 'error')) {
response.error = error.info.error;
return;
}
if (hasProperty(error, 'error')) {
response.error = error.error;
return;
}
response.error = rpcErrors.internal();
}
});
}
15 changes: 13 additions & 2 deletions packages/snaps-simulation/src/simulation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import type {
} from '@metamask/snaps-sdk';
import type { FetchedSnapFiles, Snap } from '@metamask/snaps-utils';
import { logError } from '@metamask/snaps-utils';
import type { CaipAssetType, Json } from '@metamask/utils';
import type { CaipAssetType, Hex, Json } from '@metamask/utils';
import type { Duplex } from 'readable-stream';
import { pipeline } from 'readable-stream';
import type { SagaIterator } from 'redux-saga';
Expand All @@ -54,6 +54,7 @@ import {
getTrackErrorImplementation,
getEndTraceImplementation,
getStartTraceImplementation,
getSetCurrentChainImplementation,
} from './methods/hooks';
import { getGetMnemonicSeedImplementation } from './methods/hooks/get-mnemonic-seed';
import { createJsonRpcEngine } from './middleware';
Expand Down Expand Up @@ -155,6 +156,13 @@ export type RestrictedMiddlewareHooks = {
* @returns The metadata for the given Snap.
*/
getSnap: (snapId: string) => Snap;

/**
* A hook that sets the current chain ID.
*
* @param chainId - The chain ID.
*/
setCurrentChain: (chainId: Hex) => Promise<void>;
};

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

// Set up controllers and JSON-RPC stack.
const restrictedHooks = getRestrictedHooks(options);
const restrictedHooks = getRestrictedHooks(options, runSaga);
const permittedHooks = getPermittedHooks(
snapId,
snapFiles,
Expand Down Expand Up @@ -457,10 +465,12 @@ export async function installSnap<
* Get the hooks for the simulation.
*
* @param options - The simulation options.
* @param runSaga - The run saga function.
* @returns The hooks for the simulation.
*/
export function getRestrictedHooks(
options: SimulationOptions,
runSaga: RunSagaFunction,
): RestrictedMiddlewareHooks {
return {
getMnemonic: getGetMnemonicImplementation(options.secretRecoveryPhrase),
Expand All @@ -470,6 +480,7 @@ export function getRestrictedHooks(
getIsLocked: () => false,
getClientCryptography: () => ({}),
getSnap: getGetSnapImplementation(true),
setCurrentChain: getSetCurrentChainImplementation(runSaga),
};
}

Expand Down
36 changes: 36 additions & 0 deletions packages/snaps-simulation/src/store/chain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { Hex } from '@metamask/utils';
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';

import type { ApplicationState } from './store';

export type ChainState = {
chainId: Hex;
};

/**
* The initial chain state.
*/
const INITIAL_STATE: ChainState = {
chainId: '0x1',
};

export const chainSlice = createSlice({
name: 'chain',
initialState: INITIAL_STATE,
reducers: {
setChain: (state, action: PayloadAction<Hex>) => {
state.chainId = action.payload;
},
},
});

export const { setChain } = chainSlice.actions;

/**
* Get the chain ID from the state.
*
* @param state - The application state.
* @returns The chain ID.
*/
export const getChainId = (state: ApplicationState) => state.chain.chainId;
1 change: 1 addition & 0 deletions packages/snaps-simulation/src/store/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './chain';
export * from './mocks';
export * from './notifications';
export * from './state';
Expand Down
9 changes: 9 additions & 0 deletions packages/snaps-simulation/src/store/store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ describe('createStore', () => {
expect(store).toBeDefined();
expect(store.getState()).toMatchInlineSnapshot(`
{
"chain": {
"chainId": "0x1",
},
"mocks": {
"jsonRpc": {},
},
Expand Down Expand Up @@ -43,6 +46,9 @@ describe('createStore', () => {
expect(store).toBeDefined();
expect(store.getState()).toMatchInlineSnapshot(`
{
"chain": {
"chainId": "0x1",
},
"mocks": {
"jsonRpc": {},
},
Expand Down Expand Up @@ -78,6 +84,9 @@ describe('createStore', () => {
expect(store).toBeDefined();
expect(store.getState()).toMatchInlineSnapshot(`
{
"chain": {
"chainId": "0x1",
},
"mocks": {
"jsonRpc": {},
},
Expand Down
Loading
Loading