Skip to content

Commit 34b8768

Browse files
ffmcgee725adonesky1jiexi
authored
refactor: Lazy-load MWP packages + eciesjs/KeyManager behind dynamic import() (#244)
* refactor: lazy load MWP packages + eciesjs/KeyManager behind dynamic import() * address code review feedback * minor adjustment * Update packages/connect-multichain/CHANGELOG.md Co-authored-by: Alex Donesky <adonesky@gmail.com> * address code review * fix changelog * cleanup transportType getter * lint --------- Co-authored-by: Alex Donesky <adonesky@gmail.com> Co-authored-by: Jiexi Luan <jiexiluan@gmail.com>
1 parent 145c4bf commit 34b8768

5 files changed

Lines changed: 91 additions & 64 deletions

File tree

packages/connect-multichain/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Changed
1111

12+
- Lazy-load MWP transport dependencies: `@metamask/mobile-wallet-protocol-core`, `@metamask/mobile-wallet-protocol-dapp-client`, and `eciesjs` are now dynamically imported only when MWP transport is actually used, allowing bundlers to code-split the entire MWP + crypto dependency tree for consumers who only use the browser extension flow ([#244](https://github.com/MetaMask/connect-monorepo/pull/244))
1213
- Tighten `MultichainApiClientWrapperTransport` connected transport check on `wallet_getSession` and `wallet_invokeMethod`. Remove defined transport check on `wallet_revokeSession`. ([#280](https://github.com/MetaMask/connect-monorepo/pull/280))
1314

1415
## [0.12.1]

packages/connect-multichain/src/domain/multichain/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import type {
77
import type { CaipAccountId, Json } from '@metamask/utils';
88

99
import { EventEmitter, type SDKEvents } from '../events';
10-
import type { StoreClient } from '../store/client';
1110
import type { InvokeMethodOptions, RPCAPI, Scope } from './api/types';
1211
import type {
1312
ExtendedTransport,
1413
MergeableMultichainOptions,
1514
MultichainOptions,
1615
} from './types';
16+
import type { StoreClient } from '../store/client';
1717

1818
export type ConnectionStatus =
1919
| 'pending'

packages/connect-multichain/src/multichain/index.ts

Lines changed: 45 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,8 @@
44
/* eslint-disable promise/always-return -- Event handlers */
55
/* eslint-disable no-async-promise-executor -- Async promise executor needed for complex flow */
66
import { analytics } from '@metamask/analytics';
7-
import {
8-
ErrorCode,
9-
ProtocolError,
10-
type SessionRequest,
11-
SessionStore,
12-
WebSocketTransport,
13-
} from '@metamask/mobile-wallet-protocol-core';
14-
import { DappClient } from '@metamask/mobile-wallet-protocol-dapp-client';
7+
import type { SessionRequest } from '@metamask/mobile-wallet-protocol-core';
8+
import type { DappClient } from '@metamask/mobile-wallet-protocol-dapp-client';
159
import {
1610
type SessionProperties,
1711
getMultichainClient,
@@ -59,8 +53,6 @@ import { RpcClient } from './rpc/handlers/rpcClient';
5953
import { RequestRouter } from './rpc/requestRouter';
6054
import { DefaultTransport } from './transports/default';
6155
import { MultichainApiClientWrapperTransport } from './transports/multichainApiClientWrapper';
62-
import { MWPTransport } from './transports/mwp';
63-
import { keymanager } from './transports/mwp/KeyManager';
6456
import {
6557
getDappId,
6658
getGlobalObject,
@@ -90,6 +82,8 @@ export class MetaMaskConnectMultichain extends MultichainCore {
9082

9183
#beforeUnloadListener: (() => void) | undefined;
9284

85+
#transportType?: TransportType;
86+
9387
public _status: ConnectionStatus = 'pending';
9488

9589
#listener: (() => void | Promise<void>) | undefined;
@@ -126,14 +120,12 @@ export class MetaMaskConnectMultichain extends MultichainCore {
126120
return this.#dappClient;
127121
}
128122

129-
get storage(): StoreClient {
130-
return this.options.storage;
123+
get transportType(): TransportType {
124+
return this.#transportType ?? TransportType.UNKNOWN;
131125
}
132126

133-
get transportType(): TransportType {
134-
return this.#transport instanceof MWPTransport
135-
? TransportType.MWP
136-
: TransportType.Browser;
127+
get storage(): StoreClient {
128+
return this.options.storage;
137129
}
138130

139131
readonly #sdkInfo = `Sdk/Javascript SdkVersion/${getVersion()} Platform/${getPlatformType()} dApp/${this.options.dapp.url ?? this.options.dapp.name} dAppTitle/${this.options.dapp.name}`;
@@ -284,16 +276,15 @@ export class MetaMaskConnectMultichain extends MultichainCore {
284276
}
285277
}
286278

287-
async #getStoredTransport(): Promise<
288-
DefaultTransport | MWPTransport | undefined
289-
> {
279+
async #getStoredTransport(): Promise<ExtendedTransport | undefined> {
290280
const transportType = await this.storage.getTransport();
291281
const hasExtensionInstalled = await hasExtension();
292282
if (transportType) {
293283
if (transportType === TransportType.Browser) {
294284
if (hasExtensionInstalled) {
295285
const apiTransport = new DefaultTransport();
296286
this.#transport = apiTransport;
287+
this.#transportType = TransportType.Browser;
297288
this.#providerTransportWrapper.setupTransportNotificationListener();
298289
this.#listener = apiTransport.onNotification(
299290
this.#onTransportNotification.bind(this),
@@ -303,9 +294,11 @@ export class MetaMaskConnectMultichain extends MultichainCore {
303294
} else if (transportType === TransportType.MWP) {
304295
const { adapter: kvstore } = this.options.storage;
305296
const dappClient = await this.#createDappClient();
297+
const { MWPTransport } = await import('./transports/mwp');
306298
const apiTransport = new MWPTransport(dappClient, kvstore);
307299
this.#dappClient = dappClient;
308300
this.#transport = apiTransport;
301+
this.#transportType = TransportType.MWP;
309302
this.#providerTransportWrapper.setupTransportNotificationListener();
310303
this.#listener = apiTransport.onNotification(
311304
this.#onTransportNotification.bind(this),
@@ -327,7 +320,7 @@ export class MetaMaskConnectMultichain extends MultichainCore {
327320
await this.transport.connect();
328321
}
329322
this.status = 'connected';
330-
if (this.transport instanceof MWPTransport) {
323+
if (this.#transportType === TransportType.MWP) {
331324
await this.storage.setTransport(TransportType.MWP);
332325
} else {
333326
await this.storage.setTransport(TransportType.Browser);
@@ -374,32 +367,45 @@ export class MetaMaskConnectMultichain extends MultichainCore {
374367
}
375368

376369
async #createDappClient(): Promise<DappClient> {
370+
const [mwpCore, { DappClient: DappClientClass }, { createKeyManager }] =
371+
await Promise.all([
372+
import('@metamask/mobile-wallet-protocol-core'),
373+
import('@metamask/mobile-wallet-protocol-dapp-client'),
374+
import('./transports/mwp/KeyManager'),
375+
]);
376+
const keymanager = await createKeyManager();
377+
377378
const { adapter: kvstore } = this.options.storage;
378-
const sessionstore = await SessionStore.create(kvstore);
379+
const sessionstore = await mwpCore.SessionStore.create(kvstore);
379380
const websocket =
380381
// eslint-disable-next-line no-negated-condition
381382
typeof window !== 'undefined'
382383
? WebSocket
383384
: (await import('ws')).WebSocket;
384-
const transport = await WebSocketTransport.create({
385+
const transport = await mwpCore.WebSocketTransport.create({
385386
url: MWP_RELAY_URL,
386387
kvstore,
387388
websocket,
388389
});
389-
const dappClient = new DappClient({ transport, sessionstore, keymanager });
390+
const dappClient = new DappClientClass({
391+
transport,
392+
sessionstore,
393+
keymanager,
394+
});
390395
return dappClient;
391396
}
392397

393398
async #setupMWP(): Promise<void> {
394-
if (this.#transport instanceof MWPTransport) {
399+
if (this.#transportType === TransportType.MWP) {
395400
return;
396401
}
397-
// Only setup MWP if it is not already mwp
398402
const { adapter: kvstore } = this.options.storage;
399403
const dappClient = await this.#createDappClient();
400404
this.#dappClient = dappClient;
405+
const { MWPTransport } = await import('./transports/mwp');
401406
const apiTransport = new MWPTransport(dappClient, kvstore);
402407
this.#transport = apiTransport;
408+
this.#transportType = TransportType.MWP;
403409
this.#providerTransportWrapper.setupTransportNotificationListener();
404410
this.#listener = this.transport.onNotification(
405411
this.#onTransportNotification.bind(this),
@@ -474,8 +480,10 @@ export class MetaMaskConnectMultichain extends MultichainCore {
474480
this.status = 'connected';
475481
await this.storage.setTransport(TransportType.MWP);
476482
} catch (error) {
483+
const { ProtocolError, ErrorCode } = await import(
484+
'@metamask/mobile-wallet-protocol-core'
485+
);
477486
if (error instanceof ProtocolError) {
478-
// Ignore Request expired errors to allow modal to regenerate expired qr codes
479487
if (error.code !== ErrorCode.REQUEST_EXPIRED) {
480488
this.status = 'disconnected';
481489
// Close the modal on error
@@ -586,6 +594,9 @@ export class MetaMaskConnectMultichain extends MultichainCore {
586594
resolve();
587595
})
588596
.catch(async (error) => {
597+
const { ProtocolError } = await import(
598+
'@metamask/mobile-wallet-protocol-core'
599+
);
589600
if (error instanceof ProtocolError) {
590601
// In headless mode, we don't auto-regenerate QR codes
591602
// since there's no modal to display them
@@ -604,8 +615,8 @@ export class MetaMaskConnectMultichain extends MultichainCore {
604615
async #setupDefaultTransport(
605616
options: { persist?: boolean } = { persist: true },
606617
): Promise<DefaultTransport> {
607-
if (this.#transport instanceof DefaultTransport) {
608-
return this.#transport;
618+
if (this.#transportType === TransportType.Browser) {
619+
return this.#transport as DefaultTransport;
609620
}
610621

611622
if (options?.persist) {
@@ -616,6 +627,7 @@ export class MetaMaskConnectMultichain extends MultichainCore {
616627
this.#onTransportNotification.bind(this),
617628
);
618629
this.#transport = transport;
630+
this.#transportType = TransportType.Browser;
619631
this.#providerTransportWrapper.setupTransportNotificationListener();
620632
return transport;
621633
}
@@ -763,7 +775,7 @@ export class MetaMaskConnectMultichain extends MultichainCore {
763775
): Promise<void> {
764776
if (
765777
this.status === 'connecting' &&
766-
this.transportType === TransportType.MWP
778+
this.#transportType === TransportType.MWP
767779
) {
768780
await this.#openConnectDeeplinkIfNeeded();
769781
throw new Error(
@@ -834,7 +846,7 @@ export class MetaMaskConnectMultichain extends MultichainCore {
834846
forceRequest,
835847
})
836848
.then(async () => {
837-
if (this.#transport instanceof MWPTransport) {
849+
if (this.#transportType === TransportType.MWP) {
838850
return this.storage.setTransport(TransportType.MWP);
839851
}
840852
return this.storage.setTransport(TransportType.Browser);
@@ -951,12 +963,13 @@ export class MetaMaskConnectMultichain extends MultichainCore {
951963

952964
// We want to leave the DefaultTransport instance connected so that we can
953965
// still listen for wallet_sessionChanged events.
954-
if (this.transportType !== TransportType.Browser) {
966+
if (this.#transportType !== TransportType.Browser) {
955967
await this.#listener?.();
956968
this.#beforeUnloadListener?.();
957969
this.#listener = undefined;
958970
this.#beforeUnloadListener = undefined;
959971
this.#transport = undefined;
972+
this.#transportType = undefined;
960973
this.#providerTransportWrapper.clearTransportNotificationListener();
961974
this.#dappClient = undefined;
962975
}
@@ -973,7 +986,7 @@ export class MetaMaskConnectMultichain extends MultichainCore {
973986
transport,
974987
rpcClient,
975988
options,
976-
this.transportType,
989+
this.#transportType ?? TransportType.UNKNOWN,
977990
);
978991
// TODO: need read only method support for solana
979992
return requestRouter.invokeMethod(request);

packages/connect-multichain/src/multichain/transports/mwp/KeyManager.ts

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,38 +4,49 @@ import type {
44
IKeyManager,
55
KeyPair,
66
} from '@metamask/mobile-wallet-protocol-core';
7-
import { decrypt, encrypt, PrivateKey, PublicKey } from 'eciesjs';
87

9-
class KeyManager implements IKeyManager {
10-
generateKeyPair(): KeyPair {
11-
const privateKey = new PrivateKey();
12-
return {
13-
privateKey: new Uint8Array(privateKey.secret),
14-
publicKey: privateKey.publicKey.toBytes(true),
15-
};
16-
}
8+
/**
9+
* Creates an {@link IKeyManager} backed by the `eciesjs` library.
10+
*
11+
* The factory dynamically imports `eciesjs` so the heavy crypto dependency is
12+
* only loaded when MWP transport is actually used. The returned object closes
13+
* over the imported symbols, allowing synchronous methods like
14+
* `generateKeyPair` and `validatePeerKey` to work without a second await.
15+
*
16+
* @returns A ready-to-use key manager instance.
17+
*/
18+
export async function createKeyManager(): Promise<IKeyManager> {
19+
const { decrypt, encrypt, PrivateKey, PublicKey } = await import('eciesjs');
1720

18-
async encrypt(
19-
plaintext: string,
20-
theirPublicKey: Uint8Array,
21-
): Promise<string> {
22-
const plaintextBuffer = Buffer.from(plaintext, 'utf8');
23-
const encryptedBuffer = encrypt(theirPublicKey, plaintextBuffer);
24-
return encryptedBuffer.toString('base64');
25-
}
21+
return {
22+
generateKeyPair(): KeyPair {
23+
const privateKey = new PrivateKey();
24+
return {
25+
privateKey: new Uint8Array(privateKey.secret),
26+
publicKey: privateKey.publicKey.toBytes(true),
27+
};
28+
},
2629

27-
async decrypt(
28-
encryptedB64: string,
29-
myPrivateKey: Uint8Array,
30-
): Promise<string> {
31-
const encryptedBuffer = Buffer.from(encryptedB64, 'base64');
32-
const decryptedBuffer = await decrypt(myPrivateKey, encryptedBuffer);
33-
return Buffer.from(decryptedBuffer).toString('utf8');
34-
}
30+
async encrypt(
31+
plaintext: string,
32+
theirPublicKey: Uint8Array,
33+
): Promise<string> {
34+
const plaintextBuffer = Buffer.from(plaintext, 'utf8');
35+
const encryptedBuffer = encrypt(theirPublicKey, plaintextBuffer);
36+
return encryptedBuffer.toString('base64');
37+
},
3538

36-
validatePeerKey(key: Uint8Array): void {
37-
PublicKey.fromHex(Buffer.from(key).toString('hex'));
38-
}
39-
}
39+
async decrypt(
40+
encryptedB64: string,
41+
myPrivateKey: Uint8Array,
42+
): Promise<string> {
43+
const encryptedBuffer = Buffer.from(encryptedB64, 'base64');
44+
const decryptedBuffer = await decrypt(myPrivateKey, encryptedBuffer);
45+
return Buffer.from(decryptedBuffer).toString('utf8');
46+
},
4047

41-
export const keymanager = new KeyManager();
48+
validatePeerKey(key: Uint8Array): void {
49+
PublicKey.fromHex(Buffer.from(key).toString('hex'));
50+
},
51+
};
52+
}

packages/connect-multichain/src/multichain/transports/mwp/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import type {
1616
Session,
1717
SessionRequest,
1818
} from '@metamask/mobile-wallet-protocol-core';
19-
import { SessionStore } from '@metamask/mobile-wallet-protocol-core';
2019
import type { DappClient } from '@metamask/mobile-wallet-protocol-dapp-client';
2120
import {
2221
type SessionProperties,
@@ -813,6 +812,9 @@ export class MWPTransport implements ExtendedTransport {
813812

814813
async getActiveSession(): Promise<Session | undefined> {
815814
const { kvstore } = this;
815+
const { SessionStore } = await import(
816+
'@metamask/mobile-wallet-protocol-core'
817+
);
816818
const sessionStore = await SessionStore.create(kvstore);
817819

818820
try {

0 commit comments

Comments
 (0)