Skip to content

Commit 0858b71

Browse files
authored
fix(snaps): Refactor SnapBridge to use SelectedNetworkController (#22873)
## **Description** This PR refactors the `SnapBridge` to use `SelectedNetworkController` to create a provider proxy. It also removes a lot of unnecessary logic. This fixes an issue where the snap couldn't switch the network. ## **Changelog** CHANGELOG entry: Fix snap `wallet_switchEthereumChain` ## **Related issues** Fixes: #22419 ## **Manual testing steps** 1. Set the selected network to any other network than ethereum mainnet (like polygon, base,...) 2. Go to the send flow 3. Expect a valid ENS name to be resolved when trying to send ETH on mainnet ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Refactors SnapBridge to source provider/block tracker from SelectedNetworkController, streamlines the JSON-RPC middleware stack and stream wiring, and types snapId as SnapId in init. > > - **Snaps**: > - **`SnapBridge` refactor**: > - Use `SelectedNetworkController.getProviderAndBlockTracker(snapId)` and add `createSelectedNetworkMiddleware`. > - Rebuild JSON-RPC engine: origin middleware → selected network → filter/subscription polyfills → preinstalled snaps middleware (when applicable) → permission middleware → snaps method middleware → user RPC middleware (with `#getProviderState`) → forward via `providerAsMiddleware(proxy.provider)`. > - Replace custom multiplexing with `setupMultiplex` and `pump`; add concise logging; adopt strong typings (`SnapId`, `JsonRpcMiddleware`, `InternalAccount`); remove legacy proxy/network-version logic. > - **Engine**: > - In `controllers/snaps/execution-service-init.ts`, pass `snapId` as `SnapId` when creating `SnapBridge`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 78ef601. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent ff3aa0c commit 0858b71

File tree

2 files changed

+112
-180
lines changed

2 files changed

+112
-180
lines changed

app/core/Engine/controllers/snaps/execution-service-init.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import Logger from '../../../../util/Logger';
99
import { SnapBridge } from '../../../Snaps';
1010
import getRpcMethodMiddleware from '../../../RPCMethods/RPCMethodMiddleware';
1111
import { Duration, inMilliseconds } from '@metamask/utils';
12+
import { SnapId } from '@metamask/snaps-sdk';
1213

1314
/**
1415
* Initialize the Snaps execution service.
@@ -38,7 +39,7 @@ export const executionServiceInit: ControllerInitFunction<
3839
// Consider developing an abstract class to derived custom implementations
3940
// for each use case.
4041
const bridge = new SnapBridge({
41-
snapId,
42+
snapId: snapId as SnapId,
4243
connectionStream,
4344
getRPCMethodMiddleware: ({ hostname, getProviderState }) =>
4445
getRpcMethodMiddleware({

app/core/Snaps/SnapBridge.ts

Lines changed: 110 additions & 179 deletions
Original file line numberDiff line numberDiff line change
@@ -1,259 +1,190 @@
1-
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
2-
/* eslint-disable import/no-commonjs */
3-
/* eslint-disable @typescript-eslint/no-require-imports */
4-
/* eslint-disable @typescript-eslint/no-var-requires */
5-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
6-
// @ts-nocheck
71
// eslint-disable-next-line import/no-nodejs-modules
82
import { Duplex } from 'stream';
3+
// @ts-expect-error - No types declarations
4+
import pump from 'pump';
5+
6+
import { JsonRpcEngine, JsonRpcMiddleware } from '@metamask/json-rpc-engine';
7+
// @ts-expect-error - No types declarations
8+
import createFilterMiddleware from '@metamask/eth-json-rpc-filters';
9+
// @ts-expect-error - No types declarations
10+
import createSubscriptionManager from '@metamask/eth-json-rpc-filters/subscriptionManager';
11+
import { JsonRpcParams, Json } from '@metamask/utils';
912
import {
10-
createSwappableProxy,
11-
createEventEmitterProxy,
12-
} from '@metamask/swappable-obj-proxy';
13-
import { JsonRpcEngine } from '@metamask/json-rpc-engine';
13+
createSelectedNetworkMiddleware,
14+
SelectedNetworkControllerMessenger,
15+
} from '@metamask/selected-network-controller';
16+
import { createPreinstalledSnapsMiddleware } from '@metamask/snaps-rpc-methods';
17+
import { SubjectType } from '@metamask/permission-controller';
18+
import { providerAsMiddleware } from '@metamask/eth-json-rpc-middleware';
1419
import { createEngineStream } from '@metamask/json-rpc-middleware-stream';
15-
import EthQuery from '@metamask/eth-query';
20+
import { SnapId } from '@metamask/snaps-sdk';
21+
import { InternalAccount } from '@metamask/keyring-internal-api';
1622

1723
import Engine from '../Engine';
1824
import { setupMultiplex } from '../../util/streams';
1925
import Logger from '../../util/Logger';
26+
import { createOriginMiddleware } from '../../util/middlewares';
27+
import { RPCMethodsMiddleParameters } from '../RPCMethods/RPCMethodMiddleware';
2028
import snapMethodMiddlewareBuilder from './SnapsMethodMiddleware';
21-
import { SubjectType } from '@metamask/permission-controller';
22-
import { createPreinstalledSnapsMiddleware } from '@metamask/snaps-rpc-methods';
2329
import { isSnapPreinstalled } from '../SnapKeyring/utils/snaps';
2430

25-
import ObjectMultiplex from '@metamask/object-multiplex';
26-
import createFilterMiddleware from '@metamask/eth-json-rpc-filters';
27-
import createSubscriptionManager from '@metamask/eth-json-rpc-filters/subscriptionManager';
28-
import { providerAsMiddleware } from '@metamask/eth-json-rpc-middleware';
29-
import { createOriginMiddleware } from '../../util/middlewares';
30-
import { createSelectedNetworkMiddleware } from '@metamask/selected-network-controller';
31-
const pump = require('pump');
32-
33-
interface ISnapBridgeProps {
34-
snapId: string;
35-
connectionStream: Duplex;
36-
// TODO: Replace "any" with type
37-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
38-
getRPCMethodMiddleware: (args: any) => any;
39-
}
40-
31+
/**
32+
* Type definition for the GetRPCMethodMiddleware function.
33+
*/
34+
type GetRPCMethodMiddleware = ({
35+
hostname,
36+
getProviderState,
37+
}: {
38+
hostname: RPCMethodsMiddleParameters['hostname'];
39+
getProviderState: RPCMethodsMiddleParameters['getProviderState'];
40+
}) => JsonRpcMiddleware<JsonRpcParams, Json>;
41+
42+
/**
43+
* A bridge for connecting the client Ethereum provider to a Snap's execution environment.
44+
*
45+
* @param params - The parameters for the SnapBridge.
46+
* @param params.snapId - The ID of the Snap.
47+
* @param params.connectionStream - The stream to connect to the Snap.
48+
* @param params.getRPCMethodMiddleware - A function to get the RPC method middleware.
49+
*/
4150
export default class SnapBridge {
42-
snapId: string;
43-
stream: Duplex;
44-
// TODO: Replace "any" with type
45-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
46-
getRPCMethodMiddleware: (args: any) => any;
47-
// TODO: Replace "any" with type
48-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
49-
provider: any;
50-
// TODO: Replace "any" with type
51-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
52-
blockTracker: any;
53-
54-
#mux: typeof ObjectMultiplex;
55-
// TODO: Replace "any" with type
56-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
57-
#providerProxy: any;
58-
// TODO: Replace "any" with type
59-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
60-
#blockTrackerProxy: any;
51+
#snapId: SnapId;
52+
#stream: Duplex;
53+
#getRPCMethodMiddleware: GetRPCMethodMiddleware;
6154

6255
constructor({
6356
snapId,
6457
connectionStream,
6558
getRPCMethodMiddleware,
66-
}: ISnapBridgeProps) {
67-
Logger.log(
68-
'[SNAP BRIDGE LOG] Engine+setupSnapProvider: Setup bridge for Snap',
69-
snapId,
70-
);
71-
72-
this.snapId = snapId;
73-
this.stream = connectionStream;
74-
this.getRPCMethodMiddleware = getRPCMethodMiddleware;
75-
this.deprecatedNetworkVersions = {};
76-
77-
// TODO: Replace "any" with type
78-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
79-
const { NetworkController } = Engine.context as any;
80-
81-
const { provider, blockTracker } =
82-
NetworkController.getProviderAndBlockTracker();
83-
84-
this.#providerProxy = null;
85-
this.#blockTrackerProxy = null;
86-
87-
this.#setProvider(provider);
88-
this.#setBlockTracker(blockTracker);
89-
90-
this.#mux = setupMultiplex(this.stream);
59+
}: {
60+
snapId: SnapId;
61+
connectionStream: Duplex;
62+
getRPCMethodMiddleware: GetRPCMethodMiddleware;
63+
}) {
64+
Logger.log('[SNAP BRIDGE] Initializing SnapBridge for Snap:', snapId);
65+
66+
this.#snapId = snapId;
67+
this.#stream = connectionStream;
68+
this.#getRPCMethodMiddleware = getRPCMethodMiddleware;
9169
}
9270

93-
// TODO: Replace "any" with type
94-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
95-
#setProvider = (provider: any): void => {
96-
if (this.#providerProxy) {
97-
this.#providerProxy.setTarget(provider);
98-
} else {
99-
this.#providerProxy = createSwappableProxy(provider);
100-
}
101-
this.provider = provider;
102-
};
103-
104-
// TODO: Replace "any" with type
105-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
106-
#setBlockTracker = (blockTracker: any): void => {
107-
if (this.#blockTrackerProxy) {
108-
this.#blockTrackerProxy.setTarget(blockTracker);
109-
} else {
110-
this.#blockTrackerProxy = createEventEmitterProxy(blockTracker, {
111-
eventFilter: 'skipInternal',
112-
});
113-
}
114-
this.blockTracker = blockTracker;
115-
};
116-
117-
async getProviderState() {
71+
/**
72+
* Gets the provider state.
73+
* @returns An object containing the provider state.
74+
*/
75+
#getProviderState() {
11876
return {
119-
isUnlocked: this.isUnlocked(),
120-
...(await this.getProviderNetworkState(this.snapId)),
77+
isUnlocked: Engine.context.KeyringController.isUnlocked(),
12178
};
12279
}
12380

124-
setupProviderConnection = () => {
125-
Logger.log('[SNAP BRIDGE LOG] Engine+setupProviderConnection');
126-
const outStream = this.#mux.createStream('metamask-provider');
127-
const engine = this.setupProviderEngine();
81+
/**
82+
* Sets up the provider engine for the Snap.
83+
* @returns The configured JSON-RPC engine.
84+
*/
85+
#setupProviderEngine() {
86+
Logger.log('[SNAP BRIDGE] Setting up provider engine');
12887

129-
const providerStream = createEngineStream({ engine });
130-
// TODO: Replace "any" with type
131-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
132-
pump(outStream, providerStream, outStream, (err: any) => {
133-
engine.destroy();
134-
if (err) Logger.log('Error with provider stream conn', err);
135-
});
136-
};
137-
138-
setupProviderEngine = () => {
88+
const { context, controllerMessenger } = Engine;
89+
const { SelectedNetworkController, PermissionController } = context;
13990
const engine = new JsonRpcEngine();
14091

141-
// create filter polyfill middleware
142-
const filterMiddleware = createFilterMiddleware({
143-
provider: this.#providerProxy,
144-
blockTracker: this.#blockTrackerProxy,
145-
});
92+
const proxy = SelectedNetworkController.getProviderAndBlockTracker(
93+
this.#snapId,
94+
);
14695

147-
// create subscription polyfill middleware
148-
const subscriptionManager = createSubscriptionManager({
149-
provider: this.#providerProxy,
150-
blockTracker: this.#blockTrackerProxy,
151-
});
96+
const filterMiddleware = createFilterMiddleware(proxy);
15297

153-
// TODO: Replace "any" with type
154-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
155-
subscriptionManager.events.on('notification', (message: any) =>
98+
const subscriptionManager = createSubscriptionManager(proxy);
99+
subscriptionManager.events.on('notification', (message: Json) =>
156100
engine.emit('notification', message),
157101
);
158102

159-
engine.push(createOriginMiddleware({ origin: this.snapId }));
160-
engine.push(createSelectedNetworkMiddleware(Engine.controllerMessenger));
103+
engine.push(
104+
createOriginMiddleware({ origin: this.#snapId }) as JsonRpcMiddleware<
105+
JsonRpcParams,
106+
Json
107+
>,
108+
);
109+
110+
engine.push(
111+
createSelectedNetworkMiddleware(
112+
controllerMessenger as unknown as SelectedNetworkControllerMessenger,
113+
),
114+
);
161115

162116
// Filter and subscription polyfills
163117
engine.push(filterMiddleware);
164118
engine.push(subscriptionManager.middleware);
165119

166-
const { context, controllerMessenger } = Engine;
167-
const { PermissionController } = context;
168-
169-
if (isSnapPreinstalled(this.snapId)) {
120+
if (isSnapPreinstalled(this.#snapId)) {
170121
engine.push(
171122
createPreinstalledSnapsMiddleware({
172123
getPermissions: PermissionController.getPermissions.bind(
173124
PermissionController,
174-
this.snapId,
125+
this.#snapId,
175126
),
176127
getAllEvmAccounts: () =>
177128
controllerMessenger
178129
.call('AccountsController:listAccounts')
179-
.map((account) => account.address),
130+
.map((account: InternalAccount) => account.address),
180131
grantPermissions: (approvedPermissions) =>
181132
controllerMessenger.call('PermissionController:grantPermissions', {
182133
approvedPermissions,
183-
subject: { origin: this.snapId },
134+
subject: { origin: this.#snapId },
184135
}),
185136
}),
186137
);
187138
}
188139

189140
engine.push(
190141
PermissionController.createPermissionMiddleware({
191-
origin: this.snapId,
142+
origin: this.#snapId,
192143
}),
193144
);
194145

195146
engine.push(
196147
snapMethodMiddlewareBuilder(
197148
context,
198149
controllerMessenger,
199-
this.snapId,
150+
this.#snapId,
200151
SubjectType.Snap,
201152
),
202153
);
203154

204155
// User-Facing RPC methods
205156
engine.push(
206-
this.getRPCMethodMiddleware({
207-
hostname: this.snapId,
208-
getProviderState: this.getProviderState.bind(this),
157+
this.#getRPCMethodMiddleware({
158+
hostname: this.#snapId,
159+
getProviderState: this.#getProviderState.bind(this),
209160
}),
210161
);
211162

212163
// Forward to metamask primary provider
213-
engine.push(providerAsMiddleware(this.#providerProxy));
164+
engine.push(providerAsMiddleware(proxy.provider));
165+
214166
return engine;
215-
};
167+
}
216168

217-
isUnlocked = (): boolean => {
218-
// TODO: Replace "any" with type
219-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
220-
const { KeyringController } = Engine.context as any;
221-
return KeyringController.isUnlocked();
222-
};
169+
/**
170+
* Sets up the provider connection for the Snap.
171+
*/
172+
setupProviderConnection() {
173+
Logger.log('[SNAP BRIDGE] Setting up provider connection');
223174

224-
async getProviderNetworkState(origin: string) {
225-
const networkClientId = Engine.controllerMessenger.call(
226-
'SelectedNetworkController:getNetworkClientIdForDomain',
227-
origin,
228-
);
175+
const mux = setupMultiplex(this.#stream);
176+
const stream = mux.createStream('metamask-provider');
229177

230-
const networkClient = Engine.controllerMessenger.call(
231-
'NetworkController:getNetworkClientById',
232-
networkClientId,
233-
);
178+
const engine = this.#setupProviderEngine();
234179

235-
const { chainId } = networkClient.configuration;
180+
const providerStream = createEngineStream({ engine });
236181

237-
let networkVersion = this.deprecatedNetworkVersions[networkClientId];
238-
if (!networkVersion) {
239-
const ethQuery = new EthQuery(networkClient.provider);
240-
networkVersion = await new Promise((resolve) => {
241-
ethQuery.sendAsync({ method: 'net_version' }, (error, result) => {
242-
if (error) {
243-
console.error(error);
244-
resolve(null);
245-
} else {
246-
resolve(result);
247-
}
248-
});
249-
});
250-
this.deprecatedNetworkVersions[networkClientId] = networkVersion;
251-
}
182+
pump(stream, providerStream, stream, (error: Error | null) => {
183+
engine.destroy();
252184

253-
return {
254-
chainId,
255-
networkVersion: networkVersion ?? 'loading',
256-
};
185+
if (error) {
186+
Logger.log('[SNAP BRIDGE] Error with provider stream:', error);
187+
}
188+
});
257189
}
258190
}
259-
///: END:ONLY_INCLUDE_IF

0 commit comments

Comments
 (0)