From 60739fe04a72469f8fd65a9f205cd1863d2fdd75 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 3 Feb 2026 10:48:29 -0800 Subject: [PATCH 001/103] add singleton to env entrypoint --- .../connect-multichain/src/index.browser.ts | 67 +++++++++++++------ .../connect-multichain/src/index.native.ts | 67 +++++++++++++------ packages/connect-multichain/src/index.node.ts | 67 +++++++++++++------ 3 files changed, 138 insertions(+), 63 deletions(-) diff --git a/packages/connect-multichain/src/index.browser.ts b/packages/connect-multichain/src/index.browser.ts index a18b7a5d..a9ed46b7 100644 --- a/packages/connect-multichain/src/index.browser.ts +++ b/packages/connect-multichain/src/index.browser.ts @@ -2,7 +2,11 @@ // Buffer polyfill must be imported first to set up globalThis.Buffer import './polyfills/buffer-shim'; -import type { CreateMultichainFN, StoreClient } from './domain'; +import type { + CreateMultichainFN, + MultichainCore, + StoreClient, +} from './domain'; import { enableDebug } from './domain'; import { MetaMaskConnectMultichain } from './multichain'; import { Store } from './store'; @@ -10,27 +14,48 @@ import { ModalFactory } from './ui'; export * from './domain'; +const SINGLETON_KEY = '__METAMASK_CONNECT_MULTICHAIN_SINGLETON__'; + +declare global { + // eslint-disable-next-line no-var + var __METAMASK_CONNECT_MULTICHAIN_SINGLETON__: + | Promise + | undefined; +} + export const createMultichainClient: CreateMultichainFN = async (options) => { - if (options.debug) { - enableDebug('metamask-sdk:*'); + // Return existing singleton if available + const existingSingleton = globalThis[SINGLETON_KEY]; + if (existingSingleton) { + return existingSingleton; } - const uiModules = await import('./ui/modals/web'); - let storage: StoreClient; - if (options.storage) { - storage = options.storage; - } else { - const { StoreAdapterWeb } = await import('./store/adapters/web'); - const adapter = new StoreAdapterWeb(); - storage = new Store(adapter); - } - const factory = new ModalFactory(uiModules); - return MetaMaskConnectMultichain.create({ - ...options, - storage, - ui: { - ...options.ui, - factory, - }, - }); + // Store the promise immediately to prevent concurrent calls from creating multiple instances + const instancePromise = (async () => { + if (options.debug) { + enableDebug('metamask-sdk:*'); + } + + const uiModules = await import('./ui/modals/web'); + let storage: StoreClient; + if (options.storage) { + storage = options.storage; + } else { + const { StoreAdapterWeb } = await import('./store/adapters/web'); + const adapter = new StoreAdapterWeb(); + storage = new Store(adapter); + } + const factory = new ModalFactory(uiModules); + return MetaMaskConnectMultichain.create({ + ...options, + storage, + ui: { + ...options.ui, + factory, + }, + }); + })(); + + globalThis[SINGLETON_KEY] = instancePromise; + return instancePromise; }; diff --git a/packages/connect-multichain/src/index.native.ts b/packages/connect-multichain/src/index.native.ts index 2635133c..9a4674c2 100644 --- a/packages/connect-multichain/src/index.native.ts +++ b/packages/connect-multichain/src/index.native.ts @@ -2,7 +2,11 @@ // Buffer polyfill must be imported first to set up global.Buffer import './polyfills/buffer-shim'; -import type { CreateMultichainFN, StoreClient } from './domain'; +import type { + CreateMultichainFN, + MultichainCore, + StoreClient, +} from './domain'; import { enableDebug } from './domain'; import { MetaMaskConnectMultichain } from './multichain'; import { Store } from './store'; @@ -10,27 +14,48 @@ import { ModalFactory } from './ui/index.native'; export * from './domain'; +const SINGLETON_KEY = '__METAMASK_CONNECT_MULTICHAIN_SINGLETON__'; + +declare global { + // eslint-disable-next-line no-var + var __METAMASK_CONNECT_MULTICHAIN_SINGLETON__: + | Promise + | undefined; +} + export const createMultichainClient: CreateMultichainFN = async (options) => { - if (options.debug) { - enableDebug('metamask-sdk:*'); + // Return existing singleton if available + const existingSingleton = global[SINGLETON_KEY]; + if (existingSingleton) { + return existingSingleton; } - const uiModules = await import('./ui/modals/rn'); - let storage: StoreClient; - if (options.storage) { - storage = options.storage; - } else { - const { StoreAdapterRN } = await import('./store/adapters/rn'); - const adapter = new StoreAdapterRN(); - storage = new Store(adapter); - } - const factory = new ModalFactory(uiModules); - return MetaMaskConnectMultichain.create({ - ...options, - storage, - ui: { - ...options.ui, - factory, - }, - }); + // Store the promise immediately to prevent concurrent calls from creating multiple instances + const instancePromise = (async () => { + if (options.debug) { + enableDebug('metamask-sdk:*'); + } + + const uiModules = await import('./ui/modals/rn'); + let storage: StoreClient; + if (options.storage) { + storage = options.storage; + } else { + const { StoreAdapterRN } = await import('./store/adapters/rn'); + const adapter = new StoreAdapterRN(); + storage = new Store(adapter); + } + const factory = new ModalFactory(uiModules); + return MetaMaskConnectMultichain.create({ + ...options, + storage, + ui: { + ...options.ui, + factory, + }, + }); + })(); + + global[SINGLETON_KEY] = instancePromise; + return instancePromise; }; diff --git a/packages/connect-multichain/src/index.node.ts b/packages/connect-multichain/src/index.node.ts index 8c929f1b..239bb99c 100644 --- a/packages/connect-multichain/src/index.node.ts +++ b/packages/connect-multichain/src/index.node.ts @@ -1,4 +1,8 @@ -import type { CreateMultichainFN, StoreClient } from './domain'; +import type { + CreateMultichainFN, + MultichainCore, + StoreClient, +} from './domain'; import { enableDebug } from './domain'; import { MetaMaskConnectMultichain } from './multichain'; import { Store } from './store'; @@ -6,27 +10,48 @@ import { ModalFactory } from './ui'; export * from './domain'; +const SINGLETON_KEY = '__METAMASK_CONNECT_MULTICHAIN_SINGLETON__'; + +declare global { + // eslint-disable-next-line no-var + var __METAMASK_CONNECT_MULTICHAIN_SINGLETON__: + | Promise + | undefined; +} + export const createMultichainClient: CreateMultichainFN = async (options) => { - if (options.debug) { - enableDebug('metamask-sdk:*'); + // Return existing singleton if available + const existingSingleton = globalThis[SINGLETON_KEY]; + if (existingSingleton) { + return existingSingleton; } - const uiModules = await import('./ui/modals/node'); - let storage: StoreClient; - if (options.storage) { - storage = options.storage; - } else { - const { StoreAdapterNode } = await import('./store/adapters/node'); - const adapter = new StoreAdapterNode(); - storage = new Store(adapter); - } - const factory = new ModalFactory(uiModules); - return MetaMaskConnectMultichain.create({ - ...options, - storage, - ui: { - ...options.ui, - factory, - }, - }); + // Store the promise immediately to prevent concurrent calls from creating multiple instances + const instancePromise = (async () => { + if (options.debug) { + enableDebug('metamask-sdk:*'); + } + + const uiModules = await import('./ui/modals/node'); + let storage: StoreClient; + if (options.storage) { + storage = options.storage; + } else { + const { StoreAdapterNode } = await import('./store/adapters/node'); + const adapter = new StoreAdapterNode(); + storage = new Store(adapter); + } + const factory = new ModalFactory(uiModules); + return MetaMaskConnectMultichain.create({ + ...options, + storage, + ui: { + ...options.ui, + factory, + }, + }); + })(); + + globalThis[SINGLETON_KEY] = instancePromise; + return instancePromise; }; From bda12836f1fc89eb0dc0801090c8f481ab4b6a63 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 3 Feb 2026 11:16:26 -0800 Subject: [PATCH 002/103] remove window.mmsdk setting --- .../src/multichain/index.ts | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/packages/connect-multichain/src/multichain/index.ts b/packages/connect-multichain/src/multichain/index.ts index a90e2f1d..31bc5355 100644 --- a/packages/connect-multichain/src/multichain/index.ts +++ b/packages/connect-multichain/src/multichain/index.ts @@ -260,25 +260,16 @@ export class MetaMaskConnectMultichain extends MultichainCore { async #init(): Promise { try { - // @ts-expect-error mmsdk should be accessible - if (typeof window !== 'undefined' && window.mmsdk?.isInitialized) { - logger('MetaMaskSDK: init already initialized'); - } else { - await this.#setupAnalytics(); - await this.#setupTransport(); - try { - const baseProps = await getBaseAnalyticsProperties( - this.options, - this.storage, - ); - analytics.track('mmconnect_initialized', baseProps); - } catch (error) { - logger('Error tracking initialized event', error); - } - if (typeof window !== 'undefined') { - // @ts-expect-error mmsdk should be accessible - window.mmsdk = this; - } + await this.#setupAnalytics(); + await this.#setupTransport(); + try { + const baseProps = await getBaseAnalyticsProperties( + this.options, + this.storage, + ); + analytics.track('mmconnect_initialized', baseProps); + } catch (error) { + logger('Error tracking initialized event', error); } } catch (error) { await this.storage.removeTransport(); From f69f2459bc9dbf24181b3b3b4e7db38208f87818 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 3 Feb 2026 16:00:50 -0800 Subject: [PATCH 003/103] WIP --- packages/connect-evm/src/connect.ts | 207 ++++++++++-------- .../src/domain/multichain/index.ts | 2 + .../src/multichain/index.ts | 18 ++ 3 files changed, 135 insertions(+), 92 deletions(-) diff --git a/packages/connect-evm/src/connect.ts b/packages/connect-evm/src/connect.ts index 5f1810fe..b1edfc8a 100644 --- a/packages/connect-evm/src/connect.ts +++ b/packages/connect-evm/src/connect.ts @@ -1,6 +1,6 @@ /* eslint-disable no-restricted-syntax -- Private class properties use established patterns */ import { analytics } from '@metamask/analytics'; -import type { Caip25CaveatValue } from '@metamask/chain-agnostic-permission'; +import { getEthAccounts, InternalScopesObject, type Caip25CaveatValue } from '@metamask/chain-agnostic-permission'; import type { ConnectionStatus, MultichainCore, @@ -101,6 +101,9 @@ export class MetamaskConnectEVM { /** The clean-up function for the notification handler */ #removeNotificationHandler?: () => void; + /** The current connection status */ + #status: 'disconnected' | 'connected' | 'connecting' = 'disconnected'; + /** * Creates a new MetamaskConnectEVM instance. * Use the static `create()` method instead to ensure proper async initialization. @@ -128,6 +131,18 @@ export class MetamaskConnectEVM { this.#sessionChangedHandler = (session): void => { logger('event: wallet_sessionChanged', session); this.#sessionScopes = session?.sessionScopes ?? {}; + const permittedChainIds = getPermittedEthChainIds(this.#sessionScopes); + if (permittedChainIds.length === 0) { + this.#onDisconnect(); + } + else { + // Need to somehow make an eth_accounts call here + this.#onConnect({ + chainId: permittedChainIds[0], + // Fix this type + accounts: getEthAccounts({requiredScopes: {}, optionalScopes: this.#sessionScopes as InternalScopesObject}), + }); + } }; this.#core.on( 'wallet_sessionChanged', @@ -325,14 +340,41 @@ export class MetamaskConnectEVM { throw new Error('chainIds must be an array of at least one chain ID'); } - const caipChainIds = Array.from( + // Get existing CAIP chain IDs and account IDs from sessionScopes + const existingCaipChainIds = Object.keys(this.#sessionScopes ?? {}); + // For permitted account ids, try to find account address in scopes if possible + const existingCaipAccountIds: string[] = []; + if (this.#sessionScopes) { + for (const [caipChainId, accounts] of Object.entries(this.#sessionScopes)) { + if (accounts?.accounts && Array.isArray(accounts.accounts)) { + for (const acct of accounts.accounts) { + existingCaipAccountIds.push(`${caipChainId}:${acct}`); + } + } + } + } + + // Generate requested CAIP chain IDs from input, ensuring default is present + const requestedCaipChainIds = Array.from( new Set(chainIds.concat(DEFAULT_CHAIN_ID) ?? [DEFAULT_CHAIN_ID]), ).map((id) => `eip155:${hexToNumber(id)}`); - const caipAccountIds = account + // Merge existing and requested CAIP chain IDs, ensuring uniqueness + const caipChainIds = Array.from( + new Set([...existingCaipChainIds, ...requestedCaipChainIds]) + ); + + // Compose CAIP account IDs with provided account, merged with existing account IDs + const requestedCaipAccountIds = account ? caipChainIds.map((caipChainId) => `${caipChainId}:${account}`) : []; + const caipAccountIds = Array.from( + new Set([...existingCaipAccountIds, ...requestedCaipAccountIds]) + ); + this.#status = 'connecting'; + + try { await this.#core.connect( caipChainIds as Scope[], caipAccountIds as CaipAccountId[], @@ -340,57 +382,39 @@ export class MetamaskConnectEVM { forceRequest, ); - const hexPermittedChainIds = getPermittedEthChainIds(this.#sessionScopes); + // const hexPermittedChainIds = getPermittedEthChainIds(this.#sessionScopes); - const initialAccounts = await this.#core.transport.sendEip1193Message< - { method: 'eth_accounts'; params: [] }, - { result: string[]; id: number; jsonrpc: '2.0' } - >({ method: 'eth_accounts', params: [] }); + // const initialAccounts = await this.#core.transport.sendEip1193Message< + // { method: 'eth_accounts'; params: [] }, + // { result: string[]; id: number; jsonrpc: '2.0' } + // >({ method: 'eth_accounts', params: [] }); - const chainId = await this.#getSelectedChainId(hexPermittedChainIds); + // const chainId = await this.#getSelectedChainId(hexPermittedChainIds); - this.#onConnect({ - chainId, - accounts: initialAccounts.result as Address[], - }); - - // Remove previous notification handler if it exists - this.#removeNotificationHandler?.(); - - this.#removeNotificationHandler = this.#core.transport.onNotification( - (notification) => { - // @ts-expect-error TODO: address this - if (notification?.method === 'metamask_accountsChanged') { - // @ts-expect-error TODO: address this - const accounts = notification?.params; - logger('transport-event: accountsChanged', accounts); - this.#onAccountsChanged(accounts); - } + // this.#onConnect({ + // chainId, + // accounts: initialAccounts.result as Address[], + // }); - // @ts-expect-error TODO: address this - if (notification?.method === 'metamask_chainChanged') { - // @ts-expect-error TODO: address this - const notificationChainId = notification?.params?.chainId; - logger('transport-event: chainChanged', notificationChainId); - // Cache the chainId for persistence across page refreshes - this.#cacheChainId(notificationChainId).catch((error) => { - logger('Error caching chainId in notification handler', error); - }); - this.#onChainChanged(notificationChainId); - } - }, - ); logger('fulfilled-request: connect', { chainId: chainIds[0], accounts: this.#provider.accounts, }); - // TODO: update required here since accounts and chainId are now promises + // TODO: verify the events that set the provider properties have fired by now return { accounts: this.#provider.accounts, - chainId, + chainId: this.#provider.selectedChainId as Hex, }; + + } catch (error) { + logger('Error connecting to wallet', error); + throw error; + } + finally { + this.#status = 'disconnected'; + } } /** @@ -491,10 +515,10 @@ export class MetamaskConnectEVM { this.#core.off('wallet_sessionChanged', this.#sessionChangedHandler); this.#core.off('display_uri', this.#displayUriHandler); - if (this.#removeNotificationHandler) { - this.#removeNotificationHandler(); - this.#removeNotificationHandler = undefined; - } + // if (this.#removeNotificationHandler) { + // this.#removeNotificationHandler(); + // this.#removeNotificationHandler = undefined; + // } logger('fulfilled-request: disconnect'); } @@ -792,9 +816,43 @@ export class MetamaskConnectEVM { accounts, }; - this.#provider.emit('connect', data); - this.#eventHandlers?.connect?.(data); + if (this.#status !== 'connected') { + this.#status = 'connected'; + this.#provider.emit('connect', data); + this.#eventHandlers?.connect?.(data); + + this.#removeNotificationHandler?.(); + + // transport doesn't exist (throws) if we aren't connected. Should that be a + // required for using onNotification?... + // TODO: Verify if #core.on('metamask_accountsChanged') and #core.on('metamask_chainChanged') + // would work here instead + this.#removeNotificationHandler = this.#core.transport.onNotification( + (notification) => { + // @ts-expect-error TODO: address this + if (notification?.method === 'metamask_accountsChanged') { + // @ts-expect-error TODO: address this + const accounts = notification?.params; + logger('transport-event: accountsChanged', accounts); + this.#onAccountsChanged(accounts); + } + + // @ts-expect-error TODO: address this + if (notification?.method === 'metamask_chainChanged') { + // @ts-expect-error TODO: address this + const notificationChainId = notification?.params?.chainId; + logger('transport-event: chainChanged', notificationChainId); + // Cache the chainId for persistence across page refreshes + this.#cacheChainId(notificationChainId).catch((error) => { + logger('Error caching chainId in notification handler', error); + }); + this.#onChainChanged(notificationChainId); + } + }, + ); + } + // TODO: check if chain has changed and accounts have changed this.#onChainChanged(chainId); this.#onAccountsChanged(accounts); } @@ -804,6 +862,11 @@ export class MetamaskConnectEVM { * Also clears accounts by triggering an accountsChanged event with an empty array. */ #onDisconnect(): void { + if (this.#status === 'disconnected') { + return; + } + this.#status = 'disconnected'; + logger('handler: disconnect'); this.#provider.emit('disconnect'); this.#eventHandlers?.disconnect?.(); @@ -818,6 +881,10 @@ export class MetamaskConnectEVM { * @param uri - The deeplink URI to be displayed as a QR code */ #onDisplayUri(uri: string): void { + if (this.#status !== 'connecting') { + return; + } + logger('handler: display_uri', uri); this.#provider.emit('display_uri', uri); this.#eventHandlers?.displayUri?.(uri); @@ -833,51 +900,7 @@ export class MetamaskConnectEVM { * and trigger an accountsChanged event if the results are valid accounts. */ async #attemptSessionRecovery(): Promise { - // Skip session recovery if transport is not initialized yet. - // Transport is only initialized when there's a stored session or after connect() is called. - // Only attempt recovery if we're in a state where transport should be available. - if ( - this.#core.status !== 'connected' && - this.#core.status !== 'connecting' - ) { - return; - } - try { - const response = await this.#core.transport.request< - { method: 'wallet_getSession' }, - { - result: { sessionScopes: Caip25CaveatValue }; - id: number; - jsonrpc: '2.0'; - } - >({ - method: 'wallet_getSession', - }); - - const { sessionScopes } = response.result; - - this.#sessionScopes = sessionScopes; - const permittedChainIds = getPermittedEthChainIds(sessionScopes); - - // Instead of using the accounts we get back from calling `wallet_getSession` - // we get permitted accounts from `eth_accounts` to make sure we have them ordered by last selected account - // and correctly set the currently selected account for the dapp - const permittedAccounts = await this.#core.transport.sendEip1193Message< - { method: 'eth_accounts'; params: [] }, - { result: Address[]; id: number; jsonrpc: '2.0' } - >({ method: 'eth_accounts', params: [] }); - - const chainId = await this.#getSelectedChainId(permittedChainIds); - - if (permittedChainIds.length && permittedAccounts.result) { - this.#onConnect({ - chainId, - accounts: permittedAccounts.result, - }); - } - } catch (error) { - console.error('Error attempting session recovery', error); - } + return this.#core.emitSessionChanged() } /** diff --git a/packages/connect-multichain/src/domain/multichain/index.ts b/packages/connect-multichain/src/domain/multichain/index.ts index 0aa16dd8..7d0a48ba 100644 --- a/packages/connect-multichain/src/domain/multichain/index.ts +++ b/packages/connect-multichain/src/domain/multichain/index.ts @@ -70,6 +70,8 @@ export abstract class MultichainCore extends EventEmitter { abstract openDeeplinkIfNeeded(): void; + abstract emitSessionChanged(): Promise; + constructor(protected readonly options: MultichainOptions) { super(); } diff --git a/packages/connect-multichain/src/multichain/index.ts b/packages/connect-multichain/src/multichain/index.ts index 31bc5355..7748d901 100644 --- a/packages/connect-multichain/src/multichain/index.ts +++ b/packages/connect-multichain/src/multichain/index.ts @@ -664,6 +664,7 @@ export class MetaMaskConnectMultichain extends MultichainCore { }); } + // TODO: Make this merge the existing session scopes with the new ones?? // TODO: make this into param object async connect( scopes: Scope[], @@ -816,6 +817,7 @@ export class MetaMaskConnectMultichain extends MultichainCore { await this.storage.removeTransport(); this.emit('stateChanged', 'disconnected'); + this.emit('wallet_sessionChanged', { sessionScopes: {} }); this.#listener = undefined; this.#beforeUnloadListener = undefined; @@ -856,4 +858,20 @@ export class MetaMaskConnectMultichain extends MultichainCore { }, 10); // small delay to ensure the message encryption and dispatch completes } } + + async emitSessionChanged(): Promise { + if ( + this.status !== 'connected' && + this.status !== 'connecting' + ) { + this.emit('wallet_sessionChanged', { sessionScopes: {} }); + } else { + const response = await this.transport.request({ method: 'wallet_getSession' }) + if (response.result) { + this.emit('wallet_sessionChanged', response.result); + } else { + this.emit('wallet_sessionChanged', { sessionScopes: {} }); + } + } + } } From bc0e33fb801c0617d81a1a457bece6eca20b52a9 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 4 Feb 2026 15:53:33 -0800 Subject: [PATCH 004/103] make MultichainClient.connect additive --- packages/connect-evm/src/connect.ts | 28 +-------- .../src/multichain/index.ts | 57 +++++++++++++++---- 2 files changed, 49 insertions(+), 36 deletions(-) diff --git a/packages/connect-evm/src/connect.ts b/packages/connect-evm/src/connect.ts index e362d3ad..be463804 100644 --- a/packages/connect-evm/src/connect.ts +++ b/packages/connect-evm/src/connect.ts @@ -341,37 +341,13 @@ export class MetamaskConnectEVM { throw new Error('chainIds must be an array of at least one chain ID'); } - // Get existing CAIP chain IDs and account IDs from sessionScopes - const existingCaipChainIds = Object.keys(this.#sessionScopes ?? {}); - // For permitted account ids, try to find account address in scopes if possible - const existingCaipAccountIds: string[] = []; - if (this.#sessionScopes) { - for (const [caipChainId, accounts] of Object.entries(this.#sessionScopes)) { - if (accounts?.accounts && Array.isArray(accounts.accounts)) { - for (const acct of accounts.accounts) { - existingCaipAccountIds.push(`${caipChainId}:${acct}`); - } - } - } - } - - // Generate requested CAIP chain IDs from input, ensuring default is present - const requestedCaipChainIds = Array.from( + const caipChainIds = Array.from( new Set(chainIds.concat(DEFAULT_CHAIN_ID) ?? [DEFAULT_CHAIN_ID]), ).map((id) => `eip155:${hexToNumber(id)}`); - // Merge existing and requested CAIP chain IDs, ensuring uniqueness - const caipChainIds = Array.from( - new Set([...existingCaipChainIds, ...requestedCaipChainIds]) - ); - - // Compose CAIP account IDs with provided account, merged with existing account IDs - const requestedCaipAccountIds = account + const caipAccountIds = account ? caipChainIds.map((caipChainId) => `${caipChainId}:${account}`) : []; - const caipAccountIds = Array.from( - new Set([...existingCaipAccountIds, ...requestedCaipAccountIds]) - ); this.#status = 'connecting'; diff --git a/packages/connect-multichain/src/multichain/index.ts b/packages/connect-multichain/src/multichain/index.ts index 7748d901..e1551d2f 100644 --- a/packages/connect-multichain/src/multichain/index.ts +++ b/packages/connect-multichain/src/multichain/index.ts @@ -713,18 +713,55 @@ export class MetaMaskConnectMultichain extends MultichainCore { logger('Error tracking connection_initiated event', error); } + let sessionData: SessionData = { + sessionScopes: {}, + sessionProperties: {}, + }; + if (this.status === 'connected') { + // Try to get current session scopes + const response = await this.transport.request({ + method: 'wallet_getSession', + }); + if (response.result) { + sessionData = response.result as SessionData; + } else { + // ??? + } + } + + // Get existing CAIP chain IDs and account IDs from sessionScopes + const existingCaipChainIds = Object.keys(sessionData.sessionScopes); + // For permitted account ids, try to find account address in scopes if possible + const existingCaipAccountIds: string[] = []; + Object.values(sessionData.sessionScopes).forEach((scopeObject) => { + if (scopeObject?.accounts && Array.isArray(scopeObject.accounts)) { + scopeObject.accounts.forEach((account) => { + existingCaipAccountIds.push(account); + }); + } + }); + + // TODO: Fix these types + const requestedScopes = Array.from(new Set([...existingCaipChainIds, ...scopes])) as Scope[] + const requestedCaipAccountIds = Array.from(new Set([...existingCaipAccountIds, ...caipAccountIds])) as CaipAccountId[] + const requestedSessionProperites = { + ...sessionData.sessionProperties, + ...sessionProperties, + } + + // Needed because empty object will cause wallet_createSession to return an error const nonEmptySessionProperites = - Object.keys(sessionProperties ?? {}).length > 0 - ? sessionProperties + Object.keys(requestedSessionProperites ?? {}).length > 0 + ? requestedSessionProperites : undefined; if (this.#transport?.isConnected() && !secure) { return this.#handleConnection( this.#transport .connect({ - scopes, - caipAccountIds, + scopes: requestedScopes, + caipAccountIds: requestedCaipAccountIds, sessionProperties: nonEmptySessionProperites, forceRequest, }) @@ -744,8 +781,8 @@ export class MetaMaskConnectMultichain extends MultichainCore { const defaultTransport = await this.#setupDefaultTransport(); return this.#handleConnection( defaultTransport.connect({ - scopes, - caipAccountIds, + scopes: requestedScopes, + caipAccountIds: requestedCaipAccountIds, sessionProperties: nonEmptySessionProperites, forceRequest, }), @@ -760,8 +797,8 @@ export class MetaMaskConnectMultichain extends MultichainCore { // Web transport has no initial payload return this.#handleConnection( defaultTransport.connect({ - scopes, - caipAccountIds, + scopes: requestedScopes, + caipAccountIds: requestedCaipAccountIds, sessionProperties: nonEmptySessionProperites, forceRequest, }), @@ -795,8 +832,8 @@ export class MetaMaskConnectMultichain extends MultichainCore { return this.#handleConnection( this.#showInstallModal( shouldShowInstallModal, - scopes, - caipAccountIds, + requestedScopes, + requestedCaipAccountIds, nonEmptySessionProperites, ), scopes, From 102720b01c9f053eb4b649f978eeed1d793a4607 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 4 Feb 2026 15:53:43 -0800 Subject: [PATCH 005/103] Fix multichain disconnect status bug --- packages/connect-multichain/src/multichain/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/connect-multichain/src/multichain/index.ts b/packages/connect-multichain/src/multichain/index.ts index e1551d2f..0452f0ca 100644 --- a/packages/connect-multichain/src/multichain/index.ts +++ b/packages/connect-multichain/src/multichain/index.ts @@ -853,7 +853,7 @@ export class MetaMaskConnectMultichain extends MultichainCore { await this.#transport?.disconnect(); await this.storage.removeTransport(); - this.emit('stateChanged', 'disconnected'); + this.status = 'disconnected'; this.emit('wallet_sessionChanged', { sessionScopes: {} }); this.#listener = undefined; From 1f95a073fd9b44cc4e2735b93282b84c3ed9fe06 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 4 Feb 2026 15:53:55 -0800 Subject: [PATCH 006/103] Leave comment for multichainApiWrapper listener bug --- .../multichain/transports/multichainApiClientWrapper/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/connect-multichain/src/multichain/transports/multichainApiClientWrapper/index.ts b/packages/connect-multichain/src/multichain/transports/multichainApiClientWrapper/index.ts index 10a6e1f9..88eee873 100644 --- a/packages/connect-multichain/src/multichain/transports/multichainApiClientWrapper/index.ts +++ b/packages/connect-multichain/src/multichain/transports/multichainApiClientWrapper/index.ts @@ -111,6 +111,7 @@ export class MultichainApiClientWrapperTransport implements Transport { }; } + // TODO: should not be two of these. Fix this return this.metamaskConnectMultichain.transport.onNotification(callback); } From 53107f63ca47177daee7d93861f260b35256f8a6 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 4 Feb 2026 15:54:10 -0800 Subject: [PATCH 007/103] emitSessionChanged on connect in multichainApiWrapper --- .../multichain/transports/multichainApiClientWrapper/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/connect-multichain/src/multichain/transports/multichainApiClientWrapper/index.ts b/packages/connect-multichain/src/multichain/transports/multichainApiClientWrapper/index.ts index 88eee873..4f8a9328 100644 --- a/packages/connect-multichain/src/multichain/transports/multichainApiClientWrapper/index.ts +++ b/packages/connect-multichain/src/multichain/transports/multichainApiClientWrapper/index.ts @@ -61,8 +61,7 @@ export class MultichainApiClientWrapperTransport implements Transport { async connect(): Promise { console.log('📚 connect'); - // noop - return Promise.resolve(); + return await this.metamaskConnectMultichain.emitSessionChanged(); } async disconnect(): Promise { From 55ec768a76abab4e97e1313fff7d8bc87062e489 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 4 Feb 2026 15:54:22 -0800 Subject: [PATCH 008/103] remove activeProviderStorage --- playground/browser-playground/src/App.tsx | 32 +---- .../src/sdk/LegacyEVMSDKProvider.tsx | 27 +--- .../src/sdk/SDKProvider.tsx | 16 +-- .../src/utils/activeProviderStorage.ts | 124 ------------------ .../src/wagmi/metamask-connector.ts | 1 + 5 files changed, 10 insertions(+), 190 deletions(-) delete mode 100644 playground/browser-playground/src/utils/activeProviderStorage.ts diff --git a/playground/browser-playground/src/App.tsx b/playground/browser-playground/src/App.tsx index f100632c..d6b02af9 100644 --- a/playground/browser-playground/src/App.tsx +++ b/playground/browser-playground/src/App.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useCallback } from 'react'; import type { Scope, SessionData } from '@metamask/connect-multichain'; import { hexToNumber, type CaipAccountId, type Hex } from '@metamask/utils'; -import { useAccount, useChainId, useConnect, useDisconnect } from 'wagmi'; +import { useAccount, useConnect, useDisconnect } from 'wagmi'; import { useWallet } from '@solana/wallet-adapter-react'; import { FEATURED_NETWORKS, @@ -14,11 +14,6 @@ import DynamicInputs, { INPUT_LABEL_TYPE } from './components/DynamicInputs'; import { ScopeCard } from './components/ScopeCard'; import { LegacyEVMCard } from './components/LegacyEVMCard'; import { WagmiCard } from './components/WagmiCard'; -import { - isProviderActive, - setProviderActive, - clearAllActiveProviders, -} from './utils/activeProviderStorage'; import { SolanaWalletCard } from './components/SolanaWalletCard'; import { Buffer } from 'buffer'; @@ -28,11 +23,6 @@ function App() { const [customScopes, setCustomScopes] = useState(['eip155:1']); const [caipAccountIds, setCaipAccountIds] = useState([]); - // Track whether wagmi should be shown based on localStorage - const [wagmiIsActiveProvider, setWagmiIsActiveProvider] = useState(() => - isProviderActive('wagmi'), - ); - const { error, status, @@ -50,7 +40,6 @@ function App() { disconnect: legacyDisconnect, } = useLegacyEVMSDK(); const { address: wagmiAddress, isConnected: wagmiConnected } = useAccount(); - const wagmiChainId = useChainId(); const { connectors, connectAsync: wagmiConnectAsync, @@ -58,15 +47,6 @@ function App() { } = useConnect(); const { disconnect: wagmiDisconnect } = useDisconnect(); - // On mount, check if wagmi is connected but not marked as active provider - // If so, disconnect wagmi to clear stale state - useEffect(() => { - if (wagmiConnected && !isProviderActive('wagmi')) { - // Wagmi thinks it's connected but our localStorage says it shouldn't be - // Disconnect to clear stale state - wagmiDisconnect(); - } - }, []); const { connected: solanaConnected, publicKey: solanaPublicKey, @@ -153,8 +133,6 @@ function App() { connector: metaMaskConnector, chainId, }); - setProviderActive('wagmi'); - setWagmiIsActiveProvider(true); } catch (error) { console.error('Wagmi connection error:', error); } @@ -183,10 +161,6 @@ function App() { status === 'disconnected' || status === 'pending' || status === 'loaded'; const disconnect = useCallback(async () => { - clearAllActiveProviders(); - setWagmiIsActiveProvider(false); - - // Disconnect all connections if connected if (isConnected) { await sdkDisconnect(); } @@ -276,7 +250,7 @@ function App() { )} - {(!wagmiConnected || !wagmiIsActiveProvider) && ( + {(!wagmiConnected) && (
diff --git a/playground/browser-playground/src/components/SolanaWalletCard.tsx b/playground/browser-playground/src/components/SolanaWalletCard.tsx index 14243aca..b55d14c2 100644 --- a/playground/browser-playground/src/components/SolanaWalletCard.tsx +++ b/playground/browser-playground/src/components/SolanaWalletCard.tsx @@ -1,8 +1,5 @@ import { useConnection, useWallet } from '@solana/wallet-adapter-react'; -import { - WalletDisconnectButton, - WalletMultiButton, -} from '@solana/wallet-adapter-react-ui'; +import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'; import { PublicKey, SystemProgram, Transaction, LAMPORTS_PER_SOL } from '@solana/web3.js'; import { useCallback, useState } from 'react'; @@ -12,7 +9,7 @@ import { useCallback, useState } from 'react'; */ export const SolanaWalletCard: React.FC = () => { const { connection } = useConnection(); - const { publicKey, connected, signMessage, signTransaction, sendTransaction } = + const { publicKey, connected, disconnect, signMessage, signTransaction, sendTransaction } = useWallet(); const [message, setMessage] = useState('Hello from MetaMask Connect!'); @@ -115,10 +112,21 @@ export const SolanaWalletCard: React.FC = () => { return (
-

- ☀️ - Solana Wallet -

+
+

+ ☀️ + Solana Wallet +

+ {connected && ( + + )} +
{/* Connection Status */} @@ -140,13 +148,11 @@ export const SolanaWalletCard: React.FC = () => { )} {/* Wallet Buttons */} -
- {!connected ? ( + {!connected && ( +
- ) : ( - - )} -
+
+ )} {/* Sign Message Section */} {connected && ( diff --git a/playground/browser-playground/src/components/WagmiCard.tsx b/playground/browser-playground/src/components/WagmiCard.tsx index ab9ebacd..91148529 100644 --- a/playground/browser-playground/src/components/WagmiCard.tsx +++ b/playground/browser-playground/src/components/WagmiCard.tsx @@ -7,6 +7,7 @@ import { useBlockNumber, useChainId, useConnectorClient, + useDisconnect, useSendTransaction, useSignMessage, useSwitchChain, @@ -17,6 +18,7 @@ import { TEST_IDS } from '@metamask/playground-ui'; export function WagmiCard() { const account = useAccount(); const chainId = useChainId(); + const { disconnect } = useDisconnect(); const { chains, switchChain } = useSwitchChain(); const { data: balance } = useBalance({ address: account.address }); const { data: blockNumber } = useBlockNumber({ watch: true }); @@ -60,6 +62,13 @@ export function WagmiCard() {

Wagmi Connection

+
From 92bf708c233047e29bf5bc56ce5978b7bad5397e Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 5 Feb 2026 14:12:42 -0800 Subject: [PATCH 012/103] actually make wallet_revokeSession call in MWP --- packages/connect-evm/src/connect.ts | 3 ++- .../src/multichain/transports/mwp/index.ts | 5 +++-- .../browser-playground/src/wagmi/metamask-connector.ts | 1 - 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/connect-evm/src/connect.ts b/packages/connect-evm/src/connect.ts index 43497c36..93cc6650 100644 --- a/packages/connect-evm/src/connect.ts +++ b/packages/connect-evm/src/connect.ts @@ -141,7 +141,7 @@ export class MetamaskConnectEVM { } else { // Need to somehow make an eth_accounts call here this.#onConnect({ - chainId: permittedChainIds[0], + chainId: permittedChainIds[0], // fix this to use cached chainId too? // Fix this type accounts: getEthAccounts({ requiredScopes: {}, @@ -818,6 +818,7 @@ export class MetamaskConnectEVM { // @ts-expect-error TODO: address this const accounts = notification?.params; logger('transport-event: accountsChanged', accounts); + // why are we not caching the accounts here? this.#onAccountsChanged(accounts); } diff --git a/packages/connect-multichain/src/multichain/transports/mwp/index.ts b/packages/connect-multichain/src/multichain/transports/mwp/index.ts index ea2e08d3..7a08bc6e 100644 --- a/packages/connect-multichain/src/multichain/transports/mwp/index.ts +++ b/packages/connect-multichain/src/multichain/transports/mwp/index.ts @@ -498,8 +498,9 @@ export class MWPTransport implements ExtendedTransport { this.kvstore.delete(CHAIN_STORE_KEY); return this.dappClient.disconnect(); } else { - // TODO actually call wallet_revokeSession - // wallet_revokeSession might not survive the TTL though... + // This might not actually get excuted on the wallet if the user doesn't open + // their wallet before the message TTL + this.request({ method: 'wallet_revokeSession', params: { scopes } }); const newSessionScopes = Object.fromEntries( Object.entries(cachedSessionScopes).filter(([key]) => diff --git a/playground/browser-playground/src/wagmi/metamask-connector.ts b/playground/browser-playground/src/wagmi/metamask-connector.ts index 224f3ac6..bf711f1f 100644 --- a/playground/browser-playground/src/wagmi/metamask-connector.ts +++ b/playground/browser-playground/src/wagmi/metamask-connector.ts @@ -288,7 +288,6 @@ export function metaMask(parameters: MetaMaskParameters = {}) { } const chainId = Number(connectInfo.chainId); - console.log('WAGMI onConnect', { accounts, chainId }); config.emitter.emit('connect', { accounts, chainId }); }, async onDisconnect(error) { From 8ea5c4bb8bc8472bfd301cd46afffaf31a4d0104 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 5 Feb 2026 14:16:31 -0800 Subject: [PATCH 013/103] remove old comment --- packages/connect-multichain/src/multichain/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/connect-multichain/src/multichain/index.ts b/packages/connect-multichain/src/multichain/index.ts index 26a5a944..5d5723d5 100644 --- a/packages/connect-multichain/src/multichain/index.ts +++ b/packages/connect-multichain/src/multichain/index.ts @@ -664,7 +664,6 @@ export class MetaMaskConnectMultichain extends MultichainCore { }); } - // TODO: Make this merge the existing session scopes with the new ones?? // TODO: make this into param object async connect( scopes: Scope[], From b149bec1ba6782e7fd63a86af0e97a0fa16b0551 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 5 Feb 2026 14:39:11 -0800 Subject: [PATCH 014/103] attempt to resolve eth_accounts and chainId in connect evm session changed handler --- packages/connect-evm/src/connect.ts | 49 +++++++++++++---------------- packages/connect-evm/src/types.ts | 2 +- 2 files changed, 23 insertions(+), 28 deletions(-) diff --git a/packages/connect-evm/src/connect.ts b/packages/connect-evm/src/connect.ts index 93cc6650..dc08309d 100644 --- a/packages/connect-evm/src/connect.ts +++ b/packages/connect-evm/src/connect.ts @@ -132,21 +132,26 @@ export class MetamaskConnectEVM { * * @param session - The session data */ - this.#sessionChangedHandler = (session): void => { + this.#sessionChangedHandler = async (session): Promise => { logger('event: wallet_sessionChanged', session); this.#sessionScopes = session?.sessionScopes ?? {}; const permittedChainIds = getPermittedEthChainIds(this.#sessionScopes); if (permittedChainIds.length === 0) { this.#onDisconnect(); } else { - // Need to somehow make an eth_accounts call here + + const hexPermittedChainIds = getPermittedEthChainIds(this.#sessionScopes); + + const initialAccounts = await this.#core.transport.sendEip1193Message< + { method: 'eth_accounts'; params: [] }, + { result: string[]; id: number; jsonrpc: '2.0' } + >({ method: 'eth_accounts', params: [] }); + + const chainId = await this.#getSelectedChainId(hexPermittedChainIds); + this.#onConnect({ - chainId: permittedChainIds[0], // fix this to use cached chainId too? - // Fix this type - accounts: getEthAccounts({ - requiredScopes: {}, - optionalScopes: this.#sessionScopes as InternalScopesObject, - }), + chainId, + accounts: initialAccounts.result as Address[], }); } }; @@ -364,30 +369,20 @@ export class MetamaskConnectEVM { forceRequest, ); - // const hexPermittedChainIds = getPermittedEthChainIds(this.#sessionScopes); - - // const initialAccounts = await this.#core.transport.sendEip1193Message< - // { method: 'eth_accounts'; params: [] }, - // { result: string[]; id: number; jsonrpc: '2.0' } - // >({ method: 'eth_accounts', params: [] }); - - // const chainId = await this.#getSelectedChainId(hexPermittedChainIds); - - // this.#onConnect({ - // chainId, - // accounts: initialAccounts.result as Address[], - // }); - logger('fulfilled-request: connect', { chainId: chainIds[0], accounts: this.#provider.accounts, }); - // TODO: verify the events that set the provider properties have fired by now - return { - accounts: this.#provider.accounts, - chainId: this.#provider.selectedChainId as Hex, - }; + // Wait for the wallet_sessionChanged event to fire and set the provider properties + return new Promise((resolve) => { + this.#provider.once('connect', ({ chainId, accounts }) => { + resolve({ + accounts, + chainId: chainId as Hex, + }); + }); + }); } catch (error) { logger('Error connecting to wallet', error); throw error; diff --git a/packages/connect-evm/src/types.ts b/packages/connect-evm/src/types.ts index c420e9cf..b7e69962 100644 --- a/packages/connect-evm/src/types.ts +++ b/packages/connect-evm/src/types.ts @@ -7,7 +7,7 @@ export type CaipAccountId = `${string}:${string}:${string}`; export type CaipChainId = `${string}:${string}`; export type EIP1193ProviderEvents = { - connect: [{ chainId: Hex }]; + connect: [{ chainId: Hex; accounts: Address[] }]; disconnect: []; accountsChanged: [Address[]]; chainChanged: [Hex]; From b24d1860eb64c3585d31d5c6c4cf888c28049966 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 5 Feb 2026 14:47:09 -0800 Subject: [PATCH 015/103] fix fulfilled-request log --- packages/connect-evm/src/connect.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/connect-evm/src/connect.ts b/packages/connect-evm/src/connect.ts index dc08309d..3785e124 100644 --- a/packages/connect-evm/src/connect.ts +++ b/packages/connect-evm/src/connect.ts @@ -369,14 +369,14 @@ export class MetamaskConnectEVM { forceRequest, ); - logger('fulfilled-request: connect', { - chainId: chainIds[0], - accounts: this.#provider.accounts, - }); // Wait for the wallet_sessionChanged event to fire and set the provider properties return new Promise((resolve) => { this.#provider.once('connect', ({ chainId, accounts }) => { + logger('fulfilled-request: connect', { + chainId, + accounts, + }); resolve({ accounts, chainId: chainId as Hex, From a059171b94628bfdae06eabb98b6ab7730ce6107 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 5 Feb 2026 14:52:21 -0800 Subject: [PATCH 016/103] re-enable removing notification handler in connect evm disconnect --- packages/connect-evm/src/connect.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/connect-evm/src/connect.ts b/packages/connect-evm/src/connect.ts index 3785e124..938d009c 100644 --- a/packages/connect-evm/src/connect.ts +++ b/packages/connect-evm/src/connect.ts @@ -494,10 +494,8 @@ export class MetamaskConnectEVM { this.#core.off('wallet_sessionChanged', this.#sessionChangedHandler); this.#core.off('display_uri', this.#displayUriHandler); - // if (this.#removeNotificationHandler) { - // this.#removeNotificationHandler(); - // this.#removeNotificationHandler = undefined; - // } + this.#removeNotificationHandler?.(); + this.#removeNotificationHandler = undefined; logger('fulfilled-request: disconnect'); } From eb634d27d10d405ba4b2979031ac936fea2f4a97 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 5 Feb 2026 15:23:11 -0800 Subject: [PATCH 017/103] update onConnect listener comment --- packages/connect-evm/src/connect.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/connect-evm/src/connect.ts b/packages/connect-evm/src/connect.ts index 938d009c..bf7d7af5 100644 --- a/packages/connect-evm/src/connect.ts +++ b/packages/connect-evm/src/connect.ts @@ -800,8 +800,6 @@ export class MetamaskConnectEVM { this.#removeNotificationHandler?.(); - // transport doesn't exist (throws) if we aren't connected. Should that be a - // required for using onNotification?... // TODO: Verify if #core.on('metamask_accountsChanged') and #core.on('metamask_chainChanged') // would work here instead this.#removeNotificationHandler = this.#core.transport.onNotification( From 10ca0c4c06cb560bc0b624646cf4b9b94618bcf6 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 6 Feb 2026 11:24:16 -0800 Subject: [PATCH 018/103] fix typo in method name for wallet_getSession call in DefaultTransport --- .../src/multichain/transports/default/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/connect-multichain/src/multichain/transports/default/index.ts b/packages/connect-multichain/src/multichain/transports/default/index.ts index f91e136c..1a5d6294 100644 --- a/packages/connect-multichain/src/multichain/transports/default/index.ts +++ b/packages/connect-multichain/src/multichain/transports/default/index.ts @@ -261,7 +261,7 @@ export class DefaultTransport implements ExtendedTransport { async disconnect(scopes: Scope[] = []): Promise { await this.request({ method: 'wallet_revokeSession', params: { scopes } }); - const response = await this.request({ method: 'wallet_getSession '}); + const response = await this.request({ method: 'wallet_getSession'}); let {sessionScopes} = response.result as SessionData; if (Object.keys(sessionScopes).length > 0) { From 54e38b902d9029d2fd1fedeb2ea18e943bf09502 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 6 Feb 2026 14:08:52 -0800 Subject: [PATCH 019/103] Fix multichain scope cards by not using the onNotification param listener --- .../src/sdk/SDKProvider.tsx | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/playground/browser-playground/src/sdk/SDKProvider.tsx b/playground/browser-playground/src/sdk/SDKProvider.tsx index 4734cb49..c26c0cdd 100644 --- a/playground/browser-playground/src/sdk/SDKProvider.tsx +++ b/playground/browser-playground/src/sdk/SDKProvider.tsx @@ -55,20 +55,18 @@ export const SDKProvider = ({ children }: { children: React.ReactNode }) => { }, transport: { extensionId: METAMASK_PROD_CHROME_ID, - onNotification: (notification: unknown) => { - const payload = notification as Record; - if ( - payload.method === 'wallet_sessionChanged' || - payload.method === 'wallet_createSession' || - payload.method === 'wallet_getSession' - ) { - setSession(payload.params as SessionData); - } else if (payload.method === 'stateChanged') { - setStatus(payload.params as ConnectionStatus); - } - }, }, }); + + // TODO: Check if we can get rid of transport.onNotification constructor param + sdkRef.current.then((sdkInstance) => { + sdkInstance.on('wallet_sessionChanged', (session: unknown) => { + setSession(session as SessionData); + }); + sdkInstance .on('stateChanged', (status: unknown) => { + setStatus(status as ConnectionStatus); + }); + }); } }, []); From fc3b322fd13f7f02edcbcad5ba843d4ecc643f71 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 6 Feb 2026 14:09:08 -0800 Subject: [PATCH 020/103] Fix browser playground disconnect all by not guarding on the connected state --- playground/browser-playground/src/App.tsx | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/playground/browser-playground/src/App.tsx b/playground/browser-playground/src/App.tsx index df5760c9..bd5fdbb4 100644 --- a/playground/browser-playground/src/App.tsx +++ b/playground/browser-playground/src/App.tsx @@ -45,7 +45,6 @@ function App() { connectAsync: wagmiConnectAsync, status: wagmiStatus, } = useConnect(); - const { disconnect: wagmiDisconnect } = useDisconnect(); const { connected: solanaConnected, @@ -53,7 +52,6 @@ function App() { wallets, select, connect: solanaConnect, - disconnect: solanaDisconnect, } = useWallet(); const handleCheckboxChange = useCallback( @@ -161,18 +159,9 @@ function App() { status === 'disconnected' || status === 'pending' || status === 'loaded'; const disconnect = useCallback(async () => { - if (isConnected) { - await sdkDisconnect(); - } + await sdkDisconnect(); }, [ sdkDisconnect, - legacyDisconnect, - wagmiDisconnect, - solanaDisconnect, - isConnected, - legacyConnected, - wagmiConnected, - solanaConnected, ]); const availableOptions = Object.keys(FEATURED_NETWORKS).reduce< From aa40223b2a9f7c727c49343c6981446d6a06e7bd Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 6 Feb 2026 14:09:25 -0800 Subject: [PATCH 021/103] Disable wallet_revokeSession call inside DefaultTransport's connect() --- .../src/multichain/transports/default/index.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/connect-multichain/src/multichain/transports/default/index.ts b/packages/connect-multichain/src/multichain/transports/default/index.ts index 1a5d6294..2e5c2f86 100644 --- a/packages/connect-multichain/src/multichain/transports/default/index.ts +++ b/packages/connect-multichain/src/multichain/transports/default/index.ts @@ -229,10 +229,12 @@ export class DefaultTransport implements ExtendedTransport { ); if (!hasSameScopesAndAccounts) { - await this.request( - { method: 'wallet_revokeSession', params: walletSession }, - this.#defaultRequestOptions, - ); + // MWPTransport does not reset the whole session. Why are we doing so here? + // Passing in walletSession here is wrong as it's not an array of scope strings + // await this.request( + // { method: 'wallet_revokeSession', params: walletSession }, + // this.#defaultRequestOptions, + // ); const response = await this.request( { method: 'wallet_createSession', params: createSessionParams }, this.#defaultRequestOptions, From 812faaa32d3ae1983f0dbfcd5c1e7f1034be0151 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 6 Feb 2026 15:54:33 -0800 Subject: [PATCH 022/103] remove transport.onNotification --- packages/connect-multichain/src/domain/multichain/types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/connect-multichain/src/domain/multichain/types.ts b/packages/connect-multichain/src/domain/multichain/types.ts index 136aa9df..a5dbc10b 100644 --- a/packages/connect-multichain/src/domain/multichain/types.ts +++ b/packages/connect-multichain/src/domain/multichain/types.ts @@ -77,7 +77,6 @@ export type MultichainOptions = { transport?: { /** Extension ID for browser extension transport */ extensionId?: string; - onNotification?: (notification: unknown) => void; }; /** Enable debug logging */ debug?: boolean; From 6c00cf68c349d96c1fa353417676bc6e14bd0e60 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 6 Feb 2026 15:54:43 -0800 Subject: [PATCH 023/103] Add merging partials --- .../src/domain/multichain/index.ts | 55 ++++++++++++++++++- .../src/domain/multichain/types.ts | 19 +++++++ .../connect-multichain/src/index.browser.ts | 9 ++- .../connect-multichain/src/index.native.ts | 9 ++- packages/connect-multichain/src/index.node.ts | 9 ++- 5 files changed, 94 insertions(+), 7 deletions(-) diff --git a/packages/connect-multichain/src/domain/multichain/index.ts b/packages/connect-multichain/src/domain/multichain/index.ts index ab4650ee..33c0f1e9 100644 --- a/packages/connect-multichain/src/domain/multichain/index.ts +++ b/packages/connect-multichain/src/domain/multichain/index.ts @@ -9,7 +9,11 @@ import type { CaipAccountId, Json } from '@metamask/utils'; import { EventEmitter, type SDKEvents } from '../events'; import type { StoreClient } from '../store/client'; import type { InvokeMethodOptions, RPCAPI, Scope } from './api/types'; -import type { MultichainOptions, ExtendedTransport } from './types'; +import type { + ExtendedTransport, + MergeableMultichainOptions, + MultichainOptions, +} from './types'; export type ConnectionStatus = | 'pending' @@ -75,6 +79,55 @@ export abstract class MultichainCore extends EventEmitter { constructor(protected readonly options: MultichainOptions) { super(); } + + /** + * Merges the given options into the current instance options. + * Only the mergeable keys are updated (api.supportedNetworks, ui.*, mobile.*, transport.extensionId, debug). + * Used when createMultichainClient is called with an existing singleton. + * + * @param partial - Options to merge/overwrite onto the current instance + */ + mergeOptions(partial: MergeableMultichainOptions): void { + const opts = this.options as MultichainOptions & { + api: { supportedNetworks: MultichainOptions['api']['supportedNetworks'] }; + ui: MultichainOptions['ui']; + mobile?: MultichainOptions['mobile']; + transport?: MultichainOptions['transport']; + debug?: boolean; + }; + if (partial.api?.supportedNetworks !== undefined) { + opts.api = { + ...opts.api, + supportedNetworks: { + ...opts.api.supportedNetworks, + ...partial.api.supportedNetworks, + }, + }; + } + if (partial.ui !== undefined) { + const uiUpdates: Partial = {}; + if (partial.ui.headless !== undefined) uiUpdates.headless = partial.ui.headless; + if (partial.ui.preferExtension !== undefined) + uiUpdates.preferExtension = partial.ui.preferExtension; + if (partial.ui.showInstallModal !== undefined) + uiUpdates.showInstallModal = partial.ui.showInstallModal; + if (Object.keys(uiUpdates).length > 0) { + opts.ui = { ...opts.ui, ...uiUpdates }; + } + } + if (partial.mobile !== undefined) { + opts.mobile = { ...(opts.mobile ?? {}), ...partial.mobile }; + } + if (partial.transport?.extensionId !== undefined) { + opts.transport = { + ...(opts.transport ?? {}), + extensionId: partial.transport.extensionId, + }; + } + if (partial.debug !== undefined) { + opts.debug = partial.debug; + } + } } /* c8 ignore end */ diff --git a/packages/connect-multichain/src/domain/multichain/types.ts b/packages/connect-multichain/src/domain/multichain/types.ts index a5dbc10b..0c2eb17e 100644 --- a/packages/connect-multichain/src/domain/multichain/types.ts +++ b/packages/connect-multichain/src/domain/multichain/types.ts @@ -88,6 +88,25 @@ type MultiChainFNOptions = Omit & { storage?: StoreClient; }; +/** + * Options that can be merged/overwritten when createMultichainClient is called + * with an existing singleton. + */ +export type MergeableMultichainOptions = { + api?: { supportedNetworks?: RpcUrlsMap }; + ui?: { + headless?: boolean; + preferExtension?: boolean; + showInstallModal?: boolean; + }; + mobile?: { + preferredOpenLink?: (deeplink: string, target?: string) => void; + useDeeplink?: boolean; + }; + transport?: { extensionId?: string }; + debug?: boolean; +}; + /** * Complete options for Multichain SDK configuration. * diff --git a/packages/connect-multichain/src/index.browser.ts b/packages/connect-multichain/src/index.browser.ts index a9ed46b7..0f83626d 100644 --- a/packages/connect-multichain/src/index.browser.ts +++ b/packages/connect-multichain/src/index.browser.ts @@ -24,10 +24,15 @@ declare global { } export const createMultichainClient: CreateMultichainFN = async (options) => { - // Return existing singleton if available + // Return existing singleton if available, merging in the new param options const existingSingleton = globalThis[SINGLETON_KEY]; if (existingSingleton) { - return existingSingleton; + const instance = await existingSingleton; + instance.mergeOptions(options); + if (options.debug) { + enableDebug('metamask-sdk:*'); + } + return instance; } // Store the promise immediately to prevent concurrent calls from creating multiple instances diff --git a/packages/connect-multichain/src/index.native.ts b/packages/connect-multichain/src/index.native.ts index 9a4674c2..9c2c73e6 100644 --- a/packages/connect-multichain/src/index.native.ts +++ b/packages/connect-multichain/src/index.native.ts @@ -24,10 +24,15 @@ declare global { } export const createMultichainClient: CreateMultichainFN = async (options) => { - // Return existing singleton if available + // Return existing singleton if available, merging in the new param options const existingSingleton = global[SINGLETON_KEY]; if (existingSingleton) { - return existingSingleton; + const instance = await existingSingleton; + instance.mergeOptions(options); + if (options.debug) { + enableDebug('metamask-sdk:*'); + } + return instance; } // Store the promise immediately to prevent concurrent calls from creating multiple instances diff --git a/packages/connect-multichain/src/index.node.ts b/packages/connect-multichain/src/index.node.ts index 239bb99c..1816fdee 100644 --- a/packages/connect-multichain/src/index.node.ts +++ b/packages/connect-multichain/src/index.node.ts @@ -20,10 +20,15 @@ declare global { } export const createMultichainClient: CreateMultichainFN = async (options) => { - // Return existing singleton if available + // Return existing singleton if available, merging in the new param options const existingSingleton = globalThis[SINGLETON_KEY]; if (existingSingleton) { - return existingSingleton; + const instance = await existingSingleton; + instance.mergeOptions(options); + if (options.debug) { + enableDebug('metamask-sdk:*'); + } + return instance; } // Store the promise immediately to prevent concurrent calls from creating multiple instances From 9a6b4837828607cf21003ced9de944e3c007f612 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 6 Feb 2026 16:03:35 -0800 Subject: [PATCH 024/103] Revert "remove transport.onNotification" This reverts commit 812faaa32d3ae1983f0dbfcd5c1e7f1034be0151. --- packages/connect-multichain/src/domain/multichain/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/connect-multichain/src/domain/multichain/types.ts b/packages/connect-multichain/src/domain/multichain/types.ts index 0c2eb17e..42ab98bc 100644 --- a/packages/connect-multichain/src/domain/multichain/types.ts +++ b/packages/connect-multichain/src/domain/multichain/types.ts @@ -77,6 +77,7 @@ export type MultichainOptions = { transport?: { /** Extension ID for browser extension transport */ extensionId?: string; + onNotification?: (notification: unknown) => void; }; /** Enable debug logging */ debug?: boolean; From 23f42305e8c67f1e06c78f23a8f876ec7aad388d Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 9 Feb 2026 10:52:09 -0800 Subject: [PATCH 025/103] lint --- packages/connect-evm/src/connect.ts | 36 ++++--------- .../src/domain/multichain/index.ts | 10 ++-- .../connect-multichain/src/index.browser.ts | 7 +-- .../connect-multichain/src/index.native.ts | 7 +-- packages/connect-multichain/src/index.node.ts | 7 +-- .../src/multichain/index.ts | 33 +++++++----- .../multichain/transports/default/index.ts | 6 +-- .../src/multichain/transports/mwp/index.ts | 51 +++++++++++-------- 8 files changed, 73 insertions(+), 84 deletions(-) diff --git a/packages/connect-evm/src/connect.ts b/packages/connect-evm/src/connect.ts index bf7d7af5..0101726a 100644 --- a/packages/connect-evm/src/connect.ts +++ b/packages/connect-evm/src/connect.ts @@ -1,9 +1,5 @@ /* eslint-disable no-restricted-syntax -- Private class properties use established patterns */ import { analytics } from '@metamask/analytics'; -import { - type InternalScopesObject, - getEthAccounts, -} from '@metamask/chain-agnostic-permission'; import type { ConnectionStatus, MultichainCore, @@ -132,6 +128,7 @@ export class MetamaskConnectEVM { * * @param session - The session data */ + // eslint-disable-next-line @typescript-eslint/no-misused-promises this.#sessionChangedHandler = async (session): Promise => { logger('event: wallet_sessionChanged', session); this.#sessionScopes = session?.sessionScopes ?? {}; @@ -139,8 +136,9 @@ export class MetamaskConnectEVM { if (permittedChainIds.length === 0) { this.#onDisconnect(); } else { - - const hexPermittedChainIds = getPermittedEthChainIds(this.#sessionScopes); + const hexPermittedChainIds = getPermittedEthChainIds( + this.#sessionScopes, + ); const initialAccounts = await this.#core.transport.sendEip1193Message< { method: 'eth_accounts'; params: [] }, @@ -185,7 +183,7 @@ export class MetamaskConnectEVM { options: MetamaskConnectEVMOptions, ): Promise { const instance = new MetamaskConnectEVM(options); - await instance.#attemptSessionRecovery(); + await instance.#core.emitSessionChanged(); return instance; } @@ -369,7 +367,6 @@ export class MetamaskConnectEVM { forceRequest, ); - // Wait for the wallet_sessionChanged event to fire and set the provider properties return new Promise((resolve) => { this.#provider.once('connect', ({ chainId, accounts }) => { @@ -483,8 +480,8 @@ export class MetamaskConnectEVM { logger('request: disconnect'); const sessionScopes = this.#sessionScopes; - const eip155Scopes = Object.keys(sessionScopes).filter( - (scope) => scope.startsWith('eip155:'), + const eip155Scopes = Object.keys(sessionScopes).filter((scope) => + scope.startsWith('eip155:'), ); await this.#core.disconnect(eip155Scopes as Scope[]); @@ -807,10 +804,10 @@ export class MetamaskConnectEVM { // @ts-expect-error TODO: address this if (notification?.method === 'metamask_accountsChanged') { // @ts-expect-error TODO: address this - const accounts = notification?.params; - logger('transport-event: accountsChanged', accounts); + const notificationAccounts = notification?.params; + logger('transport-event: accountsChanged', notificationAccounts); // why are we not caching the accounts here? - this.#onAccountsChanged(accounts); + this.#onAccountsChanged(notificationAccounts); } // @ts-expect-error TODO: address this @@ -866,19 +863,6 @@ export class MetamaskConnectEVM { this.#eventHandlers?.displayUri?.(uri); } - /** - * Will trigger an accountsChanged event if there's a valid previous session. - * This is needed because the accountsChanged event is not triggered when - * revising, reloading or opening the app in a new tab. - * - * This works by checking by checking events received during MultichainCore initialization, - * and if there's a wallet_sessionChanged event, it will add a 1-time listener for eth_accounts results - * and trigger an accountsChanged event if the results are valid accounts. - */ - async #attemptSessionRecovery(): Promise { - return this.#core.emitSessionChanged(); - } - /** * Gets the EIP-1193 provider instance * diff --git a/packages/connect-multichain/src/domain/multichain/index.ts b/packages/connect-multichain/src/domain/multichain/index.ts index 33c0f1e9..d4f6327d 100644 --- a/packages/connect-multichain/src/domain/multichain/index.ts +++ b/packages/connect-multichain/src/domain/multichain/index.ts @@ -106,11 +106,15 @@ export abstract class MultichainCore extends EventEmitter { } if (partial.ui !== undefined) { const uiUpdates: Partial = {}; - if (partial.ui.headless !== undefined) uiUpdates.headless = partial.ui.headless; - if (partial.ui.preferExtension !== undefined) + if (partial.ui.headless !== undefined) { + uiUpdates.headless = partial.ui.headless; + } + if (partial.ui.preferExtension !== undefined) { uiUpdates.preferExtension = partial.ui.preferExtension; - if (partial.ui.showInstallModal !== undefined) + } + if (partial.ui.showInstallModal !== undefined) { uiUpdates.showInstallModal = partial.ui.showInstallModal; + } if (Object.keys(uiUpdates).length > 0) { opts.ui = { ...opts.ui, ...uiUpdates }; } diff --git a/packages/connect-multichain/src/index.browser.ts b/packages/connect-multichain/src/index.browser.ts index 0f83626d..8b664375 100644 --- a/packages/connect-multichain/src/index.browser.ts +++ b/packages/connect-multichain/src/index.browser.ts @@ -2,11 +2,7 @@ // Buffer polyfill must be imported first to set up globalThis.Buffer import './polyfills/buffer-shim'; -import type { - CreateMultichainFN, - MultichainCore, - StoreClient, -} from './domain'; +import type { CreateMultichainFN, MultichainCore, StoreClient } from './domain'; import { enableDebug } from './domain'; import { MetaMaskConnectMultichain } from './multichain'; import { Store } from './store'; @@ -17,7 +13,6 @@ export * from './domain'; const SINGLETON_KEY = '__METAMASK_CONNECT_MULTICHAIN_SINGLETON__'; declare global { - // eslint-disable-next-line no-var var __METAMASK_CONNECT_MULTICHAIN_SINGLETON__: | Promise | undefined; diff --git a/packages/connect-multichain/src/index.native.ts b/packages/connect-multichain/src/index.native.ts index 9c2c73e6..6231beff 100644 --- a/packages/connect-multichain/src/index.native.ts +++ b/packages/connect-multichain/src/index.native.ts @@ -2,11 +2,7 @@ // Buffer polyfill must be imported first to set up global.Buffer import './polyfills/buffer-shim'; -import type { - CreateMultichainFN, - MultichainCore, - StoreClient, -} from './domain'; +import type { CreateMultichainFN, MultichainCore, StoreClient } from './domain'; import { enableDebug } from './domain'; import { MetaMaskConnectMultichain } from './multichain'; import { Store } from './store'; @@ -17,7 +13,6 @@ export * from './domain'; const SINGLETON_KEY = '__METAMASK_CONNECT_MULTICHAIN_SINGLETON__'; declare global { - // eslint-disable-next-line no-var var __METAMASK_CONNECT_MULTICHAIN_SINGLETON__: | Promise | undefined; diff --git a/packages/connect-multichain/src/index.node.ts b/packages/connect-multichain/src/index.node.ts index 1816fdee..90ee23d2 100644 --- a/packages/connect-multichain/src/index.node.ts +++ b/packages/connect-multichain/src/index.node.ts @@ -1,8 +1,4 @@ -import type { - CreateMultichainFN, - MultichainCore, - StoreClient, -} from './domain'; +import type { CreateMultichainFN, MultichainCore, StoreClient } from './domain'; import { enableDebug } from './domain'; import { MetaMaskConnectMultichain } from './multichain'; import { Store } from './store'; @@ -13,7 +9,6 @@ export * from './domain'; const SINGLETON_KEY = '__METAMASK_CONNECT_MULTICHAIN_SINGLETON__'; declare global { - // eslint-disable-next-line no-var var __METAMASK_CONNECT_MULTICHAIN_SINGLETON__: | Promise | undefined; diff --git a/packages/connect-multichain/src/multichain/index.ts b/packages/connect-multichain/src/multichain/index.ts index 302b9e81..570e6dc6 100644 --- a/packages/connect-multichain/src/multichain/index.ts +++ b/packages/connect-multichain/src/multichain/index.ts @@ -745,13 +745,16 @@ export class MetaMaskConnectMultichain extends MultichainCore { }); // TODO: Fix these types - const requestedScopes = Array.from(new Set([...existingCaipChainIds, ...scopes])) as Scope[] - const requestedCaipAccountIds = Array.from(new Set([...existingCaipAccountIds, ...caipAccountIds])) as CaipAccountId[] + const requestedScopes = Array.from( + new Set([...existingCaipChainIds, ...scopes]), + ) as Scope[]; + const requestedCaipAccountIds = Array.from( + new Set([...existingCaipAccountIds, ...caipAccountIds]), + ) as CaipAccountId[]; const requestedSessionProperites = { ...sessionData.sessionProperties, ...sessionProperties, - } - + }; // Needed because empty object will cause wallet_createSession to return an error const nonEmptySessionProperites = @@ -866,9 +869,12 @@ export class MetaMaskConnectMultichain extends MultichainCore { } } - const remainingScopes = scopes.length === 0 ? [] : Object.keys(sessionData.sessionScopes).filter( - (scope) => !scopes.includes(scope as Scope), - ); + const remainingScopes = + scopes.length === 0 + ? [] + : Object.keys(sessionData.sessionScopes).filter( + (scope) => !scopes.includes(scope as Scope), + ); await this.#transport?.disconnect(scopes); @@ -888,8 +894,8 @@ export class MetaMaskConnectMultichain extends MultichainCore { const newSessionScopes = Object.fromEntries( Object.entries(sessionData.sessionScopes).filter(([key]) => - remainingScopes.includes(key) - ) + remainingScopes.includes(key), + ), ); // in theory this is only needed for MWP @@ -930,13 +936,12 @@ export class MetaMaskConnectMultichain extends MultichainCore { } async emitSessionChanged(): Promise { - if ( - this.status !== 'connected' && - this.status !== 'connecting' - ) { + if (this.status !== 'connected' && this.status !== 'connecting') { this.emit('wallet_sessionChanged', { sessionScopes: {} }); } else { - const response = await this.transport.request({ method: 'wallet_getSession' }) + const response = await this.transport.request({ + method: 'wallet_getSession', + }); if (response.result) { this.emit('wallet_sessionChanged', response.result); } else { diff --git a/packages/connect-multichain/src/multichain/transports/default/index.ts b/packages/connect-multichain/src/multichain/transports/default/index.ts index 2e5c2f86..ac7a08a9 100644 --- a/packages/connect-multichain/src/multichain/transports/default/index.ts +++ b/packages/connect-multichain/src/multichain/transports/default/index.ts @@ -263,8 +263,8 @@ export class DefaultTransport implements ExtendedTransport { async disconnect(scopes: Scope[] = []): Promise { await this.request({ method: 'wallet_revokeSession', params: { scopes } }); - const response = await this.request({ method: 'wallet_getSession'}); - let {sessionScopes} = response.result as SessionData; + const response = await this.request({ method: 'wallet_getSession' }); + const { sessionScopes } = response.result as SessionData; if (Object.keys(sessionScopes).length > 0) { return; @@ -293,7 +293,7 @@ export class DefaultTransport implements ExtendedTransport { } this.#pendingRequests.clear(); - return this.#transport.disconnect(); + await this.#transport.disconnect(); } isConnected(): boolean { diff --git a/packages/connect-multichain/src/multichain/transports/mwp/index.ts b/packages/connect-multichain/src/multichain/transports/mwp/index.ts index 90bf559c..3af91ffd 100644 --- a/packages/connect-multichain/src/multichain/transports/mwp/index.ts +++ b/packages/connect-multichain/src/multichain/transports/mwp/index.ts @@ -511,15 +511,24 @@ export class MWPTransport implements ExtendedTransport { /** * Disconnects from the Mobile Wallet Protocol * + * @param [scopes] - The scopes to revoke * @returns Nothing */ async disconnect(scopes: Scope[] = []): Promise { - const cachedSession = await this.getCachedResponse({ jsonrpc: '2.0', id: '0', method: 'wallet_getSession' }); - const cachedSessionScopes = (cachedSession?.result as SessionData | undefined)?.sessionScopes ?? {}; - - const remainingScopes = scopes.length === 0 ? [] : Object.keys(cachedSessionScopes).filter( - (scope) => !scopes.includes(scope as Scope), - ); + const cachedSession = await this.getCachedResponse({ + jsonrpc: '2.0', + id: '0', + method: 'wallet_getSession', + }); + const cachedSessionScopes = + (cachedSession?.result as SessionData | undefined)?.sessionScopes ?? {}; + + const remainingScopes = + scopes.length === 0 + ? [] + : Object.keys(cachedSessionScopes).filter( + (scope) => !scopes.includes(scope as Scope), + ); if (remainingScopes.length === 0) { // Clean up window focus event listener @@ -535,25 +544,27 @@ export class MWPTransport implements ExtendedTransport { this.kvstore.delete(ACCOUNTS_STORE_KEY); this.kvstore.delete(CHAIN_STORE_KEY); return this.dappClient.disconnect(); - } else { - // This might not actually get excuted on the wallet if the user doesn't open - // their wallet before the message TTL - this.request({ method: 'wallet_revokeSession', params: { scopes } }); - - const newSessionScopes = Object.fromEntries( - Object.entries(cachedSessionScopes).filter(([key]) => - remainingScopes.includes(key) - ) - ); + } + // This might not actually get excuted on the wallet if the user doesn't open + // their wallet before the message TTL + this.request({ method: 'wallet_revokeSession', params: { scopes } }); + + const newSessionScopes = Object.fromEntries( + Object.entries(cachedSessionScopes).filter(([key]) => + remainingScopes.includes(key), + ), + ); - this.kvstore.set(SESSION_STORE_KEY, JSON.stringify({ + this.kvstore.set( + SESSION_STORE_KEY, + JSON.stringify({ result: { sessionScopes: newSessionScopes, }, - })); + }), + ); - // TODO: update chain_store too. Emit chainChanged - } + // TODO: update chain_store too. Emit chainChanged } /** From 652175087ee9c9a77b906fe141a219408b900757 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 9 Feb 2026 10:58:52 -0800 Subject: [PATCH 026/103] use parseScopeString instead of startsWith --- packages/connect-evm/src/connect.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/connect-evm/src/connect.ts b/packages/connect-evm/src/connect.ts index 0101726a..9a8775fa 100644 --- a/packages/connect-evm/src/connect.ts +++ b/packages/connect-evm/src/connect.ts @@ -37,6 +37,7 @@ import { isSwitchChainRequest, validSupportedChainsUrls, } from './utils/type-guards'; +import { parseScopeString } from '@metamask/chain-agnostic-permission'; const DEFAULT_CHAIN_ID = '0x1'; const CHAIN_STORE_KEY = 'cache_eth_chainId'; @@ -480,9 +481,10 @@ export class MetamaskConnectEVM { logger('request: disconnect'); const sessionScopes = this.#sessionScopes; - const eip155Scopes = Object.keys(sessionScopes).filter((scope) => - scope.startsWith('eip155:'), - ); + const eip155Scopes = Object.keys(sessionScopes).filter((scope) => { + const { namespace } = parseScopeString(scope as Scope); + return namespace === 'eip155'; + }); await this.#core.disconnect(eip155Scopes as Scope[]); this.#onDisconnect(); From 0faca2ad60f8891162a421f6f42718336608596f Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 9 Feb 2026 11:03:11 -0800 Subject: [PATCH 027/103] only emit accountsChanged if accounts have actually changed --- packages/connect-evm/src/connect.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/connect-evm/src/connect.ts b/packages/connect-evm/src/connect.ts index 9a8775fa..3afecb32 100644 --- a/packages/connect-evm/src/connect.ts +++ b/packages/connect-evm/src/connect.ts @@ -766,6 +766,12 @@ export class MetamaskConnectEVM { * @param accounts - The new list of permitted accounts */ #onAccountsChanged(accounts: Address[]): void { + const accountsUnchanged = + accounts.length === this.#provider.accounts.length && + accounts.every((acct, idx) => acct === this.#provider.accounts[idx]) + if (accountsUnchanged) { + return; + } logger('handler: accountsChanged', accounts); this.#provider.accounts = accounts; this.#provider.emit('accountsChanged', accounts); @@ -827,7 +833,6 @@ export class MetamaskConnectEVM { ); } - // TODO: check if chain has changed and accounts have changed this.#onChainChanged(chainId); this.#onAccountsChanged(accounts); } From d73e7e772b4f8c9f584538015509ec1d39a9fdd0 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 9 Feb 2026 11:15:54 -0800 Subject: [PATCH 028/103] base Mergeable types on actual types --- .../src/domain/multichain/types.ts | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/packages/connect-multichain/src/domain/multichain/types.ts b/packages/connect-multichain/src/domain/multichain/types.ts index 42ab98bc..bdd3e8e3 100644 --- a/packages/connect-multichain/src/domain/multichain/types.ts +++ b/packages/connect-multichain/src/domain/multichain/types.ts @@ -93,18 +93,11 @@ type MultiChainFNOptions = Omit & { * Options that can be merged/overwritten when createMultichainClient is called * with an existing singleton. */ -export type MergeableMultichainOptions = { - api?: { supportedNetworks?: RpcUrlsMap }; - ui?: { - headless?: boolean; - preferExtension?: boolean; - showInstallModal?: boolean; - }; - mobile?: { - preferredOpenLink?: (deeplink: string, target?: string) => void; - useDeeplink?: boolean; - }; - transport?: { extensionId?: string }; +export type MergeableMultichainOptions = Omit & +{ + api?: MultichainOptions['api']; + ui?: Pick; + transport?: Pick, 'extensionId'>; debug?: boolean; }; From 52342e71f0d941fb1fa2de6f092f465fb8d258e8 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 9 Feb 2026 11:16:32 -0800 Subject: [PATCH 029/103] remove wallet_revokeSession comment from DefaultTransport.connect --- .../src/multichain/transports/default/index.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/connect-multichain/src/multichain/transports/default/index.ts b/packages/connect-multichain/src/multichain/transports/default/index.ts index ac7a08a9..7294ddda 100644 --- a/packages/connect-multichain/src/multichain/transports/default/index.ts +++ b/packages/connect-multichain/src/multichain/transports/default/index.ts @@ -229,12 +229,6 @@ export class DefaultTransport implements ExtendedTransport { ); if (!hasSameScopesAndAccounts) { - // MWPTransport does not reset the whole session. Why are we doing so here? - // Passing in walletSession here is wrong as it's not an array of scope strings - // await this.request( - // { method: 'wallet_revokeSession', params: walletSession }, - // this.#defaultRequestOptions, - // ); const response = await this.request( { method: 'wallet_createSession', params: createSessionParams }, this.#defaultRequestOptions, From 9338d19a5189ef4579ef7dbe1201824a6823f0c8 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 9 Feb 2026 11:27:09 -0800 Subject: [PATCH 030/103] cleanup --- .../multichain/transports/multichainApiClientWrapper/index.ts | 4 ++-- .../connect-multichain/src/multichain/transports/mwp/index.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/connect-multichain/src/multichain/transports/multichainApiClientWrapper/index.ts b/packages/connect-multichain/src/multichain/transports/multichainApiClientWrapper/index.ts index 4261101e..2be359cb 100644 --- a/packages/connect-multichain/src/multichain/transports/multichainApiClientWrapper/index.ts +++ b/packages/connect-multichain/src/multichain/transports/multichainApiClientWrapper/index.ts @@ -73,7 +73,7 @@ export class MultichainApiClientWrapperTransport implements Transport { async connect(): Promise { console.log('📚 connect'); - return await this.metamaskConnectMultichain.emitSessionChanged(); + await this.metamaskConnectMultichain.emitSessionChanged(); } async disconnect(): Promise { @@ -182,7 +182,7 @@ export class MultichainApiClientWrapperTransport implements Transport { const scopes = revokeSessionParams?.scopes ?? []; try { - this.metamaskConnectMultichain.disconnect(scopes as unknown as Scope[]); + this.metamaskConnectMultichain.disconnect(scopes as Scope[]); return { jsonrpc: '2.0', id: request.id, result: true }; } catch (_error) { return { jsonrpc: '2.0', id: request.id, result: false }; diff --git a/packages/connect-multichain/src/multichain/transports/mwp/index.ts b/packages/connect-multichain/src/multichain/transports/mwp/index.ts index 3af91ffd..1cdfeb54 100644 --- a/packages/connect-multichain/src/multichain/transports/mwp/index.ts +++ b/packages/connect-multichain/src/multichain/transports/mwp/index.ts @@ -511,7 +511,7 @@ export class MWPTransport implements ExtendedTransport { /** * Disconnects from the Mobile Wallet Protocol * - * @param [scopes] - The scopes to revoke + * @param [scopes] - The scopes to revoke. If not provided or empty, all scopes will be revoked. * @returns Nothing */ async disconnect(scopes: Scope[] = []): Promise { From 48339b554b6006f34267108953c6e794f10fa0b5 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 9 Feb 2026 11:32:40 -0800 Subject: [PATCH 031/103] add #getCaipSession --- .../src/multichain/index.ts | 25 ++++++------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/packages/connect-multichain/src/multichain/index.ts b/packages/connect-multichain/src/multichain/index.ts index 570e6dc6..6685294d 100644 --- a/packages/connect-multichain/src/multichain/index.ts +++ b/packages/connect-multichain/src/multichain/index.ts @@ -716,21 +716,7 @@ export class MetaMaskConnectMultichain extends MultichainCore { logger('Error tracking connection_initiated event', error); } - let sessionData: SessionData = { - sessionScopes: {}, - sessionProperties: {}, - }; - if (this.status === 'connected') { - // Try to get current session scopes - const response = await this.transport.request({ - method: 'wallet_getSession', - }); - if (response.result) { - sessionData = response.result as SessionData; - } else { - // ??? - } - } + const sessionData = await this.#getCaipSession(); // Get existing CAIP chain IDs and account IDs from sessionScopes const existingCaipChainIds = Object.keys(sessionData.sessionScopes); @@ -852,7 +838,7 @@ export class MetaMaskConnectMultichain extends MultichainCore { super.emit(event, args); } - async disconnect(scopes: Scope[] = []): Promise { + async #getCaipSession(): Promise { let sessionData: SessionData = { sessionScopes: {}, sessionProperties: {}, @@ -864,10 +850,13 @@ export class MetaMaskConnectMultichain extends MultichainCore { }); if (response.result) { sessionData = response.result as SessionData; - } else { - // ??? } } + return sessionData; + } + + async disconnect(scopes: Scope[] = []): Promise { + const sessionData = await this.#getCaipSession(); const remainingScopes = scopes.length === 0 From c93774ffbb830c11134cd800a57de46bd5584d3c Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 9 Feb 2026 11:38:53 -0800 Subject: [PATCH 032/103] remove fix type comment --- packages/connect-multichain/src/multichain/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/connect-multichain/src/multichain/index.ts b/packages/connect-multichain/src/multichain/index.ts index 6685294d..a26c1dc5 100644 --- a/packages/connect-multichain/src/multichain/index.ts +++ b/packages/connect-multichain/src/multichain/index.ts @@ -730,7 +730,6 @@ export class MetaMaskConnectMultichain extends MultichainCore { } }); - // TODO: Fix these types const requestedScopes = Array.from( new Set([...existingCaipChainIds, ...scopes]), ) as Scope[]; @@ -843,8 +842,7 @@ export class MetaMaskConnectMultichain extends MultichainCore { sessionScopes: {}, sessionProperties: {}, }; - if (this.status === 'connected') { - // Try to get current session scopes + if (this.status !== 'connected') { const response = await this.transport.request({ method: 'wallet_getSession', }); From 37d37a27ae5441de762e4989652f2fd34c4b18fa Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 9 Feb 2026 12:03:12 -0800 Subject: [PATCH 033/103] lint --- packages/connect-evm/src/connect.ts | 4 ++-- .../connect-multichain/src/domain/multichain/types.ts | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/connect-evm/src/connect.ts b/packages/connect-evm/src/connect.ts index 3afecb32..44a8a3b3 100644 --- a/packages/connect-evm/src/connect.ts +++ b/packages/connect-evm/src/connect.ts @@ -1,5 +1,6 @@ /* eslint-disable no-restricted-syntax -- Private class properties use established patterns */ import { analytics } from '@metamask/analytics'; +import { parseScopeString } from '@metamask/chain-agnostic-permission'; import type { ConnectionStatus, MultichainCore, @@ -37,7 +38,6 @@ import { isSwitchChainRequest, validSupportedChainsUrls, } from './utils/type-guards'; -import { parseScopeString } from '@metamask/chain-agnostic-permission'; const DEFAULT_CHAIN_ID = '0x1'; const CHAIN_STORE_KEY = 'cache_eth_chainId'; @@ -768,7 +768,7 @@ export class MetamaskConnectEVM { #onAccountsChanged(accounts: Address[]): void { const accountsUnchanged = accounts.length === this.#provider.accounts.length && - accounts.every((acct, idx) => acct === this.#provider.accounts[idx]) + accounts.every((acct, idx) => acct === this.#provider.accounts[idx]); if (accountsUnchanged) { return; } diff --git a/packages/connect-multichain/src/domain/multichain/types.ts b/packages/connect-multichain/src/domain/multichain/types.ts index bdd3e8e3..3ee1c4c0 100644 --- a/packages/connect-multichain/src/domain/multichain/types.ts +++ b/packages/connect-multichain/src/domain/multichain/types.ts @@ -93,10 +93,15 @@ type MultiChainFNOptions = Omit & { * Options that can be merged/overwritten when createMultichainClient is called * with an existing singleton. */ -export type MergeableMultichainOptions = Omit & -{ +export type MergeableMultichainOptions = Omit< + MultichainOptions, + 'storage' | 'api' | 'ui' | 'transport' +> & { api?: MultichainOptions['api']; - ui?: Pick; + ui?: Pick< + MultichainOptions['ui'], + 'headless' | 'preferExtension' | 'showInstallModal' + >; transport?: Pick, 'extensionId'>; debug?: boolean; }; From 4c148a48348fd3d6863b8e04c86f57d6f481d05f Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 9 Feb 2026 12:06:22 -0800 Subject: [PATCH 034/103] DRY singleton --- .../connect-multichain/src/index.browser.ts | 67 ++++++------------- .../connect-multichain/src/index.native.ts | 67 ++++++------------- packages/connect-multichain/src/index.node.ts | 67 ++++++------------- .../src/multichain/index.ts | 45 ++++++++++--- .../src/multichain/utils/index.ts | 17 +++++ 5 files changed, 115 insertions(+), 148 deletions(-) diff --git a/packages/connect-multichain/src/index.browser.ts b/packages/connect-multichain/src/index.browser.ts index 8b664375..a18b7a5d 100644 --- a/packages/connect-multichain/src/index.browser.ts +++ b/packages/connect-multichain/src/index.browser.ts @@ -2,7 +2,7 @@ // Buffer polyfill must be imported first to set up globalThis.Buffer import './polyfills/buffer-shim'; -import type { CreateMultichainFN, MultichainCore, StoreClient } from './domain'; +import type { CreateMultichainFN, StoreClient } from './domain'; import { enableDebug } from './domain'; import { MetaMaskConnectMultichain } from './multichain'; import { Store } from './store'; @@ -10,52 +10,27 @@ import { ModalFactory } from './ui'; export * from './domain'; -const SINGLETON_KEY = '__METAMASK_CONNECT_MULTICHAIN_SINGLETON__'; - -declare global { - var __METAMASK_CONNECT_MULTICHAIN_SINGLETON__: - | Promise - | undefined; -} - export const createMultichainClient: CreateMultichainFN = async (options) => { - // Return existing singleton if available, merging in the new param options - const existingSingleton = globalThis[SINGLETON_KEY]; - if (existingSingleton) { - const instance = await existingSingleton; - instance.mergeOptions(options); - if (options.debug) { - enableDebug('metamask-sdk:*'); - } - return instance; + if (options.debug) { + enableDebug('metamask-sdk:*'); } - // Store the promise immediately to prevent concurrent calls from creating multiple instances - const instancePromise = (async () => { - if (options.debug) { - enableDebug('metamask-sdk:*'); - } - - const uiModules = await import('./ui/modals/web'); - let storage: StoreClient; - if (options.storage) { - storage = options.storage; - } else { - const { StoreAdapterWeb } = await import('./store/adapters/web'); - const adapter = new StoreAdapterWeb(); - storage = new Store(adapter); - } - const factory = new ModalFactory(uiModules); - return MetaMaskConnectMultichain.create({ - ...options, - storage, - ui: { - ...options.ui, - factory, - }, - }); - })(); - - globalThis[SINGLETON_KEY] = instancePromise; - return instancePromise; + const uiModules = await import('./ui/modals/web'); + let storage: StoreClient; + if (options.storage) { + storage = options.storage; + } else { + const { StoreAdapterWeb } = await import('./store/adapters/web'); + const adapter = new StoreAdapterWeb(); + storage = new Store(adapter); + } + const factory = new ModalFactory(uiModules); + return MetaMaskConnectMultichain.create({ + ...options, + storage, + ui: { + ...options.ui, + factory, + }, + }); }; diff --git a/packages/connect-multichain/src/index.native.ts b/packages/connect-multichain/src/index.native.ts index 6231beff..2635133c 100644 --- a/packages/connect-multichain/src/index.native.ts +++ b/packages/connect-multichain/src/index.native.ts @@ -2,7 +2,7 @@ // Buffer polyfill must be imported first to set up global.Buffer import './polyfills/buffer-shim'; -import type { CreateMultichainFN, MultichainCore, StoreClient } from './domain'; +import type { CreateMultichainFN, StoreClient } from './domain'; import { enableDebug } from './domain'; import { MetaMaskConnectMultichain } from './multichain'; import { Store } from './store'; @@ -10,52 +10,27 @@ import { ModalFactory } from './ui/index.native'; export * from './domain'; -const SINGLETON_KEY = '__METAMASK_CONNECT_MULTICHAIN_SINGLETON__'; - -declare global { - var __METAMASK_CONNECT_MULTICHAIN_SINGLETON__: - | Promise - | undefined; -} - export const createMultichainClient: CreateMultichainFN = async (options) => { - // Return existing singleton if available, merging in the new param options - const existingSingleton = global[SINGLETON_KEY]; - if (existingSingleton) { - const instance = await existingSingleton; - instance.mergeOptions(options); - if (options.debug) { - enableDebug('metamask-sdk:*'); - } - return instance; + if (options.debug) { + enableDebug('metamask-sdk:*'); } - // Store the promise immediately to prevent concurrent calls from creating multiple instances - const instancePromise = (async () => { - if (options.debug) { - enableDebug('metamask-sdk:*'); - } - - const uiModules = await import('./ui/modals/rn'); - let storage: StoreClient; - if (options.storage) { - storage = options.storage; - } else { - const { StoreAdapterRN } = await import('./store/adapters/rn'); - const adapter = new StoreAdapterRN(); - storage = new Store(adapter); - } - const factory = new ModalFactory(uiModules); - return MetaMaskConnectMultichain.create({ - ...options, - storage, - ui: { - ...options.ui, - factory, - }, - }); - })(); - - global[SINGLETON_KEY] = instancePromise; - return instancePromise; + const uiModules = await import('./ui/modals/rn'); + let storage: StoreClient; + if (options.storage) { + storage = options.storage; + } else { + const { StoreAdapterRN } = await import('./store/adapters/rn'); + const adapter = new StoreAdapterRN(); + storage = new Store(adapter); + } + const factory = new ModalFactory(uiModules); + return MetaMaskConnectMultichain.create({ + ...options, + storage, + ui: { + ...options.ui, + factory, + }, + }); }; diff --git a/packages/connect-multichain/src/index.node.ts b/packages/connect-multichain/src/index.node.ts index 90ee23d2..8c929f1b 100644 --- a/packages/connect-multichain/src/index.node.ts +++ b/packages/connect-multichain/src/index.node.ts @@ -1,4 +1,4 @@ -import type { CreateMultichainFN, MultichainCore, StoreClient } from './domain'; +import type { CreateMultichainFN, StoreClient } from './domain'; import { enableDebug } from './domain'; import { MetaMaskConnectMultichain } from './multichain'; import { Store } from './store'; @@ -6,52 +6,27 @@ import { ModalFactory } from './ui'; export * from './domain'; -const SINGLETON_KEY = '__METAMASK_CONNECT_MULTICHAIN_SINGLETON__'; - -declare global { - var __METAMASK_CONNECT_MULTICHAIN_SINGLETON__: - | Promise - | undefined; -} - export const createMultichainClient: CreateMultichainFN = async (options) => { - // Return existing singleton if available, merging in the new param options - const existingSingleton = globalThis[SINGLETON_KEY]; - if (existingSingleton) { - const instance = await existingSingleton; - instance.mergeOptions(options); - if (options.debug) { - enableDebug('metamask-sdk:*'); - } - return instance; + if (options.debug) { + enableDebug('metamask-sdk:*'); } - // Store the promise immediately to prevent concurrent calls from creating multiple instances - const instancePromise = (async () => { - if (options.debug) { - enableDebug('metamask-sdk:*'); - } - - const uiModules = await import('./ui/modals/node'); - let storage: StoreClient; - if (options.storage) { - storage = options.storage; - } else { - const { StoreAdapterNode } = await import('./store/adapters/node'); - const adapter = new StoreAdapterNode(); - storage = new Store(adapter); - } - const factory = new ModalFactory(uiModules); - return MetaMaskConnectMultichain.create({ - ...options, - storage, - ui: { - ...options.ui, - factory, - }, - }); - })(); - - globalThis[SINGLETON_KEY] = instancePromise; - return instancePromise; + const uiModules = await import('./ui/modals/node'); + let storage: StoreClient; + if (options.storage) { + storage = options.storage; + } else { + const { StoreAdapterNode } = await import('./store/adapters/node'); + const adapter = new StoreAdapterNode(); + storage = new Store(adapter); + } + const factory = new ModalFactory(uiModules); + return MetaMaskConnectMultichain.create({ + ...options, + storage, + ui: { + ...options.ui, + factory, + }, + }); }; diff --git a/packages/connect-multichain/src/multichain/index.ts b/packages/connect-multichain/src/multichain/index.ts index a26c1dc5..ffee6640 100644 --- a/packages/connect-multichain/src/multichain/index.ts +++ b/packages/connect-multichain/src/multichain/index.ts @@ -61,13 +61,20 @@ import { DefaultTransport } from './transports/default'; import { MultichainApiClientWrapperTransport } from './transports/multichainApiClientWrapper'; import { MWPTransport } from './transports/mwp'; import { keymanager } from './transports/mwp/KeyManager'; -import { getDappId, openDeeplink, setupDappMetadata } from './utils'; +import { + getDappId, + getGlobalObject, + openDeeplink, + setupDappMetadata, +} from './utils'; export { getInfuraRpcUrls } from '../domain/multichain/api/infura'; // ENFORCE NAMESPACE THAT CAN BE DISABLED const logger = createLogger('metamask-sdk:core'); +const SINGLETON_KEY = '__METAMASK_CONNECT_MULTICHAIN_SINGLETON__'; + export class MetaMaskConnectMultichain extends MultichainCore { readonly #provider: MultichainApiClient; @@ -155,16 +162,34 @@ export class MetaMaskConnectMultichain extends MultichainCore { static async create( options: MultichainOptions, ): Promise { - const instance = new MetaMaskConnectMultichain(options); - const isEnabled = await isLoggerEnabled( - 'metamask-sdk:core', - instance.options.storage, - ); - if (isEnabled) { - enableDebug('metamask-sdk:core'); + const globalObject = getGlobalObject(); + const existing = globalObject[SINGLETON_KEY] as + | Promise + | undefined; + if (existing) { + const instance = await existing; + instance.mergeOptions(options); + if (options.debug) { + enableDebug('metamask-sdk:*'); + } + return instance; } - await instance.#init(); - return instance; + + const instancePromise = (async (): Promise => { + const instance = new MetaMaskConnectMultichain(options); + const isEnabled = await isLoggerEnabled( + 'metamask-sdk:core', + instance.options.storage, + ); + if (isEnabled) { + enableDebug('metamask-sdk:core'); + } + await instance.#init(); + return instance; + })(); + + globalObject[SINGLETON_KEY] = instancePromise; + return instancePromise; } async #setupAnalytics(): Promise { diff --git a/packages/connect-multichain/src/multichain/utils/index.ts b/packages/connect-multichain/src/multichain/utils/index.ts index aecb2a6a..4db439ea 100644 --- a/packages/connect-multichain/src/multichain/utils/index.ts +++ b/packages/connect-multichain/src/multichain/utils/index.ts @@ -21,6 +21,23 @@ import { export type OptionalScopes = Record; +/** + * Returns the global object for the current JS environment. + * + * @returns The global object as a record for indexing + */ +export function getGlobalObject(): Record { + if (typeof globalThis !== 'undefined') + return globalThis as unknown as Record; + if (typeof global !== 'undefined') + return global as unknown as Record; + if (typeof self !== 'undefined') + return self as unknown as Record; + if (typeof window !== 'undefined') + return window as unknown as Record; + throw new Error('Unable to locate global object'); +} + /** * Cross-platform base64 encoding * Works in browser, Node.js, and React Native environments From eca8f415df79af351c85db22937df3fecf17a680 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 9 Feb 2026 13:19:17 -0800 Subject: [PATCH 035/103] lint --- .../connect-multichain/src/multichain/utils/index.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/connect-multichain/src/multichain/utils/index.ts b/packages/connect-multichain/src/multichain/utils/index.ts index 4db439ea..f842d059 100644 --- a/packages/connect-multichain/src/multichain/utils/index.ts +++ b/packages/connect-multichain/src/multichain/utils/index.ts @@ -27,14 +27,18 @@ export type OptionalScopes = Record; * @returns The global object as a record for indexing */ export function getGlobalObject(): Record { - if (typeof globalThis !== 'undefined') + if (typeof globalThis !== 'undefined') { return globalThis as unknown as Record; - if (typeof global !== 'undefined') + } + if (typeof global !== 'undefined') { return global as unknown as Record; - if (typeof self !== 'undefined') + } + if (typeof self !== 'undefined') { return self as unknown as Record; - if (typeof window !== 'undefined') + } + if (typeof window !== 'undefined') { return window as unknown as Record; + } throw new Error('Unable to locate global object'); } From c1b7a130636d3960b9c0908550bb464815bd3ba3 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 9 Feb 2026 13:27:06 -0800 Subject: [PATCH 036/103] fix typo --- playground/browser-playground/src/sdk/SDKProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/browser-playground/src/sdk/SDKProvider.tsx b/playground/browser-playground/src/sdk/SDKProvider.tsx index c26c0cdd..8aa00d8a 100644 --- a/playground/browser-playground/src/sdk/SDKProvider.tsx +++ b/playground/browser-playground/src/sdk/SDKProvider.tsx @@ -63,7 +63,7 @@ export const SDKProvider = ({ children }: { children: React.ReactNode }) => { sdkInstance.on('wallet_sessionChanged', (session: unknown) => { setSession(session as SessionData); }); - sdkInstance .on('stateChanged', (status: unknown) => { + sdkInstance.on('stateChanged', (status: unknown) => { setStatus(status as ConnectionStatus); }); }); From 91e31b0fd5988df4e3ae2f2aea8d86c7cc0b4587 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 9 Feb 2026 14:08:53 -0800 Subject: [PATCH 037/103] Clear accounts and chain cache --- .../src/multichain/transports/mwp/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/connect-multichain/src/multichain/transports/mwp/index.ts b/packages/connect-multichain/src/multichain/transports/mwp/index.ts index 1cdfeb54..b34e0d67 100644 --- a/packages/connect-multichain/src/multichain/transports/mwp/index.ts +++ b/packages/connect-multichain/src/multichain/transports/mwp/index.ts @@ -28,6 +28,7 @@ import { } from '@metamask/multichain-api-client'; import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import type { CaipAccountId } from '@metamask/utils'; +import { getPermittedEthChainIds, getEthAccounts, InternalScopeObject, InternalScopesObject } from '@metamask/chain-agnostic-permission'; import { createLogger, @@ -564,7 +565,12 @@ export class MWPTransport implements ExtendedTransport { }), ); - // TODO: update chain_store too. Emit chainChanged + // Clear the cached values for eth_accounts and eth_chainId if all eip155 scopes were removed. + const remainingScopesIncludeEip155 = remainingScopes.some((scope) => scope.includes('eip155')); + if (!remainingScopesIncludeEip155) { + this.kvstore.delete(ACCOUNTS_STORE_KEY); + this.kvstore.delete(CHAIN_STORE_KEY); + } } /** From e52031166b2edab98f152c2728db4cf33509f039 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 9 Feb 2026 14:50:18 -0800 Subject: [PATCH 038/103] add mergeRequestedSessionWithExisting --- .../src/multichain/index.ts | 49 +++++++------------ .../src/multichain/utils/index.ts | 49 +++++++++++++++++++ 2 files changed, 68 insertions(+), 30 deletions(-) diff --git a/packages/connect-multichain/src/multichain/index.ts b/packages/connect-multichain/src/multichain/index.ts index ffee6640..8cdbf734 100644 --- a/packages/connect-multichain/src/multichain/index.ts +++ b/packages/connect-multichain/src/multichain/index.ts @@ -64,6 +64,7 @@ import { keymanager } from './transports/mwp/KeyManager'; import { getDappId, getGlobalObject, + mergeRequestedSessionWithExisting, openDeeplink, setupDappMetadata, } from './utils'; @@ -743,33 +744,21 @@ export class MetaMaskConnectMultichain extends MultichainCore { const sessionData = await this.#getCaipSession(); - // Get existing CAIP chain IDs and account IDs from sessionScopes - const existingCaipChainIds = Object.keys(sessionData.sessionScopes); - // For permitted account ids, try to find account address in scopes if possible - const existingCaipAccountIds: string[] = []; - Object.values(sessionData.sessionScopes).forEach((scopeObject) => { - if (scopeObject?.accounts && Array.isArray(scopeObject.accounts)) { - scopeObject.accounts.forEach((account) => { - existingCaipAccountIds.push(account); - }); - } - }); - - const requestedScopes = Array.from( - new Set([...existingCaipChainIds, ...scopes]), - ) as Scope[]; - const requestedCaipAccountIds = Array.from( - new Set([...existingCaipAccountIds, ...caipAccountIds]), - ) as CaipAccountId[]; - const requestedSessionProperites = { - ...sessionData.sessionProperties, - ...sessionProperties, - }; + const { + requestedScopes, + requestedCaipAccountIds, + requestedSessionProperties, + } = mergeRequestedSessionWithExisting( + sessionData, + scopes, + caipAccountIds, + sessionProperties, + ); // Needed because empty object will cause wallet_createSession to return an error - const nonEmptySessionProperites = - Object.keys(requestedSessionProperites ?? {}).length > 0 - ? requestedSessionProperites + const nonEmptySessionProperties = + Object.keys(requestedSessionProperties ?? {}).length > 0 + ? requestedSessionProperties : undefined; if (this.#transport?.isConnected() && !secure) { @@ -778,7 +767,7 @@ export class MetaMaskConnectMultichain extends MultichainCore { .connect({ scopes: requestedScopes, caipAccountIds: requestedCaipAccountIds, - sessionProperties: nonEmptySessionProperites, + sessionProperties: nonEmptySessionProperties, forceRequest, }) .then(async () => { @@ -799,7 +788,7 @@ export class MetaMaskConnectMultichain extends MultichainCore { defaultTransport.connect({ scopes: requestedScopes, caipAccountIds: requestedCaipAccountIds, - sessionProperties: nonEmptySessionProperites, + sessionProperties: nonEmptySessionProperties, forceRequest, }), scopes, @@ -815,7 +804,7 @@ export class MetaMaskConnectMultichain extends MultichainCore { defaultTransport.connect({ scopes: requestedScopes, caipAccountIds: requestedCaipAccountIds, - sessionProperties: nonEmptySessionProperites, + sessionProperties: nonEmptySessionProperties, forceRequest, }), scopes, @@ -837,7 +826,7 @@ export class MetaMaskConnectMultichain extends MultichainCore { this.#deeplinkConnect( scopes, caipAccountIds, - nonEmptySessionProperites, + nonEmptySessionProperties, ), scopes, transportType, @@ -850,7 +839,7 @@ export class MetaMaskConnectMultichain extends MultichainCore { shouldShowInstallModal, requestedScopes, requestedCaipAccountIds, - nonEmptySessionProperites, + nonEmptySessionProperties, ), scopes, transportType, diff --git a/packages/connect-multichain/src/multichain/utils/index.ts b/packages/connect-multichain/src/multichain/utils/index.ts index f842d059..7a32ba14 100644 --- a/packages/connect-multichain/src/multichain/utils/index.ts +++ b/packages/connect-multichain/src/multichain/utils/index.ts @@ -10,6 +10,8 @@ import { } from '@metamask/utils'; import { deflate } from 'pako'; +import type { SessionProperties } from '@metamask/multichain-api-client'; + import { type DappSettings, getPlatformType, @@ -118,6 +120,53 @@ export function openDeeplink( } } +/** + * Merges existing session (from getCaipSession) with newly requested scopes, accounts, and session properties. + * Derives existing scopes/accounts from sessionData.sessionScopes, then merges with requested values. + * + * @param sessionData - Current CAIP session data + * @param scopes - Newly requested scopes + * @param caipAccountIds - Newly requested account IDs + * @param sessionProperties - New session properties to merge over existing + * @returns requestedScopes, requestedCaipAccountIds, and requestedSessionProperties + */ +export function mergeRequestedSessionWithExisting( + sessionData: SessionData, + scopes: Scope[], + caipAccountIds: CaipAccountId[], + sessionProperties?: SessionProperties, +): { + requestedScopes: Scope[]; + requestedCaipAccountIds: CaipAccountId[]; + requestedSessionProperties: SessionProperties; +} { + const existingCaipChainIds = Object.keys(sessionData.sessionScopes); + const existingCaipAccountIds: string[] = []; + Object.values(sessionData.sessionScopes).forEach((scopeObject) => { + if (scopeObject?.accounts && Array.isArray(scopeObject.accounts)) { + scopeObject.accounts.forEach((account) => { + existingCaipAccountIds.push(account); + }); + } + }); + + const requestedScopes = Array.from( + new Set([...existingCaipChainIds, ...scopes]), + ) as Scope[]; + const requestedCaipAccountIds = Array.from( + new Set([...existingCaipAccountIds, ...caipAccountIds]), + ) as CaipAccountId[]; + const requestedSessionProperties = { + ...sessionData.sessionProperties, + ...sessionProperties, + }; + return { + requestedScopes, + requestedCaipAccountIds, + requestedSessionProperties, + }; +} + /** * * @param scopes From 13c875022bc7f1a3e561e5c398edaa7b7224bb5b Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 10 Feb 2026 10:33:01 -0800 Subject: [PATCH 039/103] Rearrange MWP disconnect logic --- .../src/multichain/index.ts | 9 --- .../src/multichain/transports/mwp/index.ts | 62 +++++++++++-------- 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/packages/connect-multichain/src/multichain/index.ts b/packages/connect-multichain/src/multichain/index.ts index 8cdbf734..425d611e 100644 --- a/packages/connect-multichain/src/multichain/index.ts +++ b/packages/connect-multichain/src/multichain/index.ts @@ -892,15 +892,6 @@ export class MetaMaskConnectMultichain extends MultichainCore { this.#dappClient = undefined; this.status = 'disconnected'; } - - const newSessionScopes = Object.fromEntries( - Object.entries(sessionData.sessionScopes).filter(([key]) => - remainingScopes.includes(key), - ), - ); - - // in theory this is only needed for MWP - this.emit('wallet_sessionChanged', { sessionScopes: newSessionScopes }); } async invokeMethod(request: InvokeMethodOptions): Promise { diff --git a/packages/connect-multichain/src/multichain/transports/mwp/index.ts b/packages/connect-multichain/src/multichain/transports/mwp/index.ts index b34e0d67..d8067b91 100644 --- a/packages/connect-multichain/src/multichain/transports/mwp/index.ts +++ b/packages/connect-multichain/src/multichain/transports/mwp/index.ts @@ -531,39 +531,15 @@ export class MWPTransport implements ExtendedTransport { (scope) => !scopes.includes(scope as Scope), ); - if (remainingScopes.length === 0) { - // Clean up window focus event listener - if ( - typeof window !== 'undefined' && - typeof window.removeEventListener !== 'undefined' && - this.windowFocusHandler - ) { - window.removeEventListener('focus', this.windowFocusHandler); - this.windowFocusHandler = undefined; - } - this.kvstore.delete(SESSION_STORE_KEY); - this.kvstore.delete(ACCOUNTS_STORE_KEY); - this.kvstore.delete(CHAIN_STORE_KEY); - return this.dappClient.disconnect(); - } - // This might not actually get excuted on the wallet if the user doesn't open - // their wallet before the message TTL - this.request({ method: 'wallet_revokeSession', params: { scopes } }); - const newSessionScopes = Object.fromEntries( Object.entries(cachedSessionScopes).filter(([key]) => remainingScopes.includes(key), ), ); - this.kvstore.set( - SESSION_STORE_KEY, - JSON.stringify({ - result: { - sessionScopes: newSessionScopes, - }, - }), - ); + // This might not actually get excuted on the wallet if the user doesn't open + // their wallet before the message TTL + this.request({ method: 'wallet_revokeSession', params: { scopes } }); // Clear the cached values for eth_accounts and eth_chainId if all eip155 scopes were removed. const remainingScopesIncludeEip155 = remainingScopes.some((scope) => scope.includes('eip155')); @@ -571,6 +547,38 @@ export class MWPTransport implements ExtendedTransport { this.kvstore.delete(ACCOUNTS_STORE_KEY); this.kvstore.delete(CHAIN_STORE_KEY); } + + if (remainingScopes.length > 0) { + this.kvstore.set( + SESSION_STORE_KEY, + JSON.stringify({ + result: { + sessionScopes: newSessionScopes, + }, + }), + ); + } else { + this.kvstore.delete(SESSION_STORE_KEY); + + // Clean up window focus event listener + if ( + typeof window !== 'undefined' && + typeof window.removeEventListener !== 'undefined' && + this.windowFocusHandler + ) { + window.removeEventListener('focus', this.windowFocusHandler); + this.windowFocusHandler = undefined; + } + + this.dappClient.disconnect(); + } + + this.notifyCallbacks({ + method: 'wallet_sessionChanged', + params: { + sessionScopes: newSessionScopes, + }, + }); } /** From 319c1414f3393d8e9353ed5b1e593af7b8a0a9b7 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 10 Feb 2026 10:33:25 -0800 Subject: [PATCH 040/103] Fix multichain getCaipSession --- packages/connect-multichain/src/multichain/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/connect-multichain/src/multichain/index.ts b/packages/connect-multichain/src/multichain/index.ts index 425d611e..3173e9c3 100644 --- a/packages/connect-multichain/src/multichain/index.ts +++ b/packages/connect-multichain/src/multichain/index.ts @@ -856,7 +856,7 @@ export class MetaMaskConnectMultichain extends MultichainCore { sessionScopes: {}, sessionProperties: {}, }; - if (this.status !== 'connected') { + if (this.status === 'connected') { const response = await this.transport.request({ method: 'wallet_getSession', }); From 62d2948fe3521323f6b3ac47e2c566f46986b1d2 Mon Sep 17 00:00:00 2001 From: jiexi Date: Tue, 10 Feb 2026 10:52:12 -0800 Subject: [PATCH 041/103] Update packages/connect-evm/src/connect.ts Co-authored-by: Alex Donesky --- packages/connect-evm/src/connect.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/connect-evm/src/connect.ts b/packages/connect-evm/src/connect.ts index 44a8a3b3..a6cd7cc4 100644 --- a/packages/connect-evm/src/connect.ts +++ b/packages/connect-evm/src/connect.ts @@ -133,14 +133,11 @@ export class MetamaskConnectEVM { this.#sessionChangedHandler = async (session): Promise => { logger('event: wallet_sessionChanged', session); this.#sessionScopes = session?.sessionScopes ?? {}; - const permittedChainIds = getPermittedEthChainIds(this.#sessionScopes); - if (permittedChainIds.length === 0) { + const hexPermittedChainIds = getPermittedEthChainIds(this.#sessionScopes); + if (hexPermittedChainIds.length === 0) { this.#onDisconnect(); } else { - const hexPermittedChainIds = getPermittedEthChainIds( - this.#sessionScopes, - ); - + const initialAccounts = await this.#core.transport.sendEip1193Message< { method: 'eth_accounts'; params: [] }, { result: string[]; id: number; jsonrpc: '2.0' } From 7a719a28dff79c655a0102936e84efce977b9974 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 10 Feb 2026 10:59:36 -0800 Subject: [PATCH 042/103] fix initial sdkInstance status in browser playground --- playground/browser-playground/src/sdk/SDKProvider.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/playground/browser-playground/src/sdk/SDKProvider.tsx b/playground/browser-playground/src/sdk/SDKProvider.tsx index 8aa00d8a..88329cb7 100644 --- a/playground/browser-playground/src/sdk/SDKProvider.tsx +++ b/playground/browser-playground/src/sdk/SDKProvider.tsx @@ -60,12 +60,13 @@ export const SDKProvider = ({ children }: { children: React.ReactNode }) => { // TODO: Check if we can get rid of transport.onNotification constructor param sdkRef.current.then((sdkInstance) => { - sdkInstance.on('wallet_sessionChanged', (session: unknown) => { - setSession(session as SessionData); - }); + setStatus(sdkInstance.status); sdkInstance.on('stateChanged', (status: unknown) => { setStatus(status as ConnectionStatus); }); + sdkInstance.on('wallet_sessionChanged', (session: unknown) => { + setSession(session as SessionData); + }); }); } }, []); From 4d49746e5ac9ba8ce6d558e37904709c3b88de05 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 10 Feb 2026 11:12:35 -0800 Subject: [PATCH 043/103] Remove Multichain disconnect in browser playground --- playground/browser-playground/src/App.tsx | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/playground/browser-playground/src/App.tsx b/playground/browser-playground/src/App.tsx index e3de4ce1..c97180a2 100644 --- a/playground/browser-playground/src/App.tsx +++ b/playground/browser-playground/src/App.tsx @@ -266,21 +266,13 @@ function App() { )} - {isConnected && ( + {isConnected && scopesHaveChanged() && ( + > Reconnect (Multichain) )} {(isConnected || From 9c489b5d7954cb7f0e5a46e62b801835c748bd22 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 10 Feb 2026 13:55:29 -0800 Subject: [PATCH 044/103] Fix test-dapp playground --- .../browser-playground/src/sdk/LegacyEVMSDKProvider.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/playground/browser-playground/src/sdk/LegacyEVMSDKProvider.tsx b/playground/browser-playground/src/sdk/LegacyEVMSDKProvider.tsx index f091856a..2ad2e385 100644 --- a/playground/browser-playground/src/sdk/LegacyEVMSDKProvider.tsx +++ b/playground/browser-playground/src/sdk/LegacyEVMSDKProvider.tsx @@ -89,7 +89,9 @@ export const LegacyEVMSDKProvider = ({ const providerInstance = await clientSDK.getProvider(); if (providerInstance) { - providerInstance.on('connect', () => { + providerInstance.on('connect', ({ chainId, accounts }: { chainId: string; accounts: string[] }) => { + setChainId(chainId); + setAccounts(accounts); setConnected(true); }); From fe8233e75f9fc6a66878343160543e52404604ec Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Wed, 11 Feb 2026 16:23:19 -0600 Subject: [PATCH 045/103] remove unnecessary eslint disable --- packages/connect-evm/src/connect.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/connect-evm/src/connect.ts b/packages/connect-evm/src/connect.ts index a6cd7cc4..c0176d5c 100644 --- a/packages/connect-evm/src/connect.ts +++ b/packages/connect-evm/src/connect.ts @@ -129,7 +129,6 @@ export class MetamaskConnectEVM { * * @param session - The session data */ - // eslint-disable-next-line @typescript-eslint/no-misused-promises this.#sessionChangedHandler = async (session): Promise => { logger('event: wallet_sessionChanged', session); this.#sessionScopes = session?.sessionScopes ?? {}; From f0b569b66e6e2c66713acea92a2c6bd400876277 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Wed, 11 Feb 2026 16:34:58 -0600 Subject: [PATCH 046/103] remove redundant binding --- packages/connect-evm/src/connect.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/connect-evm/src/connect.ts b/packages/connect-evm/src/connect.ts index 7fc6fb9f..4be12e1d 100644 --- a/packages/connect-evm/src/connect.ts +++ b/packages/connect-evm/src/connect.ts @@ -150,10 +150,7 @@ export class MetamaskConnectEVM { }); } }; - this.#core.on( - 'wallet_sessionChanged', - this.#sessionChangedHandler.bind(this), - ); + this.#core.on('wallet_sessionChanged', this.#sessionChangedHandler); /** * Handles the display_uri event. From 09bc769859e879d7768b749201f66d6550a99b70 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 11 Feb 2026 14:34:43 -0800 Subject: [PATCH 047/103] ensure we're connected before trying to resolve eth_accounts --- packages/connect-evm/src/connect.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/connect-evm/src/connect.ts b/packages/connect-evm/src/connect.ts index a6cd7cc4..f0934b2f 100644 --- a/packages/connect-evm/src/connect.ts +++ b/packages/connect-evm/src/connect.ts @@ -29,7 +29,7 @@ import type { ProviderRequest, ProviderRequestInterceptor, } from './types'; -import { getPermittedEthChainIds } from './utils/caip'; +import { getEthAccounts, getPermittedEthChainIds } from './utils/caip'; import { isAccountsRequest, isAddChainRequest, @@ -137,17 +137,22 @@ export class MetamaskConnectEVM { if (hexPermittedChainIds.length === 0) { this.#onDisconnect(); } else { - - const initialAccounts = await this.#core.transport.sendEip1193Message< - { method: 'eth_accounts'; params: [] }, - { result: string[]; id: number; jsonrpc: '2.0' } - >({ method: 'eth_accounts', params: [] }); + let initialAccounts: Address[] = []; + if (this.#core.status === 'connected') { + const ethAccountsResponse = await this.#core.transport.sendEip1193Message< + { method: 'eth_accounts'; params: [] }, + { result: string[]; id: number; jsonrpc: '2.0' } + >({ method: 'eth_accounts', params: [] }); + initialAccounts = ethAccountsResponse.result as Address[]; + } else { + initialAccounts = getEthAccounts(this.#sessionScopes); + } const chainId = await this.#getSelectedChainId(hexPermittedChainIds); this.#onConnect({ chainId, - accounts: initialAccounts.result as Address[], + accounts: initialAccounts as Address[], }); } }; From f930bf7420862b03dd0590a889d006cfadd295ef Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 11 Feb 2026 14:35:33 -0800 Subject: [PATCH 048/103] Fix incorrect disconnected state in connect-evm --- packages/connect-evm/src/connect.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/connect-evm/src/connect.ts b/packages/connect-evm/src/connect.ts index f0934b2f..91a49a8b 100644 --- a/packages/connect-evm/src/connect.ts +++ b/packages/connect-evm/src/connect.ts @@ -384,10 +384,9 @@ export class MetamaskConnectEVM { }); }); } catch (error) { + this.#status = 'disconnected'; logger('Error connecting to wallet', error); throw error; - } finally { - this.#status = 'disconnected'; } } From b43e5aedfd6ffad0bd012c414840bc1bbdae1a60 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 17 Feb 2026 10:24:37 -0800 Subject: [PATCH 049/103] bring in changes from playground branch --- packages/connect-evm/src/connect.ts | 18 ++++++++++-------- .../src/multichain/transports/mwp/index.ts | 9 ++++++--- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/packages/connect-evm/src/connect.ts b/packages/connect-evm/src/connect.ts index a9b87d72..99a5173d 100644 --- a/packages/connect-evm/src/connect.ts +++ b/packages/connect-evm/src/connect.ts @@ -359,15 +359,8 @@ export class MetamaskConnectEVM { this.#status = 'connecting'; try { - await this.#core.connect( - caipChainIds as Scope[], - caipAccountIds as CaipAccountId[], - undefined, - forceRequest, - ); - // Wait for the wallet_sessionChanged event to fire and set the provider properties - return new Promise((resolve) => { + const result = new Promise((resolve) => { this.#provider.once('connect', ({ chainId, accounts }) => { logger('fulfilled-request: connect', { chainId, @@ -379,6 +372,15 @@ export class MetamaskConnectEVM { }); }); }); + + await this.#core.connect( + caipChainIds as Scope[], + caipAccountIds as CaipAccountId[], + undefined, + forceRequest, + ); + + return result as Promise<{ accounts: Address[]; chainId: Hex }>; } catch (error) { this.#status = 'disconnected'; logger('Error connecting to wallet', error); diff --git a/packages/connect-multichain/src/multichain/transports/mwp/index.ts b/packages/connect-multichain/src/multichain/transports/mwp/index.ts index d8067b91..c2e47988 100644 --- a/packages/connect-multichain/src/multichain/transports/mwp/index.ts +++ b/packages/connect-multichain/src/multichain/transports/mwp/index.ts @@ -537,9 +537,12 @@ export class MWPTransport implements ExtendedTransport { ), ); - // This might not actually get excuted on the wallet if the user doesn't open - // their wallet before the message TTL - this.request({ method: 'wallet_revokeSession', params: { scopes } }); + // NOTE: Purposely not awaiting this to avoid blocking the disconnect flow. + // This might not actually get executed on the wallet if the user doesn't open + // their wallet before the message TTL or if the underlying transport isn't actually connected + this.request({ method: 'wallet_revokeSession', params: { scopes } }).catch((err) => { + console.error('error revoking session', err); + }); // Clear the cached values for eth_accounts and eth_chainId if all eip155 scopes were removed. const remainingScopesIncludeEip155 = remainingScopes.some((scope) => scope.includes('eip155')); From 164e6a9d5288adbf01adae594c09b733057c0491 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 17 Feb 2026 10:38:49 -0800 Subject: [PATCH 050/103] lint --- packages/connect-evm/src/connect.ts | 12 +++++++----- .../src/multichain/transports/mwp/index.ts | 13 ++++++++----- .../src/multichain/utils/index.ts | 3 +-- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/connect-evm/src/connect.ts b/packages/connect-evm/src/connect.ts index 99a5173d..93512512 100644 --- a/packages/connect-evm/src/connect.ts +++ b/packages/connect-evm/src/connect.ts @@ -129,6 +129,7 @@ export class MetamaskConnectEVM { * * @param session - The session data */ + // eslint-disable-next-line @typescript-eslint/no-misused-promises this.#sessionChangedHandler = async (session): Promise => { logger('event: wallet_sessionChanged', session); this.#sessionScopes = session?.sessionScopes ?? {}; @@ -138,10 +139,11 @@ export class MetamaskConnectEVM { } else { let initialAccounts: Address[] = []; if (this.#core.status === 'connected') { - const ethAccountsResponse = await this.#core.transport.sendEip1193Message< - { method: 'eth_accounts'; params: [] }, - { result: string[]; id: number; jsonrpc: '2.0' } - >({ method: 'eth_accounts', params: [] }); + const ethAccountsResponse = + await this.#core.transport.sendEip1193Message< + { method: 'eth_accounts'; params: [] }, + { result: string[]; id: number; jsonrpc: '2.0' } + >({ method: 'eth_accounts', params: [] }); initialAccounts = ethAccountsResponse.result as Address[]; } else { initialAccounts = getEthAccounts(this.#sessionScopes); @@ -151,7 +153,7 @@ export class MetamaskConnectEVM { this.#onConnect({ chainId, - accounts: initialAccounts as Address[], + accounts: initialAccounts, }); } }; diff --git a/packages/connect-multichain/src/multichain/transports/mwp/index.ts b/packages/connect-multichain/src/multichain/transports/mwp/index.ts index c2e47988..9f29dc44 100644 --- a/packages/connect-multichain/src/multichain/transports/mwp/index.ts +++ b/packages/connect-multichain/src/multichain/transports/mwp/index.ts @@ -28,7 +28,6 @@ import { } from '@metamask/multichain-api-client'; import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import type { CaipAccountId } from '@metamask/utils'; -import { getPermittedEthChainIds, getEthAccounts, InternalScopeObject, InternalScopesObject } from '@metamask/chain-agnostic-permission'; import { createLogger, @@ -540,12 +539,16 @@ export class MWPTransport implements ExtendedTransport { // NOTE: Purposely not awaiting this to avoid blocking the disconnect flow. // This might not actually get executed on the wallet if the user doesn't open // their wallet before the message TTL or if the underlying transport isn't actually connected - this.request({ method: 'wallet_revokeSession', params: { scopes } }).catch((err) => { - console.error('error revoking session', err); - }); + this.request({ method: 'wallet_revokeSession', params: { scopes } }).catch( + (err) => { + console.error('error revoking session', err); + }, + ); // Clear the cached values for eth_accounts and eth_chainId if all eip155 scopes were removed. - const remainingScopesIncludeEip155 = remainingScopes.some((scope) => scope.includes('eip155')); + const remainingScopesIncludeEip155 = remainingScopes.some((scope) => + scope.includes('eip155'), + ); if (!remainingScopesIncludeEip155) { this.kvstore.delete(ACCOUNTS_STORE_KEY); this.kvstore.delete(CHAIN_STORE_KEY); diff --git a/packages/connect-multichain/src/multichain/utils/index.ts b/packages/connect-multichain/src/multichain/utils/index.ts index 7a32ba14..547f7dbf 100644 --- a/packages/connect-multichain/src/multichain/utils/index.ts +++ b/packages/connect-multichain/src/multichain/utils/index.ts @@ -2,6 +2,7 @@ /* eslint-disable jsdoc/require-param-description -- Auto-generated JSDoc */ /* eslint-disable jsdoc/require-returns -- Auto-generated JSDoc */ /* eslint-disable @typescript-eslint/explicit-function-return-type -- Inferred types are sufficient */ +import type { SessionProperties } from '@metamask/multichain-api-client'; import { type CaipAccountId, type CaipChainId, @@ -10,8 +11,6 @@ import { } from '@metamask/utils'; import { deflate } from 'pako'; -import type { SessionProperties } from '@metamask/multichain-api-client'; - import { type DappSettings, getPlatformType, From f60c76f9a4e13686b7cb3d99d626ecdea5ffeeb5 Mon Sep 17 00:00:00 2001 From: jiexi Date: Tue, 17 Feb 2026 10:51:31 -0800 Subject: [PATCH 051/103] Apply suggestion from @ffmcgee725 Co-authored-by: ffmcgee <51971598+ffmcgee725@users.noreply.github.com> --- .../src/multichain/index.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/connect-multichain/src/multichain/index.ts b/packages/connect-multichain/src/multichain/index.ts index 496eda13..be9d21e0 100644 --- a/packages/connect-multichain/src/multichain/index.ts +++ b/packages/connect-multichain/src/multichain/index.ts @@ -927,17 +927,17 @@ export class MetaMaskConnectMultichain extends MultichainCore { } async emitSessionChanged(): Promise { + const emptySession = { sessionScopes: {} }; + if (this.status !== 'connected' && this.status !== 'connecting') { - this.emit('wallet_sessionChanged', { sessionScopes: {} }); - } else { - const response = await this.transport.request({ - method: 'wallet_getSession', - }); - if (response.result) { - this.emit('wallet_sessionChanged', response.result); - } else { - this.emit('wallet_sessionChanged', { sessionScopes: {} }); - } + this.emit('wallet_sessionChanged', emptySession); + return; } - } + + const response = await this.transport.request({ + method: 'wallet_getSession', + }); + + this.emit('wallet_sessionChanged', response.result ?? emptySession); +} } From f45468c45293d2834b471b3bda206d2560410d56 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 17 Feb 2026 10:56:04 -0800 Subject: [PATCH 052/103] get rid of typecast --- packages/connect-evm/src/connect.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/connect-evm/src/connect.ts b/packages/connect-evm/src/connect.ts index 93512512..864f3387 100644 --- a/packages/connect-evm/src/connect.ts +++ b/packages/connect-evm/src/connect.ts @@ -142,9 +142,9 @@ export class MetamaskConnectEVM { const ethAccountsResponse = await this.#core.transport.sendEip1193Message< { method: 'eth_accounts'; params: [] }, - { result: string[]; id: number; jsonrpc: '2.0' } + { result: Address[]; id: number; jsonrpc: '2.0' } >({ method: 'eth_accounts', params: [] }); - initialAccounts = ethAccountsResponse.result as Address[]; + initialAccounts = ethAccountsResponse.result; } else { initialAccounts = getEthAccounts(this.#sessionScopes); } From 1426333b36f64d152f022ec9adb7816dd686fd0c Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 17 Feb 2026 11:00:29 -0800 Subject: [PATCH 053/103] unset global singleton if throws --- packages/connect-multichain/src/multichain/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/connect-multichain/src/multichain/index.ts b/packages/connect-multichain/src/multichain/index.ts index be9d21e0..a42410cf 100644 --- a/packages/connect-multichain/src/multichain/index.ts +++ b/packages/connect-multichain/src/multichain/index.ts @@ -190,6 +190,12 @@ export class MetaMaskConnectMultichain extends MultichainCore { })(); globalObject[SINGLETON_KEY] = instancePromise; + + instancePromise.catch((error) => { + globalObject[SINGLETON_KEY] = undefined; + console.error('Error initializing MetaMaskConnectMultichain', error); + }); + return instancePromise; } From 89e45a68a558bad4debfcaec3fde7f066f59fd3b Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 17 Feb 2026 11:20:56 -0800 Subject: [PATCH 054/103] use optional chaining for merging options --- .../src/domain/multichain/index.ts | 59 +++++++------------ 1 file changed, 21 insertions(+), 38 deletions(-) diff --git a/packages/connect-multichain/src/domain/multichain/index.ts b/packages/connect-multichain/src/domain/multichain/index.ts index d4f6327d..369152e3 100644 --- a/packages/connect-multichain/src/domain/multichain/index.ts +++ b/packages/connect-multichain/src/domain/multichain/index.ts @@ -76,7 +76,7 @@ export abstract class MultichainCore extends EventEmitter { abstract emitSessionChanged(): Promise; - constructor(protected readonly options: MultichainOptions) { + constructor(protected options: MultichainOptions) { super(); } @@ -88,48 +88,31 @@ export abstract class MultichainCore extends EventEmitter { * @param partial - Options to merge/overwrite onto the current instance */ mergeOptions(partial: MergeableMultichainOptions): void { - const opts = this.options as MultichainOptions & { - api: { supportedNetworks: MultichainOptions['api']['supportedNetworks'] }; - ui: MultichainOptions['ui']; - mobile?: MultichainOptions['mobile']; - transport?: MultichainOptions['transport']; - debug?: boolean; - }; - if (partial.api?.supportedNetworks !== undefined) { - opts.api = { + let opts = this.options; + this.options = { + ...opts, + api: { ...opts.api, supportedNetworks: { ...opts.api.supportedNetworks, - ...partial.api.supportedNetworks, + ...(partial.api?.supportedNetworks ?? {}), }, - }; - } - if (partial.ui !== undefined) { - const uiUpdates: Partial = {}; - if (partial.ui.headless !== undefined) { - uiUpdates.headless = partial.ui.headless; - } - if (partial.ui.preferExtension !== undefined) { - uiUpdates.preferExtension = partial.ui.preferExtension; - } - if (partial.ui.showInstallModal !== undefined) { - uiUpdates.showInstallModal = partial.ui.showInstallModal; - } - if (Object.keys(uiUpdates).length > 0) { - opts.ui = { ...opts.ui, ...uiUpdates }; - } - } - if (partial.mobile !== undefined) { - opts.mobile = { ...(opts.mobile ?? {}), ...partial.mobile }; - } - if (partial.transport?.extensionId !== undefined) { - opts.transport = { + }, + ui: { + ...opts.ui, + headless: partial.ui?.headless ?? opts.ui.headless, + preferExtension: partial.ui?.preferExtension ?? opts.ui.preferExtension, + showInstallModal: partial.ui?.showInstallModal ?? opts.ui.showInstallModal, + }, + mobile: { + ...opts.mobile, + ...(partial.mobile ?? {}), + }, + transport: { ...(opts.transport ?? {}), - extensionId: partial.transport.extensionId, - }; - } - if (partial.debug !== undefined) { - opts.debug = partial.debug; + extensionId: partial.transport?.extensionId ?? opts.transport?.extensionId, + }, + debug: partial.debug ?? opts.debug, } } } From d9a97351e86dff5ddb1226db7880af123d91053f Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 17 Feb 2026 11:47:48 -0800 Subject: [PATCH 055/103] add option merging spec --- .../src/domain/multichain/index.test.ts | 290 ++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 packages/connect-multichain/src/domain/multichain/index.test.ts diff --git a/packages/connect-multichain/src/domain/multichain/index.test.ts b/packages/connect-multichain/src/domain/multichain/index.test.ts new file mode 100644 index 00000000..b0475471 --- /dev/null +++ b/packages/connect-multichain/src/domain/multichain/index.test.ts @@ -0,0 +1,290 @@ +/* eslint-disable id-length -- vitest alias */ +import type { MultichainApiClient } from '@metamask/multichain-api-client'; +import * as t from 'vitest'; + +import { + MultichainCore, + TransportType, + type ConnectionStatus, +} from './index'; +import type { RPCAPI, RpcUrlsMap } from './api/types'; +import type { + ExtendedTransport, + MergeableMultichainOptions, + MultichainOptions, +} from './types'; +import type { StoreClient } from '../store/client'; + +/** + * Minimal concrete implementation of MultichainCore for testing mergeOptions. + * Abstract members are stubbed; only options and mergeOptions are under test. + */ +class TestMultichainCore extends MultichainCore { + storage = {} as StoreClient; + + status: ConnectionStatus = 'loaded'; + + provider = {} as MultichainApiClient; + + transport = {} as ExtendedTransport; + + transportType = TransportType.UNKNOWN; + + connect = () => Promise.resolve(); + + disconnect = () => Promise.resolve(); + + invokeMethod = () => Promise.resolve({}); + + openDeeplinkIfNeeded = () => {}; + + emitSessionChanged = () => Promise.resolve(); + + getOptions(): MultichainOptions { + return this.options; + } +} + +function createBaseOptions(): MultichainOptions { + return { + dapp: { name: 'Test Dapp', url: 'https://example.com' }, + api: { + supportedNetworks: { + 'eip155:1': 'https://eth.mainnet.example', + 'eip155:11155111': 'https://eth.sepolia.example', + } as RpcUrlsMap, + }, + storage: {} as StoreClient, + ui: { + factory: {} as MultichainOptions['ui']['factory'], + headless: false, + preferExtension: true, + showInstallModal: false, + }, + mobile: { + useDeeplink: false, + }, + transport: { + extensionId: 'ext-123', + }, + debug: false, + }; +} + +t.describe('MultichainCore', () => { + t.describe('mergeOptions', () => { + t.it('merges api.supportedNetworks shallowly over existing', () => { + const base = createBaseOptions(); + const core = new TestMultichainCore(base); + + core.mergeOptions({ + api: { + supportedNetworks: { + 'eip155:1': 'https://overridden.example', + 'solana:mainnet': 'https://solana.example', + }, + }, + }); + + const opts = core.getOptions(); + t.expect(opts.api.supportedNetworks['eip155:1']).toBe( + 'https://overridden.example', + ); + t.expect(opts.api.supportedNetworks['eip155:11155111']).toBe( + 'https://eth.sepolia.example', + ); + t.expect(opts.api.supportedNetworks['solana:mainnet']).toBe( + 'https://solana.example', + ); + }); + + t.it('leaves api.supportedNetworks unchanged when partial.api is omitted', () => { + const base = createBaseOptions(); + const core = new TestMultichainCore(base); + + core.mergeOptions({}); + + const opts = core.getOptions(); + t.expect(opts.api.supportedNetworks).toEqual(base.api.supportedNetworks); + }); + + t.it('leaves api.supportedNetworks unchanged when partial.api.supportedNetworks is empty', () => { + const base = createBaseOptions(); + const core = new TestMultichainCore(base); + + core.mergeOptions({ api: { supportedNetworks: {} } }); + + const opts = core.getOptions(); + t.expect(opts.api.supportedNetworks).toEqual(base.api.supportedNetworks); + }); + + t.it('merges ui.headless, preferExtension, showInstallModal from partial', () => { + const base = createBaseOptions(); + const core = new TestMultichainCore(base); + + core.mergeOptions({ + ui: { + headless: true, + preferExtension: false, + showInstallModal: true, + }, + }); + + const opts = core.getOptions(); + t.expect(opts.ui.headless).toBe(true); + t.expect(opts.ui.preferExtension).toBe(false); + t.expect(opts.ui.showInstallModal).toBe(true); + t.expect(opts.ui.factory).toBe(base.ui.factory); + }); + + t.it('keeps existing ui values when partial.ui fields are omitted', () => { + const base = createBaseOptions(); + base.ui.headless = true; + base.ui.preferExtension = false; + const core = new TestMultichainCore(base); + + core.mergeOptions({ ui: {} }); + + const opts = core.getOptions(); + t.expect(opts.ui.headless).toBe(true); + t.expect(opts.ui.preferExtension).toBe(false); + t.expect(opts.ui.showInstallModal).toBe(false); + }); + + t.it('merges mobile options over existing', () => { + const base = createBaseOptions(); + const core = new TestMultichainCore(base); + + core.mergeOptions({ + mobile: { + useDeeplink: true, + }, + }); + + const opts = core.getOptions(); + t.expect(opts.mobile?.useDeeplink).toBe(true); + t.expect(opts.mobile).toEqual({ useDeeplink: true }); + }); + + t.it('leaves mobile unchanged when partial.mobile is omitted', () => { + const base = createBaseOptions(); + const core = new TestMultichainCore(base); + + core.mergeOptions({}); + + const opts = core.getOptions(); + t.expect(opts.mobile).toEqual(base.mobile); + }); + + t.it('merges transport.extensionId from partial', () => { + const base = createBaseOptions(); + const core = new TestMultichainCore(base); + + core.mergeOptions({ + transport: { extensionId: 'new-ext-456' }, + }); + + const opts = core.getOptions(); + t.expect(opts.transport?.extensionId).toBe('new-ext-456'); + }); + + t.it('preserves existing transport when partial.transport is omitted', () => { + const base = createBaseOptions(); + const core = new TestMultichainCore(base); + + core.mergeOptions({}); + + const opts = core.getOptions(); + t.expect(opts.transport).toEqual(base.transport); + }); + + t.it('sets transport when initial options had no transport', () => { + const base = createBaseOptions(); + delete base.transport; + const core = new TestMultichainCore(base); + + core.mergeOptions({ transport: { extensionId: 'new-ext' } }); + + const opts = core.getOptions(); + t.expect(opts.transport?.extensionId).toBe('new-ext'); + }); + + t.it('merges debug from partial', () => { + const base = createBaseOptions(); + const core = new TestMultichainCore(base); + + core.mergeOptions({ debug: true }); + + const opts = core.getOptions(); + t.expect(opts.debug).toBe(true); + }); + + t.it('keeps existing debug when partial.debug is omitted', () => { + const base = createBaseOptions(); + base.debug = true; + const core = new TestMultichainCore(base); + + core.mergeOptions({}); + + const opts = core.getOptions(); + t.expect(opts.debug).toBe(true); + }); + + t.it('does not mutate dapp, storage, or analytics', () => { + const base = createBaseOptions(); + base.analytics = { integrationType: 'direct' }; + const core = new TestMultichainCore(base); + + core.mergeOptions({ + api: { supportedNetworks: { 'eip155:1': 'https://x.com' } }, + ui: { headless: true }, + debug: true, + }); + + const opts = core.getOptions(); + t.expect(opts.dapp).toBe(base.dapp); + t.expect(opts.storage).toBe(base.storage); + t.expect(opts.analytics).toEqual(base.analytics); + }); + + t.it('handles full partial merge correctly', () => { + const base = createBaseOptions(); + const core = new TestMultichainCore(base); + + const partial: MergeableMultichainOptions = { + api: { + supportedNetworks: { + 'eip155:1': 'https://merged-eth.example', + }, + }, + ui: { + headless: true, + preferExtension: false, + showInstallModal: true, + }, + mobile: { useDeeplink: true }, + transport: { extensionId: 'merged-ext' }, + debug: true, + }; + + core.mergeOptions(partial); + + const opts = core.getOptions(); + t.expect(opts.api.supportedNetworks['eip155:1']).toBe( + 'https://merged-eth.example', + ); + t.expect(opts.api.supportedNetworks['eip155:11155111']).toBe( + 'https://eth.sepolia.example', + ); + t.expect(opts.ui).toMatchObject({ + headless: true, + preferExtension: false, + showInstallModal: true, + }); + t.expect(opts.mobile).toEqual({ useDeeplink: true }); + t.expect(opts.transport?.extensionId).toBe('merged-ext'); + t.expect(opts.debug).toBe(true); + }); + }); +}); + From d04c9770a466cfc9ff20256349395ea25e3747b4 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 17 Feb 2026 11:52:00 -0800 Subject: [PATCH 056/103] exclude dapp and analytics for mergable type --- packages/connect-multichain/src/domain/multichain/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/connect-multichain/src/domain/multichain/types.ts b/packages/connect-multichain/src/domain/multichain/types.ts index 3ee1c4c0..57749ddb 100644 --- a/packages/connect-multichain/src/domain/multichain/types.ts +++ b/packages/connect-multichain/src/domain/multichain/types.ts @@ -95,7 +95,7 @@ type MultiChainFNOptions = Omit & { */ export type MergeableMultichainOptions = Omit< MultichainOptions, - 'storage' | 'api' | 'ui' | 'transport' + 'dapp' | 'analytics' | 'storage' | 'api' | 'ui' | 'transport' > & { api?: MultichainOptions['api']; ui?: Pick< From 54e17740aca5fb709e56dacdf78451a5c11d33fe Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 17 Feb 2026 11:52:12 -0800 Subject: [PATCH 057/103] update mergeOptions spec --- .../src/domain/multichain/index.test.ts | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/packages/connect-multichain/src/domain/multichain/index.test.ts b/packages/connect-multichain/src/domain/multichain/index.test.ts index b0475471..9683d04d 100644 --- a/packages/connect-multichain/src/domain/multichain/index.test.ts +++ b/packages/connect-multichain/src/domain/multichain/index.test.ts @@ -15,11 +15,7 @@ import type { } from './types'; import type { StoreClient } from '../store/client'; -/** - * Minimal concrete implementation of MultichainCore for testing mergeOptions. - * Abstract members are stubbed; only options and mergeOptions are under test. - */ -class TestMultichainCore extends MultichainCore { +class MockMultichainCore extends MultichainCore { storage = {} as StoreClient; status: ConnectionStatus = 'loaded'; @@ -75,7 +71,7 @@ t.describe('MultichainCore', () => { t.describe('mergeOptions', () => { t.it('merges api.supportedNetworks shallowly over existing', () => { const base = createBaseOptions(); - const core = new TestMultichainCore(base); + const core = new MockMultichainCore(base); core.mergeOptions({ api: { @@ -100,7 +96,7 @@ t.describe('MultichainCore', () => { t.it('leaves api.supportedNetworks unchanged when partial.api is omitted', () => { const base = createBaseOptions(); - const core = new TestMultichainCore(base); + const core = new MockMultichainCore(base); core.mergeOptions({}); @@ -110,7 +106,7 @@ t.describe('MultichainCore', () => { t.it('leaves api.supportedNetworks unchanged when partial.api.supportedNetworks is empty', () => { const base = createBaseOptions(); - const core = new TestMultichainCore(base); + const core = new MockMultichainCore(base); core.mergeOptions({ api: { supportedNetworks: {} } }); @@ -120,7 +116,7 @@ t.describe('MultichainCore', () => { t.it('merges ui.headless, preferExtension, showInstallModal from partial', () => { const base = createBaseOptions(); - const core = new TestMultichainCore(base); + const core = new MockMultichainCore(base); core.mergeOptions({ ui: { @@ -141,7 +137,7 @@ t.describe('MultichainCore', () => { const base = createBaseOptions(); base.ui.headless = true; base.ui.preferExtension = false; - const core = new TestMultichainCore(base); + const core = new MockMultichainCore(base); core.mergeOptions({ ui: {} }); @@ -153,7 +149,7 @@ t.describe('MultichainCore', () => { t.it('merges mobile options over existing', () => { const base = createBaseOptions(); - const core = new TestMultichainCore(base); + const core = new MockMultichainCore(base); core.mergeOptions({ mobile: { @@ -168,7 +164,7 @@ t.describe('MultichainCore', () => { t.it('leaves mobile unchanged when partial.mobile is omitted', () => { const base = createBaseOptions(); - const core = new TestMultichainCore(base); + const core = new MockMultichainCore(base); core.mergeOptions({}); @@ -178,7 +174,7 @@ t.describe('MultichainCore', () => { t.it('merges transport.extensionId from partial', () => { const base = createBaseOptions(); - const core = new TestMultichainCore(base); + const core = new MockMultichainCore(base); core.mergeOptions({ transport: { extensionId: 'new-ext-456' }, @@ -190,7 +186,7 @@ t.describe('MultichainCore', () => { t.it('preserves existing transport when partial.transport is omitted', () => { const base = createBaseOptions(); - const core = new TestMultichainCore(base); + const core = new MockMultichainCore(base); core.mergeOptions({}); @@ -201,7 +197,7 @@ t.describe('MultichainCore', () => { t.it('sets transport when initial options had no transport', () => { const base = createBaseOptions(); delete base.transport; - const core = new TestMultichainCore(base); + const core = new MockMultichainCore(base); core.mergeOptions({ transport: { extensionId: 'new-ext' } }); @@ -211,7 +207,7 @@ t.describe('MultichainCore', () => { t.it('merges debug from partial', () => { const base = createBaseOptions(); - const core = new TestMultichainCore(base); + const core = new MockMultichainCore(base); core.mergeOptions({ debug: true }); @@ -222,7 +218,7 @@ t.describe('MultichainCore', () => { t.it('keeps existing debug when partial.debug is omitted', () => { const base = createBaseOptions(); base.debug = true; - const core = new TestMultichainCore(base); + const core = new MockMultichainCore(base); core.mergeOptions({}); @@ -233,7 +229,7 @@ t.describe('MultichainCore', () => { t.it('does not mutate dapp, storage, or analytics', () => { const base = createBaseOptions(); base.analytics = { integrationType: 'direct' }; - const core = new TestMultichainCore(base); + const core = new MockMultichainCore(base); core.mergeOptions({ api: { supportedNetworks: { 'eip155:1': 'https://x.com' } }, @@ -249,7 +245,7 @@ t.describe('MultichainCore', () => { t.it('handles full partial merge correctly', () => { const base = createBaseOptions(); - const core = new TestMultichainCore(base); + const core = new MockMultichainCore(base); const partial: MergeableMultichainOptions = { api: { @@ -287,4 +283,3 @@ t.describe('MultichainCore', () => { }); }); }); - From 168d7c2dda14525812e47cff2fc2b86d7ba024ee Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 17 Feb 2026 11:59:30 -0800 Subject: [PATCH 058/103] move sessionChangedHandler into method --- packages/connect-evm/src/connect.ts | 56 +++++++++++++++-------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/packages/connect-evm/src/connect.ts b/packages/connect-evm/src/connect.ts index 864f3387..2d626098 100644 --- a/packages/connect-evm/src/connect.ts +++ b/packages/connect-evm/src/connect.ts @@ -130,33 +130,7 @@ export class MetamaskConnectEVM { * @param session - The session data */ // eslint-disable-next-line @typescript-eslint/no-misused-promises - this.#sessionChangedHandler = async (session): Promise => { - logger('event: wallet_sessionChanged', session); - this.#sessionScopes = session?.sessionScopes ?? {}; - const hexPermittedChainIds = getPermittedEthChainIds(this.#sessionScopes); - if (hexPermittedChainIds.length === 0) { - this.#onDisconnect(); - } else { - let initialAccounts: Address[] = []; - if (this.#core.status === 'connected') { - const ethAccountsResponse = - await this.#core.transport.sendEip1193Message< - { method: 'eth_accounts'; params: [] }, - { result: Address[]; id: number; jsonrpc: '2.0' } - >({ method: 'eth_accounts', params: [] }); - initialAccounts = ethAccountsResponse.result; - } else { - initialAccounts = getEthAccounts(this.#sessionScopes); - } - - const chainId = await this.#getSelectedChainId(hexPermittedChainIds); - - this.#onConnect({ - chainId, - accounts: initialAccounts, - }); - } - }; + this.#sessionChangedHandler = this.#onSessionChanged.bind(this); this.#core.on('wallet_sessionChanged', this.#sessionChangedHandler); /** @@ -748,6 +722,34 @@ export class MetamaskConnectEVM { } } + async #onSessionChanged(session?: SessionData): Promise { + logger('event: wallet_sessionChanged', session); + this.#sessionScopes = session?.sessionScopes ?? {}; + const hexPermittedChainIds = getPermittedEthChainIds(this.#sessionScopes); + if (hexPermittedChainIds.length === 0) { + this.#onDisconnect(); + } else { + let initialAccounts: Address[] = []; + if (this.#core.status === 'connected') { + const ethAccountsResponse = + await this.#core.transport.sendEip1193Message< + { method: 'eth_accounts'; params: [] }, + { result: Address[]; id: number; jsonrpc: '2.0' } + >({ method: 'eth_accounts', params: [] }); + initialAccounts = ethAccountsResponse.result; + } else { + initialAccounts = getEthAccounts(this.#sessionScopes); + } + + const chainId = await this.#getSelectedChainId(hexPermittedChainIds); + + this.#onConnect({ + chainId, + accounts: initialAccounts, + }); + } + } + /** * Handles chain change events and updates the provider's selected chain ID. * From 45faffa7c3c7259906e337c3a71c4d7cc8d1682f Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 17 Feb 2026 12:04:26 -0800 Subject: [PATCH 059/103] lint --- .../src/domain/multichain/index.test.ts | 131 ++++++++++-------- .../src/domain/multichain/index.ts | 10 +- .../src/multichain/index.ts | 2 +- 3 files changed, 84 insertions(+), 59 deletions(-) diff --git a/packages/connect-multichain/src/domain/multichain/index.test.ts b/packages/connect-multichain/src/domain/multichain/index.test.ts index 9683d04d..4b3fe7d1 100644 --- a/packages/connect-multichain/src/domain/multichain/index.test.ts +++ b/packages/connect-multichain/src/domain/multichain/index.test.ts @@ -1,12 +1,9 @@ /* eslint-disable id-length -- vitest alias */ import type { MultichainApiClient } from '@metamask/multichain-api-client'; +import type { Json } from '@metamask/utils'; import * as t from 'vitest'; -import { - MultichainCore, - TransportType, - type ConnectionStatus, -} from './index'; +import { MultichainCore, TransportType, type ConnectionStatus } from '.'; import type { RPCAPI, RpcUrlsMap } from './api/types'; import type { ExtendedTransport, @@ -26,21 +23,31 @@ class MockMultichainCore extends MultichainCore { transportType = TransportType.UNKNOWN; - connect = () => Promise.resolve(); + connect = async (): Promise => Promise.resolve(); - disconnect = () => Promise.resolve(); + disconnect = async (): Promise => Promise.resolve(); - invokeMethod = () => Promise.resolve({}); + invokeMethod = async (): Promise => Promise.resolve({}); - openDeeplinkIfNeeded = () => {}; + openDeeplinkIfNeeded = (): void => undefined; - emitSessionChanged = () => Promise.resolve(); + emitSessionChanged = async (): Promise => Promise.resolve(); + /** + * Exposes options for test assertions. + * + * @returns Current merged options. + */ getOptions(): MultichainOptions { return this.options; } } +/** + * Creates base multichain options for tests. + * + * @returns Default MultichainOptions used in mergeOptions tests. + */ function createBaseOptions(): MultichainOptions { return { dapp: { name: 'Test Dapp', url: 'https://example.com' }, @@ -94,44 +101,57 @@ t.describe('MultichainCore', () => { ); }); - t.it('leaves api.supportedNetworks unchanged when partial.api is omitted', () => { - const base = createBaseOptions(); - const core = new MockMultichainCore(base); - - core.mergeOptions({}); - - const opts = core.getOptions(); - t.expect(opts.api.supportedNetworks).toEqual(base.api.supportedNetworks); - }); - - t.it('leaves api.supportedNetworks unchanged when partial.api.supportedNetworks is empty', () => { - const base = createBaseOptions(); - const core = new MockMultichainCore(base); - - core.mergeOptions({ api: { supportedNetworks: {} } }); - - const opts = core.getOptions(); - t.expect(opts.api.supportedNetworks).toEqual(base.api.supportedNetworks); - }); - - t.it('merges ui.headless, preferExtension, showInstallModal from partial', () => { - const base = createBaseOptions(); - const core = new MockMultichainCore(base); - - core.mergeOptions({ - ui: { - headless: true, - preferExtension: false, - showInstallModal: true, - }, - }); + t.it( + 'leaves api.supportedNetworks unchanged when partial.api is omitted', + () => { + const base = createBaseOptions(); + const core = new MockMultichainCore(base); + + core.mergeOptions({}); + + const opts = core.getOptions(); + t.expect(opts.api.supportedNetworks).toEqual( + base.api.supportedNetworks, + ); + }, + ); + + t.it( + 'leaves api.supportedNetworks unchanged when partial.api.supportedNetworks is empty', + () => { + const base = createBaseOptions(); + const core = new MockMultichainCore(base); + + core.mergeOptions({ api: { supportedNetworks: {} } }); + + const opts = core.getOptions(); + t.expect(opts.api.supportedNetworks).toEqual( + base.api.supportedNetworks, + ); + }, + ); + + t.it( + 'merges ui.headless, preferExtension, showInstallModal from partial', + () => { + const base = createBaseOptions(); + const core = new MockMultichainCore(base); + + core.mergeOptions({ + ui: { + headless: true, + preferExtension: false, + showInstallModal: true, + }, + }); - const opts = core.getOptions(); - t.expect(opts.ui.headless).toBe(true); - t.expect(opts.ui.preferExtension).toBe(false); - t.expect(opts.ui.showInstallModal).toBe(true); - t.expect(opts.ui.factory).toBe(base.ui.factory); - }); + const opts = core.getOptions(); + t.expect(opts.ui.headless).toBe(true); + t.expect(opts.ui.preferExtension).toBe(false); + t.expect(opts.ui.showInstallModal).toBe(true); + t.expect(opts.ui.factory).toBe(base.ui.factory); + }, + ); t.it('keeps existing ui values when partial.ui fields are omitted', () => { const base = createBaseOptions(); @@ -184,15 +204,18 @@ t.describe('MultichainCore', () => { t.expect(opts.transport?.extensionId).toBe('new-ext-456'); }); - t.it('preserves existing transport when partial.transport is omitted', () => { - const base = createBaseOptions(); - const core = new MockMultichainCore(base); + t.it( + 'preserves existing transport when partial.transport is omitted', + () => { + const base = createBaseOptions(); + const core = new MockMultichainCore(base); - core.mergeOptions({}); + core.mergeOptions({}); - const opts = core.getOptions(); - t.expect(opts.transport).toEqual(base.transport); - }); + const opts = core.getOptions(); + t.expect(opts.transport).toEqual(base.transport); + }, + ); t.it('sets transport when initial options had no transport', () => { const base = createBaseOptions(); diff --git a/packages/connect-multichain/src/domain/multichain/index.ts b/packages/connect-multichain/src/domain/multichain/index.ts index 369152e3..e5066530 100644 --- a/packages/connect-multichain/src/domain/multichain/index.ts +++ b/packages/connect-multichain/src/domain/multichain/index.ts @@ -88,7 +88,7 @@ export abstract class MultichainCore extends EventEmitter { * @param partial - Options to merge/overwrite onto the current instance */ mergeOptions(partial: MergeableMultichainOptions): void { - let opts = this.options; + const opts = this.options; this.options = { ...opts, api: { @@ -102,7 +102,8 @@ export abstract class MultichainCore extends EventEmitter { ...opts.ui, headless: partial.ui?.headless ?? opts.ui.headless, preferExtension: partial.ui?.preferExtension ?? opts.ui.preferExtension, - showInstallModal: partial.ui?.showInstallModal ?? opts.ui.showInstallModal, + showInstallModal: + partial.ui?.showInstallModal ?? opts.ui.showInstallModal, }, mobile: { ...opts.mobile, @@ -110,10 +111,11 @@ export abstract class MultichainCore extends EventEmitter { }, transport: { ...(opts.transport ?? {}), - extensionId: partial.transport?.extensionId ?? opts.transport?.extensionId, + extensionId: + partial.transport?.extensionId ?? opts.transport?.extensionId, }, debug: partial.debug ?? opts.debug, - } + }; } } /* c8 ignore end */ diff --git a/packages/connect-multichain/src/multichain/index.ts b/packages/connect-multichain/src/multichain/index.ts index a42410cf..f0b4c3e3 100644 --- a/packages/connect-multichain/src/multichain/index.ts +++ b/packages/connect-multichain/src/multichain/index.ts @@ -945,5 +945,5 @@ export class MetaMaskConnectMultichain extends MultichainCore { }); this.emit('wallet_sessionChanged', response.result ?? emptySession); -} + } } From e21639393ebbece99a5c794a01dc2474aa47deff Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 17 Feb 2026 14:24:56 -0800 Subject: [PATCH 060/103] WIP test --- packages/connect-evm/src/connect.test.ts | 277 ++++++++++++++++++++++- 1 file changed, 273 insertions(+), 4 deletions(-) diff --git a/packages/connect-evm/src/connect.test.ts b/packages/connect-evm/src/connect.test.ts index 34a74163..b874af48 100644 --- a/packages/connect-evm/src/connect.test.ts +++ b/packages/connect-evm/src/connect.test.ts @@ -1,8 +1,277 @@ /* eslint-disable @typescript-eslint/no-shadow -- Vitest globals */ -import { describe, it, expect } from 'vitest'; +import type { SessionData } from '@metamask/connect-multichain'; +import { describe, it, expect, vi, type Mock } from 'vitest'; -describe('smoke', () => { - it('works', () => { - expect(true).toBe(true); +import { MetamaskConnectEVM } from './connect'; +import type { MultichainCore } from '@metamask/connect-multichain'; + +type Status = 'connected' | 'disconnected' | 'connecting' | 'loaded' | 'pending'; + +/** Mock core type so storage/transport mocks keep .mockResolvedValue in tests */ +type MockCore = MultichainCore & { + emit: (event: string, ...args: unknown[]) => void; + _status: Status; + storage: MultichainCore['storage'] & { + adapter: { + get: Mock<(key: string) => Promise>; + set: Mock<(key: string, value: string) => Promise>; + }; + }; + transport: MultichainCore['transport'] & { + sendEip1193Message: Mock; + }; +}; + +function createMockCore(): MockCore { + const handlers: Record void)[]> = {}; + let _status: Status = 'disconnected'; + + const sendEip1193Message = vi.fn().mockResolvedValue({ + result: [] as string[], + id: 1, + jsonrpc: '2.0' as const, + }); + const onNotification = vi.fn().mockReturnValue(() => {}); + + const storageGet = vi.fn().mockResolvedValue(null); + const storageSet = vi.fn().mockResolvedValue(undefined); + + const mockCore = { + _status: _status as Status, + get status() { + return this._status; + }, + set status(value: Status) { + this._status = value; + }, + on(event: string, handler: (...args: unknown[]) => void): void { + if (!handlers[event]) handlers[event] = []; + handlers[event].push(handler); + }, + emit(event: string, ...args: unknown[]): void { + handlers[event]?.forEach((h) => h(...args)); + }, + emitSessionChanged: vi.fn().mockImplementation(async (): Promise => { + mockCore.emit('wallet_sessionChanged', { sessionScopes: {} }); + }), + transport: { + sendEip1193Message, + onNotification, + }, + storage: { + adapter: { + get: storageGet, + set: storageSet, + }, + }, + }; + + mockCore._status = _status; + return mockCore as unknown as MockCore; +} + +describe('MetamaskConnectEVM', () => { + describe('#onSessionChanged', () => { + it('disconnects when session has no permitted EIP-155 chain IDs if the MultichainClient is connected', async () => { + const mockCore = createMockCore(); + mockCore._status = 'connecting' + mockCore.storage.adapter.get.mockResolvedValue(JSON.stringify('0x1')); + const client = await MetamaskConnectEVM.create({ core: mockCore }); + const session: SessionData = { + sessionScopes: { + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': { + methods: [], + notifications: [], + accounts: ['solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:1234567890'], + }, + }, + }; + mockCore.emit('wallet_sessionChanged', session); + await new Promise((resolve) => { + client.getProvider().once('connect', () => resolve()); + }); + const disconnectPromise = new Promise((resolve) => { + client.getProvider().once('disconnect', resolve); + }); + mockCore.emit('wallet_sessionChanged', { sessionScopes: {} }); + await disconnectPromise; + expect(client.accounts).toEqual([]); + }); + + it('disconnects when wallet_sessionChanged is emitted with undefined session after being connected', async () => { + const mockCore = createMockCore(); + mockCore.storage.adapter.get.mockResolvedValue(JSON.stringify('0x1')); + const client = await MetamaskConnectEVM.create({ core: mockCore }); + const session: SessionData = { + sessionScopes: { + 'eip155:1': { + methods: [], + notifications: [], + accounts: ['eip155:1:0x1234567890123456789012345678901234567890'], + }, + }, + }; + mockCore.emit('wallet_sessionChanged', session); + await new Promise((resolve) => { + client.getProvider().once('connect', () => resolve()); + }); + const disconnectPromise = new Promise((resolve) => { + client.getProvider().once('disconnect', resolve); + }); + mockCore.emit('wallet_sessionChanged', undefined); + await disconnectPromise; + expect(client.accounts).toEqual([]); + }); + + it('disconnects when wallet_sessionChanged is emitted with empty sessionScopes after being connected', async () => { + const mockCore = createMockCore(); + mockCore.storage.adapter.get.mockResolvedValue(JSON.stringify('0x1')); + const client = await MetamaskConnectEVM.create({ core: mockCore }); + const session: SessionData = { + sessionScopes: { + 'eip155:1': { + methods: [], + notifications: [], + accounts: ['eip155:1:0x1234567890123456789012345678901234567890'], + }, + }, + }; + mockCore.emit('wallet_sessionChanged', session); + await new Promise((resolve) => { + client.getProvider().once('connect', () => resolve()); + }); + const disconnectPromise = new Promise((resolve) => { + client.getProvider().once('disconnect', resolve); + }); + mockCore.emit('wallet_sessionChanged', { sessionScopes: {} }); + await disconnectPromise; + expect(client.accounts).toEqual([]); + }); + + it('connects using the accounts from the CAIP-25 permissions when the MultichainClient is disconnected', async () => { + const mockCore = createMockCore(); + mockCore.storage.adapter.get.mockResolvedValue(JSON.stringify('0x1')); + const client = await MetamaskConnectEVM.create({ core: mockCore }); + + const connectPromise = new Promise<{ + chainId: string; + accounts: string[]; + }>((resolve) => { + client.getProvider().once('connect', resolve); + }); + + const session: SessionData = { + sessionScopes: { + 'eip155:1': { + methods: [], + notifications: [], + accounts: ['eip155:1:0x1234567890123456789012345678901234567890'], + }, + }, + }; + mockCore.emit('wallet_sessionChanged', session); + + const connectData = await connectPromise; + expect(connectData.chainId).toBe('0x1'); + expect(connectData.accounts).toContain( + '0x1234567890123456789012345678901234567890', + ); + }); + + it('connects using accounts from a eth_accounts response when the MultichainClient is connected', async () => { + const mockCore = createMockCore(); + mockCore._status = 'connected'; + mockCore.storage.adapter.get.mockResolvedValue(JSON.stringify('0x1')); + mockCore.transport.sendEip1193Message.mockResolvedValue({ + result: ['0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'], + id: 1, + jsonrpc: '2.0', + }); + + const client = await MetamaskConnectEVM.create({ core: mockCore }); + + const connectPromise = new Promise<{ + chainId: string; + accounts: string[]; + }>((resolve) => { + client.getProvider().once('connect', resolve); + }); + + const session: SessionData = { + sessionScopes: { + 'eip155:1': { + methods: [], + notifications: [], + accounts: ['eip155:1:0x1234567890123456789012345678901234567890'], + }, + }, + }; + mockCore.emit('wallet_sessionChanged', session); + + const connectData = await connectPromise; + expect(connectData.chainId).toBe('0x1'); + expect(connectData.accounts).toContain( + '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd', + ); + expect(mockCore.transport.sendEip1193Message).toHaveBeenCalledWith({ + method: 'eth_accounts', + params: [], + }); + }); + + it('connects using the cached eth_chainId when valid and also in the CAIP-25 permission scopes', async () => { + const mockCore = createMockCore(); + mockCore.storage.adapter.get.mockResolvedValue(JSON.stringify('0x89')); // Polygon + const client = await MetamaskConnectEVM.create({ core: mockCore }); + + const connectPromise = new Promise<{ chainId: string }>((resolve) => { + client.getProvider().once('connect', (data) => resolve(data)); + }); + + const session: SessionData = { + sessionScopes: { + 'eip155:1': { + methods: [], + notifications: [], + accounts: ['eip155:1:0x1234567890123456789012345678901234567890'], + }, + 'eip155:137': { + methods: [], + notifications: [], + accounts: ['eip155:137:0x1234567890123456789012345678901234567890'], + }, + }, + }; + mockCore.emit('wallet_sessionChanged', session); + + const connectData = await connectPromise; + expect(connectData.chainId).toBe('0x89'); + }); + + it('connects using the first permitted chain id from the CAIP-25 permission when there is no cached eth_chainId', async () => { + const mockCore = createMockCore(); + mockCore.storage.adapter.get.mockResolvedValue(null); + const client = await MetamaskConnectEVM.create({ core: mockCore }); + + const connectPromise = new Promise<{ chainId: string }>((resolve) => { + client.getProvider().once('connect', (data) => resolve(data)); + }); + + const session: SessionData = { + sessionScopes: { + 'eip155:11155111': { + methods: [], + notifications: [], + accounts: [ + 'eip155:11155111:0x1234567890123456789012345678901234567890', + ], + }, + }, + }; + mockCore.emit('wallet_sessionChanged', session); + + const connectData = await connectPromise; + expect(connectData.chainId).toBe('0xaa36a7'); // sepolia + }); }); }); From e5d73bebae375e00d6a33f0c1d7b8dbeadaa5342 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 17 Feb 2026 15:00:21 -0800 Subject: [PATCH 061/103] fix sessionChanged test --- packages/connect-evm/src/connect.test.ts | 332 +++++++++++------------ 1 file changed, 160 insertions(+), 172 deletions(-) diff --git a/packages/connect-evm/src/connect.test.ts b/packages/connect-evm/src/connect.test.ts index b874af48..906cf3c8 100644 --- a/packages/connect-evm/src/connect.test.ts +++ b/packages/connect-evm/src/connect.test.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-shadow -- Vitest globals */ import type { SessionData } from '@metamask/connect-multichain'; -import { describe, it, expect, vi, type Mock } from 'vitest'; +import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'; import { MetamaskConnectEVM } from './connect'; import type { MultichainCore } from '@metamask/connect-multichain'; @@ -31,7 +31,7 @@ function createMockCore(): MockCore { id: 1, jsonrpc: '2.0' as const, }); - const onNotification = vi.fn().mockReturnValue(() => {}); + const onNotification = vi.fn().mockReturnValue(() => { }); const storageGet = vi.fn().mockResolvedValue(null); const storageSet = vi.fn().mockResolvedValue(undefined); @@ -72,206 +72,194 @@ function createMockCore(): MockCore { describe('MetamaskConnectEVM', () => { describe('#onSessionChanged', () => { - it('disconnects when session has no permitted EIP-155 chain IDs if the MultichainClient is connected', async () => { - const mockCore = createMockCore(); - mockCore._status = 'connecting' - mockCore.storage.adapter.get.mockResolvedValue(JSON.stringify('0x1')); - const client = await MetamaskConnectEVM.create({ core: mockCore }); - const session: SessionData = { - sessionScopes: { - 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': { - methods: [], - notifications: [], - accounts: ['solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:1234567890'], + describe('disconnects', () => { + let mockCore: MockCore; + let client: Awaited>; + + beforeEach(async () => { + mockCore = createMockCore(); + mockCore.storage.adapter.get.mockResolvedValue(JSON.stringify('0x1')); + client = await MetamaskConnectEVM.create({ core: mockCore }); + const session: SessionData = { + sessionScopes: { + 'eip155:1': { + methods: [], + notifications: [], + accounts: ['eip155:1:0x1234567890123456789012345678901234567890'], + }, }, - }, - }; - mockCore.emit('wallet_sessionChanged', session); - await new Promise((resolve) => { - client.getProvider().once('connect', () => resolve()); - }); - const disconnectPromise = new Promise((resolve) => { - client.getProvider().once('disconnect', resolve); + }; + mockCore.emit('wallet_sessionChanged', session); + await new Promise((resolve) => { + client.getProvider().once('connect', () => resolve()); + }); }); - mockCore.emit('wallet_sessionChanged', { sessionScopes: {} }); - await disconnectPromise; - expect(client.accounts).toEqual([]); - }); - it('disconnects when wallet_sessionChanged is emitted with undefined session after being connected', async () => { - const mockCore = createMockCore(); - mockCore.storage.adapter.get.mockResolvedValue(JSON.stringify('0x1')); - const client = await MetamaskConnectEVM.create({ core: mockCore }); - const session: SessionData = { - sessionScopes: { - 'eip155:1': { - methods: [], - notifications: [], - accounts: ['eip155:1:0x1234567890123456789012345678901234567890'], + it('disconnects when session has no permitted EIP-155 chain IDs if the MultichainClient is connected', async () => { + const disconnectPromise = new Promise((resolve) => { + client.getProvider().once('disconnect', resolve); + }); + + const newSession: SessionData = { + sessionScopes: { + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': { + methods: [], + notifications: [], + accounts: ['solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:1234567890'], + }, }, - }, - }; - mockCore.emit('wallet_sessionChanged', session); - await new Promise((resolve) => { - client.getProvider().once('connect', () => resolve()); - }); - const disconnectPromise = new Promise((resolve) => { - client.getProvider().once('disconnect', resolve); + }; + + mockCore.emit('wallet_sessionChanged', newSession); + await disconnectPromise; + expect(client.accounts).toEqual([]); }); - mockCore.emit('wallet_sessionChanged', undefined); - await disconnectPromise; - expect(client.accounts).toEqual([]); - }); - it('disconnects when wallet_sessionChanged is emitted with empty sessionScopes after being connected', async () => { - const mockCore = createMockCore(); - mockCore.storage.adapter.get.mockResolvedValue(JSON.stringify('0x1')); - const client = await MetamaskConnectEVM.create({ core: mockCore }); - const session: SessionData = { - sessionScopes: { - 'eip155:1': { - methods: [], - notifications: [], - accounts: ['eip155:1:0x1234567890123456789012345678901234567890'], - }, - }, - }; - mockCore.emit('wallet_sessionChanged', session); - await new Promise((resolve) => { - client.getProvider().once('connect', () => resolve()); + it('disconnects when wallet_sessionChanged is emitted with undefined session after being connected', async () => { + const disconnectPromise = new Promise((resolve) => { + client.getProvider().once('disconnect', resolve); + }); + mockCore.emit('wallet_sessionChanged', undefined); + await disconnectPromise; + expect(client.accounts).toEqual([]); }); - const disconnectPromise = new Promise((resolve) => { - client.getProvider().once('disconnect', resolve); + + it('disconnects when wallet_sessionChanged is emitted with empty sessionScopes after being connected', async () => { + const disconnectPromise = new Promise((resolve) => { + client.getProvider().once('disconnect', resolve); + }); + mockCore.emit('wallet_sessionChanged', { sessionScopes: {} }); + await disconnectPromise; + expect(client.accounts).toEqual([]); }); - mockCore.emit('wallet_sessionChanged', { sessionScopes: {} }); - await disconnectPromise; - expect(client.accounts).toEqual([]); }); - it('connects using the accounts from the CAIP-25 permissions when the MultichainClient is disconnected', async () => { - const mockCore = createMockCore(); - mockCore.storage.adapter.get.mockResolvedValue(JSON.stringify('0x1')); - const client = await MetamaskConnectEVM.create({ core: mockCore }); + describe('connects', () => { + it('connects using the accounts from the CAIP-25 permissions when the MultichainClient is disconnected', async () => { + const mockCore = createMockCore(); + mockCore.storage.adapter.get.mockResolvedValue(JSON.stringify('0x1')); + const client = await MetamaskConnectEVM.create({ core: mockCore }); - const connectPromise = new Promise<{ - chainId: string; - accounts: string[]; - }>((resolve) => { - client.getProvider().once('connect', resolve); - }); + const connectPromise = new Promise<{ + chainId: string; + accounts: string[]; + }>((resolve) => { + client.getProvider().once('connect', resolve); + }); - const session: SessionData = { - sessionScopes: { - 'eip155:1': { - methods: [], - notifications: [], - accounts: ['eip155:1:0x1234567890123456789012345678901234567890'], + const session: SessionData = { + sessionScopes: { + 'eip155:1': { + methods: [], + notifications: [], + accounts: ['eip155:1:0x1234567890123456789012345678901234567890'], + }, }, - }, - }; - mockCore.emit('wallet_sessionChanged', session); - - const connectData = await connectPromise; - expect(connectData.chainId).toBe('0x1'); - expect(connectData.accounts).toContain( - '0x1234567890123456789012345678901234567890', - ); - }); + }; + mockCore.emit('wallet_sessionChanged', session); - it('connects using accounts from a eth_accounts response when the MultichainClient is connected', async () => { - const mockCore = createMockCore(); - mockCore._status = 'connected'; - mockCore.storage.adapter.get.mockResolvedValue(JSON.stringify('0x1')); - mockCore.transport.sendEip1193Message.mockResolvedValue({ - result: ['0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'], - id: 1, - jsonrpc: '2.0', + const connectData = await connectPromise; + expect(connectData.chainId).toBe('0x1'); + expect(connectData.accounts).toContain( + '0x1234567890123456789012345678901234567890', + ); }); - const client = await MetamaskConnectEVM.create({ core: mockCore }); + it('connects using accounts from a eth_accounts response when the MultichainClient is connected', async () => { + const mockCore = createMockCore(); + mockCore._status = 'connected'; + mockCore.storage.adapter.get.mockResolvedValue(JSON.stringify('0x1')); + mockCore.transport.sendEip1193Message.mockResolvedValue({ + result: ['0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'], + id: 1, + jsonrpc: '2.0', + }); - const connectPromise = new Promise<{ - chainId: string; - accounts: string[]; - }>((resolve) => { - client.getProvider().once('connect', resolve); - }); + const client = await MetamaskConnectEVM.create({ core: mockCore }); + + const connectPromise = new Promise<{ + chainId: string; + accounts: string[]; + }>((resolve) => { + client.getProvider().once('connect', resolve); + }); - const session: SessionData = { - sessionScopes: { - 'eip155:1': { - methods: [], - notifications: [], - accounts: ['eip155:1:0x1234567890123456789012345678901234567890'], + const session: SessionData = { + sessionScopes: { + 'eip155:1': { + methods: [], + notifications: [], + accounts: ['eip155:1:0x1234567890123456789012345678901234567890'], + }, }, - }, - }; - mockCore.emit('wallet_sessionChanged', session); - - const connectData = await connectPromise; - expect(connectData.chainId).toBe('0x1'); - expect(connectData.accounts).toContain( - '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd', - ); - expect(mockCore.transport.sendEip1193Message).toHaveBeenCalledWith({ - method: 'eth_accounts', - params: [], + }; + mockCore.emit('wallet_sessionChanged', session); + + const connectData = await connectPromise; + expect(connectData.chainId).toBe('0x1'); + expect(connectData.accounts).toContain( + '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd', + ); + expect(mockCore.transport.sendEip1193Message).toHaveBeenCalledWith({ + method: 'eth_accounts', + params: [], + }); }); - }); - it('connects using the cached eth_chainId when valid and also in the CAIP-25 permission scopes', async () => { - const mockCore = createMockCore(); - mockCore.storage.adapter.get.mockResolvedValue(JSON.stringify('0x89')); // Polygon - const client = await MetamaskConnectEVM.create({ core: mockCore }); + it('connects using the cached eth_chainId when valid and also in the CAIP-25 permission scopes', async () => { + const mockCore = createMockCore(); + mockCore.storage.adapter.get.mockResolvedValue(JSON.stringify('0x89')); // Polygon + const client = await MetamaskConnectEVM.create({ core: mockCore }); - const connectPromise = new Promise<{ chainId: string }>((resolve) => { - client.getProvider().once('connect', (data) => resolve(data)); - }); + const connectPromise = new Promise<{ chainId: string }>((resolve) => { + client.getProvider().once('connect', (data) => resolve(data)); + }); - const session: SessionData = { - sessionScopes: { - 'eip155:1': { - methods: [], - notifications: [], - accounts: ['eip155:1:0x1234567890123456789012345678901234567890'], + const session: SessionData = { + sessionScopes: { + 'eip155:1': { + methods: [], + notifications: [], + accounts: ['eip155:1:0x1234567890123456789012345678901234567890'], + }, + 'eip155:137': { + methods: [], + notifications: [], + accounts: ['eip155:137:0x1234567890123456789012345678901234567890'], + }, }, - 'eip155:137': { - methods: [], - notifications: [], - accounts: ['eip155:137:0x1234567890123456789012345678901234567890'], - }, - }, - }; - mockCore.emit('wallet_sessionChanged', session); + }; + mockCore.emit('wallet_sessionChanged', session); - const connectData = await connectPromise; - expect(connectData.chainId).toBe('0x89'); - }); + const connectData = await connectPromise; + expect(connectData.chainId).toBe('0x89'); + }); - it('connects using the first permitted chain id from the CAIP-25 permission when there is no cached eth_chainId', async () => { - const mockCore = createMockCore(); - mockCore.storage.adapter.get.mockResolvedValue(null); - const client = await MetamaskConnectEVM.create({ core: mockCore }); + it('connects using the first permitted chain id from the CAIP-25 permission when there is no cached eth_chainId', async () => { + const mockCore = createMockCore(); + mockCore.storage.adapter.get.mockResolvedValue(null); + const client = await MetamaskConnectEVM.create({ core: mockCore }); - const connectPromise = new Promise<{ chainId: string }>((resolve) => { - client.getProvider().once('connect', (data) => resolve(data)); - }); + const connectPromise = new Promise<{ chainId: string }>((resolve) => { + client.getProvider().once('connect', (data) => resolve(data)); + }); - const session: SessionData = { - sessionScopes: { - 'eip155:11155111': { - methods: [], - notifications: [], - accounts: [ - 'eip155:11155111:0x1234567890123456789012345678901234567890', - ], + const session: SessionData = { + sessionScopes: { + 'eip155:11155111': { + methods: [], + notifications: [], + accounts: [ + 'eip155:11155111:0x1234567890123456789012345678901234567890', + ], + }, }, - }, - }; - mockCore.emit('wallet_sessionChanged', session); + }; + mockCore.emit('wallet_sessionChanged', session); - const connectData = await connectPromise; - expect(connectData.chainId).toBe('0xaa36a7'); // sepolia + const connectData = await connectPromise; + expect(connectData.chainId).toBe('0xaa36a7'); // sepolia + }); }); }); }); From ec18eb4528b333e0988ce581b7bc31c40ce7f047 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 17 Feb 2026 15:57:01 -0800 Subject: [PATCH 062/103] add EvmClient.disconnect() spec --- packages/connect-evm/src/connect.test.ts | 36 ++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/packages/connect-evm/src/connect.test.ts b/packages/connect-evm/src/connect.test.ts index 906cf3c8..b84c01f1 100644 --- a/packages/connect-evm/src/connect.test.ts +++ b/packages/connect-evm/src/connect.test.ts @@ -20,6 +20,7 @@ type MockCore = MultichainCore & { transport: MultichainCore['transport'] & { sendEip1193Message: Mock; }; + disconnect: Mock<(scopes?: unknown[]) => Promise>; }; function createMockCore(): MockCore { @@ -54,6 +55,7 @@ function createMockCore(): MockCore { emitSessionChanged: vi.fn().mockImplementation(async (): Promise => { mockCore.emit('wallet_sessionChanged', { sessionScopes: {} }); }), + disconnect: vi.fn().mockResolvedValue(undefined), transport: { sendEip1193Message, onNotification, @@ -262,4 +264,38 @@ describe('MetamaskConnectEVM', () => { }); }); }); + + describe('disconnect', () => { + it('calls core.disconnect with all eip155 scopes from the current session', async () => { + const mockCore = createMockCore(); + mockCore.storage.adapter.get.mockResolvedValue(JSON.stringify('0x1')); + const client = await MetamaskConnectEVM.create({ core: mockCore }); + + const session: SessionData = { + sessionScopes: { + 'eip155:1': { + methods: [], + notifications: [], + accounts: ['eip155:1:0x1234567890123456789012345678901234567890'], + }, + 'eip155:137': { + methods: [], + notifications: [], + accounts: ['eip155:137:0x1234567890123456789012345678901234567890'], + }, + }, + }; + mockCore.emit('wallet_sessionChanged', session); + await new Promise((resolve) => { + client.getProvider().once('connect', () => resolve()); + }); + + await client.disconnect(); + + expect(mockCore.disconnect).toHaveBeenCalledTimes(1); + const [scopes] = mockCore.disconnect.mock.calls[0]; + expect(scopes).toEqual(expect.arrayContaining(['eip155:1', 'eip155:137'])); + expect(scopes).toHaveLength(2); + }); + }); }); From 0b0ae780d9cc6ea20177767b734f5dbeaa781934 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 17 Feb 2026 16:07:59 -0800 Subject: [PATCH 063/103] add connect() test --- packages/connect-evm/src/connect.test.ts | 36 ++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/packages/connect-evm/src/connect.test.ts b/packages/connect-evm/src/connect.test.ts index b84c01f1..95aceffb 100644 --- a/packages/connect-evm/src/connect.test.ts +++ b/packages/connect-evm/src/connect.test.ts @@ -21,6 +21,14 @@ type MockCore = MultichainCore & { sendEip1193Message: Mock; }; disconnect: Mock<(scopes?: unknown[]) => Promise>; + connect: Mock< + ( + scopes: unknown[], + caipAccountIds: unknown[], + sessionProperties?: unknown, + forceRequest?: boolean, + ) => Promise + >; }; function createMockCore(): MockCore { @@ -56,6 +64,7 @@ function createMockCore(): MockCore { mockCore.emit('wallet_sessionChanged', { sessionScopes: {} }); }), disconnect: vi.fn().mockResolvedValue(undefined), + connect: vi.fn().mockResolvedValue(undefined), transport: { sendEip1193Message, onNotification, @@ -265,6 +274,33 @@ describe('MetamaskConnectEVM', () => { }); }); + describe('connect', () => { + it('resolves with the value emitted by the provider connect event (triggered by wallet_sessionChanged)', async () => { + const mockCore = createMockCore(); + mockCore.storage.adapter.get.mockResolvedValue(JSON.stringify('0x1')); + mockCore.connect.mockImplementation(async (): Promise => { + const session: SessionData = { + sessionScopes: { + 'eip155:1': { + methods: [], + notifications: [], + accounts: ['eip155:1:0x1234567890123456789012345678901234567890'], + }, + }, + }; + mockCore.emit('wallet_sessionChanged', session); + }); + const client = await MetamaskConnectEVM.create({ core: mockCore }); + + const result = await client.connect({ chainIds: ['0x1'] }); + + expect(result).toEqual({ + chainId: '0x1', + accounts: ['0x1234567890123456789012345678901234567890'], + }); + }); + }); + describe('disconnect', () => { it('calls core.disconnect with all eip155 scopes from the current session', async () => { const mockCore = createMockCore(); From 8fd54f6e28694d4e2a657b29bd5df995d3e2b30b Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 18 Feb 2026 08:59:28 -0800 Subject: [PATCH 064/103] lint --- packages/connect-evm/src/connect.test.ts | 33 +++++++++++++++++------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/packages/connect-evm/src/connect.test.ts b/packages/connect-evm/src/connect.test.ts index 95aceffb..54b36a39 100644 --- a/packages/connect-evm/src/connect.test.ts +++ b/packages/connect-evm/src/connect.test.ts @@ -1,11 +1,15 @@ /* eslint-disable @typescript-eslint/no-shadow -- Vitest globals */ -import type { SessionData } from '@metamask/connect-multichain'; +import type { SessionData, MultichainCore } from '@metamask/connect-multichain'; import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'; import { MetamaskConnectEVM } from './connect'; -import type { MultichainCore } from '@metamask/connect-multichain'; -type Status = 'connected' | 'disconnected' | 'connecting' | 'loaded' | 'pending'; +type Status = + | 'connected' + | 'disconnected' + | 'connecting' + | 'loaded' + | 'pending'; /** Mock core type so storage/transport mocks keep .mockResolvedValue in tests */ type MockCore = MultichainCore & { @@ -31,16 +35,21 @@ type MockCore = MultichainCore & { >; }; +/** + * + */ function createMockCore(): MockCore { const handlers: Record void)[]> = {}; - let _status: Status = 'disconnected'; + const _status: Status = 'disconnected'; const sendEip1193Message = vi.fn().mockResolvedValue({ result: [] as string[], id: 1, jsonrpc: '2.0' as const, }); - const onNotification = vi.fn().mockReturnValue(() => { }); + const onNotification = vi.fn().mockReturnValue(() => { + // noop + }); const storageGet = vi.fn().mockResolvedValue(null); const storageSet = vi.fn().mockResolvedValue(undefined); @@ -54,11 +63,13 @@ function createMockCore(): MockCore { this._status = value; }, on(event: string, handler: (...args: unknown[]) => void): void { - if (!handlers[event]) handlers[event] = []; + if (!handlers[event]) { + handlers[event] = []; + } handlers[event].push(handler); }, emit(event: string, ...args: unknown[]): void { - handlers[event]?.forEach((h) => h(...args)); + handlers[event]?.forEach((handler) => handler(...args)); }, emitSessionChanged: vi.fn().mockImplementation(async (): Promise => { mockCore.emit('wallet_sessionChanged', { sessionScopes: {} }); @@ -236,7 +247,9 @@ describe('MetamaskConnectEVM', () => { 'eip155:137': { methods: [], notifications: [], - accounts: ['eip155:137:0x1234567890123456789012345678901234567890'], + accounts: [ + 'eip155:137:0x1234567890123456789012345678901234567890', + ], }, }, }; @@ -330,7 +343,9 @@ describe('MetamaskConnectEVM', () => { expect(mockCore.disconnect).toHaveBeenCalledTimes(1); const [scopes] = mockCore.disconnect.mock.calls[0]; - expect(scopes).toEqual(expect.arrayContaining(['eip155:1', 'eip155:137'])); + expect(scopes).toEqual( + expect.arrayContaining(['eip155:1', 'eip155:137']), + ); expect(scopes).toHaveLength(2); }); }); From e34d764359cdcbb087189dbcdcecec25da07926d Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 18 Feb 2026 10:18:55 -0800 Subject: [PATCH 065/103] Add openConnectDeeplinkIfNeeded --- packages/connect-evm/src/connect.test.ts | 5 +- packages/connect-evm/src/connect.ts | 2 +- .../src/domain/multichain/index.test.ts | 4 +- .../src/domain/multichain/index.ts | 4 +- .../src/domain/multichain/types.ts | 2 + .../src/multichain/index.ts | 50 +++++++++++++++++-- .../multichain/transports/default/index.ts | 7 +++ .../src/multichain/transports/mwp/index.ts | 32 ++++++++++++ 8 files changed, 98 insertions(+), 8 deletions(-) diff --git a/packages/connect-evm/src/connect.test.ts b/packages/connect-evm/src/connect.test.ts index 54b36a39..b6099426 100644 --- a/packages/connect-evm/src/connect.test.ts +++ b/packages/connect-evm/src/connect.test.ts @@ -36,7 +36,9 @@ type MockCore = MultichainCore & { }; /** + * Creates a mock MultichainCore for testing. * + * @returns A mock core instance implementing MockCore. */ function createMockCore(): MockCore { const handlers: Record void)[]> = {}; @@ -55,8 +57,9 @@ function createMockCore(): MockCore { const storageSet = vi.fn().mockResolvedValue(undefined); const mockCore = { + // eslint-disable-next-line @typescript-eslint/naming-convention -- mock mirrors real class _status _status: _status as Status, - get status() { + get status(): Status { return this._status; }, set status(value: Status) { diff --git a/packages/connect-evm/src/connect.ts b/packages/connect-evm/src/connect.ts index 2d626098..3756685c 100644 --- a/packages/connect-evm/src/connect.ts +++ b/packages/connect-evm/src/connect.ts @@ -701,7 +701,7 @@ export class MetamaskConnectEVM { request.method === 'wallet_addEthereumChain' || request.method === 'wallet_switchEthereumChain' ) { - this.#core.openDeeplinkIfNeeded(); + this.#core.openSimpleDeeplinkIfNeeded(); } return result; } diff --git a/packages/connect-multichain/src/domain/multichain/index.test.ts b/packages/connect-multichain/src/domain/multichain/index.test.ts index 4b3fe7d1..36003bb6 100644 --- a/packages/connect-multichain/src/domain/multichain/index.test.ts +++ b/packages/connect-multichain/src/domain/multichain/index.test.ts @@ -29,7 +29,9 @@ class MockMultichainCore extends MultichainCore { invokeMethod = async (): Promise => Promise.resolve({}); - openDeeplinkIfNeeded = (): void => undefined; + openSimpleDeeplinkIfNeeded = (): void => undefined; + + openConnectDeeplinkIfNeeded = async (): Promise => Promise.resolve(); emitSessionChanged = async (): Promise => Promise.resolve(); diff --git a/packages/connect-multichain/src/domain/multichain/index.ts b/packages/connect-multichain/src/domain/multichain/index.ts index e5066530..ac135071 100644 --- a/packages/connect-multichain/src/domain/multichain/index.ts +++ b/packages/connect-multichain/src/domain/multichain/index.ts @@ -72,7 +72,9 @@ export abstract class MultichainCore extends EventEmitter { */ abstract invokeMethod(options: InvokeMethodOptions): Promise; - abstract openDeeplinkIfNeeded(): void; + abstract openSimpleDeeplinkIfNeeded(): void; + + abstract openConnectDeeplinkIfNeeded(): Promise; abstract emitSessionChanged(): Promise; diff --git a/packages/connect-multichain/src/domain/multichain/types.ts b/packages/connect-multichain/src/domain/multichain/types.ts index 57749ddb..825bc013 100644 --- a/packages/connect-multichain/src/domain/multichain/types.ts +++ b/packages/connect-multichain/src/domain/multichain/types.ts @@ -136,5 +136,7 @@ export type ExtendedTransport = Omit & { getActiveSession: () => Promise; + getStoredSessionRequest: () => Promise; + disconnect: (scopes: Scope[]) => Promise; }; diff --git a/packages/connect-multichain/src/multichain/index.ts b/packages/connect-multichain/src/multichain/index.ts index f0b4c3e3..f18550c3 100644 --- a/packages/connect-multichain/src/multichain/index.ts +++ b/packages/connect-multichain/src/multichain/index.ts @@ -597,7 +597,7 @@ export class MetaMaskConnectMultichain extends MultichainCore { if (this.transport.isConnected()) { timeout = setTimeout(() => { - this.openDeeplinkIfNeeded(); + this.openSimpleDeeplinkIfNeeded(); }, 250); } else { this.dappClient.once( @@ -706,8 +706,14 @@ export class MetaMaskConnectMultichain extends MultichainCore { sessionProperties?: SessionProperties, forceRequest?: boolean, ): Promise { - if (this.status !== 'connected') { - await this.disconnect(); + if ( + this.status === 'connecting' && + this.transportType === TransportType.MWP + ) { + await this.openConnectDeeplinkIfNeeded(); + throw new Error( + 'Existing connection is pending. Please check your MetaMask Mobile app to continue.', + ); } const { ui } = this.options; const platformType = getPlatformType(); @@ -909,7 +915,7 @@ export class MetaMaskConnectMultichain extends MultichainCore { } // DRY THIS WITH REQUEST ROUTER - openDeeplinkIfNeeded(): void { + openSimpleDeeplinkIfNeeded(): void { const { ui, mobile } = this.options; const { showInstallModal = false } = ui ?? {}; const secure = isSecure(); @@ -932,6 +938,42 @@ export class MetaMaskConnectMultichain extends MultichainCore { } } + async openConnectDeeplinkIfNeeded(): Promise { + const { ui } = this.options; + const { showInstallModal = false } = ui ?? {}; + const secure = isSecure(); + const shouldOpenDeeplink = secure && !showInstallModal; + + if (!shouldOpenDeeplink) { + return; + } + + const storedSessionRequest = + await this.#transport?.getStoredSessionRequest(); + if (!storedSessionRequest) { + return; + } + + const connectionRequest = { + sessionRequest: storedSessionRequest, + metadata: { + dapp: this.options.dapp, + sdk: { version: getVersion(), platform: getPlatformType() }, + }, + }; + const deeplink = + this.options.ui.factory.createConnectionDeeplink(connectionRequest); + + const universalLink = + this.options.ui.factory.createConnectionUniversalLink(connectionRequest); + + if (this.options.mobile?.preferredOpenLink) { + this.options.mobile.preferredOpenLink(deeplink, '_self'); + } else { + openDeeplink(this.options, deeplink, universalLink); + } + } + async emitSessionChanged(): Promise { const emptySession = { sessionScopes: {} }; diff --git a/packages/connect-multichain/src/multichain/transports/default/index.ts b/packages/connect-multichain/src/multichain/transports/default/index.ts index 7294ddda..f57dc48b 100644 --- a/packages/connect-multichain/src/multichain/transports/default/index.ts +++ b/packages/connect-multichain/src/multichain/transports/default/index.ts @@ -1,4 +1,5 @@ import type { Session } from '@metamask/mobile-wallet-protocol-core'; +import type { SessionRequest } from '@metamask/mobile-wallet-protocol-dapp-client'; import { type SessionProperties, type CreateSessionParams, @@ -320,4 +321,10 @@ export class DefaultTransport implements ExtendedTransport { 'getActiveSession is purposely not implemented for the DefaultTransport', ); } + + async getStoredSessionRequest(): Promise { + throw new Error( + 'getStoredSessionRequest is purposely not implemented for the DefaultTransport', + ); + } } diff --git a/packages/connect-multichain/src/multichain/transports/mwp/index.ts b/packages/connect-multichain/src/multichain/transports/mwp/index.ts index 9f29dc44..6cb2b4de 100644 --- a/packages/connect-multichain/src/multichain/transports/mwp/index.ts +++ b/packages/connect-multichain/src/multichain/transports/mwp/index.ts @@ -53,6 +53,7 @@ const DEFAULT_RESUME_TIMEOUT = 10 * 1000; const SESSION_STORE_KEY = 'cache_wallet_getSession'; const ACCOUNTS_STORE_KEY = 'cache_eth_accounts'; const CHAIN_STORE_KEY = 'cache_eth_chainId'; +const PENDING_SESSION_REQUEST_KEY = 'pending_session_request'; const CACHED_METHOD_LIST = [ 'wallet_getSession', @@ -115,6 +116,14 @@ export class MWPTransport implements ExtendedTransport { }, ) { this.dappClient.on('message', this.handleMessage.bind(this)); + this.dappClient.on('session_request', (sessionRequest: SessionRequest) => { + this.currentSessionRequest = sessionRequest; + this.kvstore + .set(PENDING_SESSION_REQUEST_KEY, JSON.stringify(sessionRequest)) + .catch((err) => { + logger('Failed to store pending session request', err); + }); + }); if ( typeof window !== 'undefined' && typeof window.addEventListener !== 'undefined' @@ -124,6 +133,27 @@ export class MWPTransport implements ExtendedTransport { } } + private async removeStoredSessionRequest(): Promise { + await this.kvstore.delete(PENDING_SESSION_REQUEST_KEY); + } + + /** + * Returns the stored pending session request from the dappClient session_request event, if any. + * + * @returns The stored SessionRequest, or null if none or invalid. + */ + async getStoredSessionRequest(): Promise { + try { + const raw = await this.kvstore.get(PENDING_SESSION_REQUEST_KEY); + if (!raw) { + return null; + } + return JSON.parse(raw) as SessionRequest; + } catch { + return null; + } + } + private onWindowFocus(): void { if (!this.isConnected()) { this.dappClient.reconnect(); @@ -324,6 +354,7 @@ export class MWPTransport implements ExtendedTransport { } walletSession = response.result as SessionData; } + await this.removeStoredSessionRequest(); this.notifyCallbacks({ method: 'wallet_sessionChanged', params: walletSession, @@ -459,6 +490,7 @@ export class MWPTransport implements ExtendedTransport { request, messagePayload as TransportResponse, ); + await this.removeStoredSessionRequest(); this.notifyCallbacks(messagePayload); return resolveConnection(); }; From 2116ed855afe056957f5e075ad7afd1280fcecc0 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 18 Feb 2026 11:19:00 -0800 Subject: [PATCH 066/103] Add missing removedStoredSessionRequest --- .../connect-multichain/src/multichain/transports/mwp/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/connect-multichain/src/multichain/transports/mwp/index.ts b/packages/connect-multichain/src/multichain/transports/mwp/index.ts index 6cb2b4de..12596bc7 100644 --- a/packages/connect-multichain/src/multichain/transports/mwp/index.ts +++ b/packages/connect-multichain/src/multichain/transports/mwp/index.ts @@ -823,6 +823,7 @@ export class MWPTransport implements ExtendedTransport { const timeoutPromise = new Promise((_resolve, reject) => { setTimeout(() => { unsubscribe(); + this.removeStoredSessionRequest(); reject(new TransportTimeoutError()); }, this.options.resumeTimeout); }); From 72f7a30cb4c0e332085e6d5f4228e3a7dec89f1e Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 18 Feb 2026 12:58:23 -0800 Subject: [PATCH 067/103] make multichain connect button gray when connecting --- playground/browser-playground/src/App.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/playground/browser-playground/src/App.tsx b/playground/browser-playground/src/App.tsx index c97180a2..9cb0395d 100644 --- a/playground/browser-playground/src/App.tsx +++ b/playground/browser-playground/src/App.tsx @@ -166,6 +166,7 @@ function App() { }, [wallets, select, clearSolanaError]); const isConnected = status === 'connected'; + const isConnecting = status === 'connecting'; const isDisconnected = status === 'disconnected' || status === 'pending' || status === 'loaded'; @@ -184,7 +185,6 @@ function App() { return all; }, []); - const isConnecting = status === 'connecting'; return (
Connecting (Multichain) From 0203c52e4c09636d7f5e6c6f3c265ceee6a9e9ec Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 18 Feb 2026 12:58:39 -0800 Subject: [PATCH 068/103] Make SDKProvider start in connecting state --- playground/browser-playground/src/sdk/SDKProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/browser-playground/src/sdk/SDKProvider.tsx b/playground/browser-playground/src/sdk/SDKProvider.tsx index 88329cb7..f702d27f 100644 --- a/playground/browser-playground/src/sdk/SDKProvider.tsx +++ b/playground/browser-playground/src/sdk/SDKProvider.tsx @@ -37,7 +37,7 @@ const SDKContext = createContext< >(undefined); export const SDKProvider = ({ children }: { children: React.ReactNode }) => { - const [status, setStatus] = useState('pending'); + const [status, setStatus] = useState('connecting'); const [session, setSession] = useState(undefined); const [error, setError] = useState(null); From ec72dde170fd58ac95f7c7b982c7ba7713faf9a9 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 18 Feb 2026 13:03:31 -0800 Subject: [PATCH 069/103] Fix resumeTimeout on refresh --- .../connect-multichain/src/multichain/transports/mwp/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/connect-multichain/src/multichain/transports/mwp/index.ts b/packages/connect-multichain/src/multichain/transports/mwp/index.ts index 12596bc7..a6303ca9 100644 --- a/packages/connect-multichain/src/multichain/transports/mwp/index.ts +++ b/packages/connect-multichain/src/multichain/transports/mwp/index.ts @@ -518,9 +518,11 @@ export class MWPTransport implements ExtendedTransport { ); } + const storedSessionRequest = await this.getStoredSessionRequest(); + timeout = setTimeout(() => { reject(new TransportTimeoutError()); - }, this.options.connectionTimeout); + }, storedSessionRequest ? this.options.resumeTimeout : this.options.connectionTimeout); connection.then(resolve).catch(reject); }); From 5b9df60219062fff815563e93103b26e4b9ac135 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 18 Feb 2026 13:11:21 -0800 Subject: [PATCH 070/103] clear storedSessionRequest after connection handled --- .../connect-multichain/src/multichain/transports/mwp/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/connect-multichain/src/multichain/transports/mwp/index.ts b/packages/connect-multichain/src/multichain/transports/mwp/index.ts index a6303ca9..f227a0f3 100644 --- a/packages/connect-multichain/src/multichain/transports/mwp/index.ts +++ b/packages/connect-multichain/src/multichain/transports/mwp/index.ts @@ -539,6 +539,7 @@ export class MWPTransport implements ExtendedTransport { this.dappClient.off('message', initialConnectionMessageHandler); initialConnectionMessageHandler = undefined; } + this.removeStoredSessionRequest(); }); } From 22ec40d419be7bd8c30f3f596458742abc68f25e Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 18 Feb 2026 13:14:50 -0800 Subject: [PATCH 071/103] remove refresh timeout fix --- .../connect-multichain/src/multichain/transports/mwp/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/connect-multichain/src/multichain/transports/mwp/index.ts b/packages/connect-multichain/src/multichain/transports/mwp/index.ts index f227a0f3..309154ff 100644 --- a/packages/connect-multichain/src/multichain/transports/mwp/index.ts +++ b/packages/connect-multichain/src/multichain/transports/mwp/index.ts @@ -518,11 +518,9 @@ export class MWPTransport implements ExtendedTransport { ); } - const storedSessionRequest = await this.getStoredSessionRequest(); - timeout = setTimeout(() => { reject(new TransportTimeoutError()); - }, storedSessionRequest ? this.options.resumeTimeout : this.options.connectionTimeout); + }, this.options.connectionTimeout); connection.then(resolve).catch(reject); }); From 69375df84b9d76a1ca1cac0d2ee43a971cfeba19 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 18 Feb 2026 13:55:00 -0800 Subject: [PATCH 072/103] Revert "Add missing removedStoredSessionRequest" This reverts commit 2116ed855afe056957f5e075ad7afd1280fcecc0. --- .../connect-multichain/src/multichain/transports/mwp/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/connect-multichain/src/multichain/transports/mwp/index.ts b/packages/connect-multichain/src/multichain/transports/mwp/index.ts index 309154ff..16e48888 100644 --- a/packages/connect-multichain/src/multichain/transports/mwp/index.ts +++ b/packages/connect-multichain/src/multichain/transports/mwp/index.ts @@ -824,7 +824,6 @@ export class MWPTransport implements ExtendedTransport { const timeoutPromise = new Promise((_resolve, reject) => { setTimeout(() => { unsubscribe(); - this.removeStoredSessionRequest(); reject(new TransportTimeoutError()); }, this.options.resumeTimeout); }); From 1ae0d2cc8c385b896b914b9ec2ff557565473e7a Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 18 Feb 2026 13:55:09 -0800 Subject: [PATCH 073/103] Revert "clear storedSessionRequest after connection handled" This reverts commit 5b9df60219062fff815563e93103b26e4b9ac135. --- .../connect-multichain/src/multichain/transports/mwp/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/connect-multichain/src/multichain/transports/mwp/index.ts b/packages/connect-multichain/src/multichain/transports/mwp/index.ts index 16e48888..6cb2b4de 100644 --- a/packages/connect-multichain/src/multichain/transports/mwp/index.ts +++ b/packages/connect-multichain/src/multichain/transports/mwp/index.ts @@ -537,7 +537,6 @@ export class MWPTransport implements ExtendedTransport { this.dappClient.off('message', initialConnectionMessageHandler); initialConnectionMessageHandler = undefined; } - this.removeStoredSessionRequest(); }); } From 32d0e75ffc15d6191d011e191bd046ee0827809d Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 18 Feb 2026 13:55:16 -0800 Subject: [PATCH 074/103] Revert "Add openConnectDeeplinkIfNeeded" This reverts commit e34d764359cdcbb087189dbcdcecec25da07926d. --- packages/connect-evm/src/connect.test.ts | 5 +- packages/connect-evm/src/connect.ts | 2 +- .../src/domain/multichain/index.test.ts | 4 +- .../src/domain/multichain/index.ts | 4 +- .../src/domain/multichain/types.ts | 2 - .../src/multichain/index.ts | 50 ++----------------- .../multichain/transports/default/index.ts | 7 --- .../src/multichain/transports/mwp/index.ts | 32 ------------ 8 files changed, 8 insertions(+), 98 deletions(-) diff --git a/packages/connect-evm/src/connect.test.ts b/packages/connect-evm/src/connect.test.ts index b6099426..54b36a39 100644 --- a/packages/connect-evm/src/connect.test.ts +++ b/packages/connect-evm/src/connect.test.ts @@ -36,9 +36,7 @@ type MockCore = MultichainCore & { }; /** - * Creates a mock MultichainCore for testing. * - * @returns A mock core instance implementing MockCore. */ function createMockCore(): MockCore { const handlers: Record void)[]> = {}; @@ -57,9 +55,8 @@ function createMockCore(): MockCore { const storageSet = vi.fn().mockResolvedValue(undefined); const mockCore = { - // eslint-disable-next-line @typescript-eslint/naming-convention -- mock mirrors real class _status _status: _status as Status, - get status(): Status { + get status() { return this._status; }, set status(value: Status) { diff --git a/packages/connect-evm/src/connect.ts b/packages/connect-evm/src/connect.ts index 3756685c..2d626098 100644 --- a/packages/connect-evm/src/connect.ts +++ b/packages/connect-evm/src/connect.ts @@ -701,7 +701,7 @@ export class MetamaskConnectEVM { request.method === 'wallet_addEthereumChain' || request.method === 'wallet_switchEthereumChain' ) { - this.#core.openSimpleDeeplinkIfNeeded(); + this.#core.openDeeplinkIfNeeded(); } return result; } diff --git a/packages/connect-multichain/src/domain/multichain/index.test.ts b/packages/connect-multichain/src/domain/multichain/index.test.ts index 36003bb6..4b3fe7d1 100644 --- a/packages/connect-multichain/src/domain/multichain/index.test.ts +++ b/packages/connect-multichain/src/domain/multichain/index.test.ts @@ -29,9 +29,7 @@ class MockMultichainCore extends MultichainCore { invokeMethod = async (): Promise => Promise.resolve({}); - openSimpleDeeplinkIfNeeded = (): void => undefined; - - openConnectDeeplinkIfNeeded = async (): Promise => Promise.resolve(); + openDeeplinkIfNeeded = (): void => undefined; emitSessionChanged = async (): Promise => Promise.resolve(); diff --git a/packages/connect-multichain/src/domain/multichain/index.ts b/packages/connect-multichain/src/domain/multichain/index.ts index ac135071..e5066530 100644 --- a/packages/connect-multichain/src/domain/multichain/index.ts +++ b/packages/connect-multichain/src/domain/multichain/index.ts @@ -72,9 +72,7 @@ export abstract class MultichainCore extends EventEmitter { */ abstract invokeMethod(options: InvokeMethodOptions): Promise; - abstract openSimpleDeeplinkIfNeeded(): void; - - abstract openConnectDeeplinkIfNeeded(): Promise; + abstract openDeeplinkIfNeeded(): void; abstract emitSessionChanged(): Promise; diff --git a/packages/connect-multichain/src/domain/multichain/types.ts b/packages/connect-multichain/src/domain/multichain/types.ts index 825bc013..57749ddb 100644 --- a/packages/connect-multichain/src/domain/multichain/types.ts +++ b/packages/connect-multichain/src/domain/multichain/types.ts @@ -136,7 +136,5 @@ export type ExtendedTransport = Omit & { getActiveSession: () => Promise; - getStoredSessionRequest: () => Promise; - disconnect: (scopes: Scope[]) => Promise; }; diff --git a/packages/connect-multichain/src/multichain/index.ts b/packages/connect-multichain/src/multichain/index.ts index f18550c3..f0b4c3e3 100644 --- a/packages/connect-multichain/src/multichain/index.ts +++ b/packages/connect-multichain/src/multichain/index.ts @@ -597,7 +597,7 @@ export class MetaMaskConnectMultichain extends MultichainCore { if (this.transport.isConnected()) { timeout = setTimeout(() => { - this.openSimpleDeeplinkIfNeeded(); + this.openDeeplinkIfNeeded(); }, 250); } else { this.dappClient.once( @@ -706,14 +706,8 @@ export class MetaMaskConnectMultichain extends MultichainCore { sessionProperties?: SessionProperties, forceRequest?: boolean, ): Promise { - if ( - this.status === 'connecting' && - this.transportType === TransportType.MWP - ) { - await this.openConnectDeeplinkIfNeeded(); - throw new Error( - 'Existing connection is pending. Please check your MetaMask Mobile app to continue.', - ); + if (this.status !== 'connected') { + await this.disconnect(); } const { ui } = this.options; const platformType = getPlatformType(); @@ -915,7 +909,7 @@ export class MetaMaskConnectMultichain extends MultichainCore { } // DRY THIS WITH REQUEST ROUTER - openSimpleDeeplinkIfNeeded(): void { + openDeeplinkIfNeeded(): void { const { ui, mobile } = this.options; const { showInstallModal = false } = ui ?? {}; const secure = isSecure(); @@ -938,42 +932,6 @@ export class MetaMaskConnectMultichain extends MultichainCore { } } - async openConnectDeeplinkIfNeeded(): Promise { - const { ui } = this.options; - const { showInstallModal = false } = ui ?? {}; - const secure = isSecure(); - const shouldOpenDeeplink = secure && !showInstallModal; - - if (!shouldOpenDeeplink) { - return; - } - - const storedSessionRequest = - await this.#transport?.getStoredSessionRequest(); - if (!storedSessionRequest) { - return; - } - - const connectionRequest = { - sessionRequest: storedSessionRequest, - metadata: { - dapp: this.options.dapp, - sdk: { version: getVersion(), platform: getPlatformType() }, - }, - }; - const deeplink = - this.options.ui.factory.createConnectionDeeplink(connectionRequest); - - const universalLink = - this.options.ui.factory.createConnectionUniversalLink(connectionRequest); - - if (this.options.mobile?.preferredOpenLink) { - this.options.mobile.preferredOpenLink(deeplink, '_self'); - } else { - openDeeplink(this.options, deeplink, universalLink); - } - } - async emitSessionChanged(): Promise { const emptySession = { sessionScopes: {} }; diff --git a/packages/connect-multichain/src/multichain/transports/default/index.ts b/packages/connect-multichain/src/multichain/transports/default/index.ts index f57dc48b..7294ddda 100644 --- a/packages/connect-multichain/src/multichain/transports/default/index.ts +++ b/packages/connect-multichain/src/multichain/transports/default/index.ts @@ -1,5 +1,4 @@ import type { Session } from '@metamask/mobile-wallet-protocol-core'; -import type { SessionRequest } from '@metamask/mobile-wallet-protocol-dapp-client'; import { type SessionProperties, type CreateSessionParams, @@ -321,10 +320,4 @@ export class DefaultTransport implements ExtendedTransport { 'getActiveSession is purposely not implemented for the DefaultTransport', ); } - - async getStoredSessionRequest(): Promise { - throw new Error( - 'getStoredSessionRequest is purposely not implemented for the DefaultTransport', - ); - } } diff --git a/packages/connect-multichain/src/multichain/transports/mwp/index.ts b/packages/connect-multichain/src/multichain/transports/mwp/index.ts index 6cb2b4de..9f29dc44 100644 --- a/packages/connect-multichain/src/multichain/transports/mwp/index.ts +++ b/packages/connect-multichain/src/multichain/transports/mwp/index.ts @@ -53,7 +53,6 @@ const DEFAULT_RESUME_TIMEOUT = 10 * 1000; const SESSION_STORE_KEY = 'cache_wallet_getSession'; const ACCOUNTS_STORE_KEY = 'cache_eth_accounts'; const CHAIN_STORE_KEY = 'cache_eth_chainId'; -const PENDING_SESSION_REQUEST_KEY = 'pending_session_request'; const CACHED_METHOD_LIST = [ 'wallet_getSession', @@ -116,14 +115,6 @@ export class MWPTransport implements ExtendedTransport { }, ) { this.dappClient.on('message', this.handleMessage.bind(this)); - this.dappClient.on('session_request', (sessionRequest: SessionRequest) => { - this.currentSessionRequest = sessionRequest; - this.kvstore - .set(PENDING_SESSION_REQUEST_KEY, JSON.stringify(sessionRequest)) - .catch((err) => { - logger('Failed to store pending session request', err); - }); - }); if ( typeof window !== 'undefined' && typeof window.addEventListener !== 'undefined' @@ -133,27 +124,6 @@ export class MWPTransport implements ExtendedTransport { } } - private async removeStoredSessionRequest(): Promise { - await this.kvstore.delete(PENDING_SESSION_REQUEST_KEY); - } - - /** - * Returns the stored pending session request from the dappClient session_request event, if any. - * - * @returns The stored SessionRequest, or null if none or invalid. - */ - async getStoredSessionRequest(): Promise { - try { - const raw = await this.kvstore.get(PENDING_SESSION_REQUEST_KEY); - if (!raw) { - return null; - } - return JSON.parse(raw) as SessionRequest; - } catch { - return null; - } - } - private onWindowFocus(): void { if (!this.isConnected()) { this.dappClient.reconnect(); @@ -354,7 +324,6 @@ export class MWPTransport implements ExtendedTransport { } walletSession = response.result as SessionData; } - await this.removeStoredSessionRequest(); this.notifyCallbacks({ method: 'wallet_sessionChanged', params: walletSession, @@ -490,7 +459,6 @@ export class MWPTransport implements ExtendedTransport { request, messagePayload as TransportResponse, ); - await this.removeStoredSessionRequest(); this.notifyCallbacks(messagePayload); return resolveConnection(); }; From 03e00681e27ca877b6cac6aa11492df3e5a602f0 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 18 Feb 2026 13:55:59 -0800 Subject: [PATCH 075/103] Reapply "Add openConnectDeeplinkIfNeeded" This reverts commit 32d0e75ffc15d6191d011e191bd046ee0827809d. --- packages/connect-evm/src/connect.test.ts | 5 +- packages/connect-evm/src/connect.ts | 2 +- .../src/domain/multichain/index.test.ts | 4 +- .../src/domain/multichain/index.ts | 4 +- .../src/domain/multichain/types.ts | 2 + .../src/multichain/index.ts | 50 +++++++++++++++++-- .../multichain/transports/default/index.ts | 7 +++ .../src/multichain/transports/mwp/index.ts | 32 ++++++++++++ 8 files changed, 98 insertions(+), 8 deletions(-) diff --git a/packages/connect-evm/src/connect.test.ts b/packages/connect-evm/src/connect.test.ts index 54b36a39..b6099426 100644 --- a/packages/connect-evm/src/connect.test.ts +++ b/packages/connect-evm/src/connect.test.ts @@ -36,7 +36,9 @@ type MockCore = MultichainCore & { }; /** + * Creates a mock MultichainCore for testing. * + * @returns A mock core instance implementing MockCore. */ function createMockCore(): MockCore { const handlers: Record void)[]> = {}; @@ -55,8 +57,9 @@ function createMockCore(): MockCore { const storageSet = vi.fn().mockResolvedValue(undefined); const mockCore = { + // eslint-disable-next-line @typescript-eslint/naming-convention -- mock mirrors real class _status _status: _status as Status, - get status() { + get status(): Status { return this._status; }, set status(value: Status) { diff --git a/packages/connect-evm/src/connect.ts b/packages/connect-evm/src/connect.ts index 2d626098..3756685c 100644 --- a/packages/connect-evm/src/connect.ts +++ b/packages/connect-evm/src/connect.ts @@ -701,7 +701,7 @@ export class MetamaskConnectEVM { request.method === 'wallet_addEthereumChain' || request.method === 'wallet_switchEthereumChain' ) { - this.#core.openDeeplinkIfNeeded(); + this.#core.openSimpleDeeplinkIfNeeded(); } return result; } diff --git a/packages/connect-multichain/src/domain/multichain/index.test.ts b/packages/connect-multichain/src/domain/multichain/index.test.ts index 4b3fe7d1..36003bb6 100644 --- a/packages/connect-multichain/src/domain/multichain/index.test.ts +++ b/packages/connect-multichain/src/domain/multichain/index.test.ts @@ -29,7 +29,9 @@ class MockMultichainCore extends MultichainCore { invokeMethod = async (): Promise => Promise.resolve({}); - openDeeplinkIfNeeded = (): void => undefined; + openSimpleDeeplinkIfNeeded = (): void => undefined; + + openConnectDeeplinkIfNeeded = async (): Promise => Promise.resolve(); emitSessionChanged = async (): Promise => Promise.resolve(); diff --git a/packages/connect-multichain/src/domain/multichain/index.ts b/packages/connect-multichain/src/domain/multichain/index.ts index e5066530..ac135071 100644 --- a/packages/connect-multichain/src/domain/multichain/index.ts +++ b/packages/connect-multichain/src/domain/multichain/index.ts @@ -72,7 +72,9 @@ export abstract class MultichainCore extends EventEmitter { */ abstract invokeMethod(options: InvokeMethodOptions): Promise; - abstract openDeeplinkIfNeeded(): void; + abstract openSimpleDeeplinkIfNeeded(): void; + + abstract openConnectDeeplinkIfNeeded(): Promise; abstract emitSessionChanged(): Promise; diff --git a/packages/connect-multichain/src/domain/multichain/types.ts b/packages/connect-multichain/src/domain/multichain/types.ts index 57749ddb..825bc013 100644 --- a/packages/connect-multichain/src/domain/multichain/types.ts +++ b/packages/connect-multichain/src/domain/multichain/types.ts @@ -136,5 +136,7 @@ export type ExtendedTransport = Omit & { getActiveSession: () => Promise; + getStoredSessionRequest: () => Promise; + disconnect: (scopes: Scope[]) => Promise; }; diff --git a/packages/connect-multichain/src/multichain/index.ts b/packages/connect-multichain/src/multichain/index.ts index f0b4c3e3..f18550c3 100644 --- a/packages/connect-multichain/src/multichain/index.ts +++ b/packages/connect-multichain/src/multichain/index.ts @@ -597,7 +597,7 @@ export class MetaMaskConnectMultichain extends MultichainCore { if (this.transport.isConnected()) { timeout = setTimeout(() => { - this.openDeeplinkIfNeeded(); + this.openSimpleDeeplinkIfNeeded(); }, 250); } else { this.dappClient.once( @@ -706,8 +706,14 @@ export class MetaMaskConnectMultichain extends MultichainCore { sessionProperties?: SessionProperties, forceRequest?: boolean, ): Promise { - if (this.status !== 'connected') { - await this.disconnect(); + if ( + this.status === 'connecting' && + this.transportType === TransportType.MWP + ) { + await this.openConnectDeeplinkIfNeeded(); + throw new Error( + 'Existing connection is pending. Please check your MetaMask Mobile app to continue.', + ); } const { ui } = this.options; const platformType = getPlatformType(); @@ -909,7 +915,7 @@ export class MetaMaskConnectMultichain extends MultichainCore { } // DRY THIS WITH REQUEST ROUTER - openDeeplinkIfNeeded(): void { + openSimpleDeeplinkIfNeeded(): void { const { ui, mobile } = this.options; const { showInstallModal = false } = ui ?? {}; const secure = isSecure(); @@ -932,6 +938,42 @@ export class MetaMaskConnectMultichain extends MultichainCore { } } + async openConnectDeeplinkIfNeeded(): Promise { + const { ui } = this.options; + const { showInstallModal = false } = ui ?? {}; + const secure = isSecure(); + const shouldOpenDeeplink = secure && !showInstallModal; + + if (!shouldOpenDeeplink) { + return; + } + + const storedSessionRequest = + await this.#transport?.getStoredSessionRequest(); + if (!storedSessionRequest) { + return; + } + + const connectionRequest = { + sessionRequest: storedSessionRequest, + metadata: { + dapp: this.options.dapp, + sdk: { version: getVersion(), platform: getPlatformType() }, + }, + }; + const deeplink = + this.options.ui.factory.createConnectionDeeplink(connectionRequest); + + const universalLink = + this.options.ui.factory.createConnectionUniversalLink(connectionRequest); + + if (this.options.mobile?.preferredOpenLink) { + this.options.mobile.preferredOpenLink(deeplink, '_self'); + } else { + openDeeplink(this.options, deeplink, universalLink); + } + } + async emitSessionChanged(): Promise { const emptySession = { sessionScopes: {} }; diff --git a/packages/connect-multichain/src/multichain/transports/default/index.ts b/packages/connect-multichain/src/multichain/transports/default/index.ts index 7294ddda..f57dc48b 100644 --- a/packages/connect-multichain/src/multichain/transports/default/index.ts +++ b/packages/connect-multichain/src/multichain/transports/default/index.ts @@ -1,4 +1,5 @@ import type { Session } from '@metamask/mobile-wallet-protocol-core'; +import type { SessionRequest } from '@metamask/mobile-wallet-protocol-dapp-client'; import { type SessionProperties, type CreateSessionParams, @@ -320,4 +321,10 @@ export class DefaultTransport implements ExtendedTransport { 'getActiveSession is purposely not implemented for the DefaultTransport', ); } + + async getStoredSessionRequest(): Promise { + throw new Error( + 'getStoredSessionRequest is purposely not implemented for the DefaultTransport', + ); + } } diff --git a/packages/connect-multichain/src/multichain/transports/mwp/index.ts b/packages/connect-multichain/src/multichain/transports/mwp/index.ts index 9f29dc44..6cb2b4de 100644 --- a/packages/connect-multichain/src/multichain/transports/mwp/index.ts +++ b/packages/connect-multichain/src/multichain/transports/mwp/index.ts @@ -53,6 +53,7 @@ const DEFAULT_RESUME_TIMEOUT = 10 * 1000; const SESSION_STORE_KEY = 'cache_wallet_getSession'; const ACCOUNTS_STORE_KEY = 'cache_eth_accounts'; const CHAIN_STORE_KEY = 'cache_eth_chainId'; +const PENDING_SESSION_REQUEST_KEY = 'pending_session_request'; const CACHED_METHOD_LIST = [ 'wallet_getSession', @@ -115,6 +116,14 @@ export class MWPTransport implements ExtendedTransport { }, ) { this.dappClient.on('message', this.handleMessage.bind(this)); + this.dappClient.on('session_request', (sessionRequest: SessionRequest) => { + this.currentSessionRequest = sessionRequest; + this.kvstore + .set(PENDING_SESSION_REQUEST_KEY, JSON.stringify(sessionRequest)) + .catch((err) => { + logger('Failed to store pending session request', err); + }); + }); if ( typeof window !== 'undefined' && typeof window.addEventListener !== 'undefined' @@ -124,6 +133,27 @@ export class MWPTransport implements ExtendedTransport { } } + private async removeStoredSessionRequest(): Promise { + await this.kvstore.delete(PENDING_SESSION_REQUEST_KEY); + } + + /** + * Returns the stored pending session request from the dappClient session_request event, if any. + * + * @returns The stored SessionRequest, or null if none or invalid. + */ + async getStoredSessionRequest(): Promise { + try { + const raw = await this.kvstore.get(PENDING_SESSION_REQUEST_KEY); + if (!raw) { + return null; + } + return JSON.parse(raw) as SessionRequest; + } catch { + return null; + } + } + private onWindowFocus(): void { if (!this.isConnected()) { this.dappClient.reconnect(); @@ -324,6 +354,7 @@ export class MWPTransport implements ExtendedTransport { } walletSession = response.result as SessionData; } + await this.removeStoredSessionRequest(); this.notifyCallbacks({ method: 'wallet_sessionChanged', params: walletSession, @@ -459,6 +490,7 @@ export class MWPTransport implements ExtendedTransport { request, messagePayload as TransportResponse, ); + await this.removeStoredSessionRequest(); this.notifyCallbacks(messagePayload); return resolveConnection(); }; From dece6a16bd8a87b6f68aae094d585ef096f9c4b2 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 18 Feb 2026 13:56:05 -0800 Subject: [PATCH 076/103] Reapply "clear storedSessionRequest after connection handled" This reverts commit 1ae0d2cc8c385b896b914b9ec2ff557565473e7a. --- .../connect-multichain/src/multichain/transports/mwp/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/connect-multichain/src/multichain/transports/mwp/index.ts b/packages/connect-multichain/src/multichain/transports/mwp/index.ts index 6cb2b4de..16e48888 100644 --- a/packages/connect-multichain/src/multichain/transports/mwp/index.ts +++ b/packages/connect-multichain/src/multichain/transports/mwp/index.ts @@ -537,6 +537,7 @@ export class MWPTransport implements ExtendedTransport { this.dappClient.off('message', initialConnectionMessageHandler); initialConnectionMessageHandler = undefined; } + this.removeStoredSessionRequest(); }); } From f86cd2f0b61bf151c24f1b4a6db6887c01a1f4e1 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 18 Feb 2026 13:56:09 -0800 Subject: [PATCH 077/103] Reapply "Add missing removedStoredSessionRequest" This reverts commit 69375df84b9d76a1ca1cac0d2ee43a971cfeba19. --- .../connect-multichain/src/multichain/transports/mwp/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/connect-multichain/src/multichain/transports/mwp/index.ts b/packages/connect-multichain/src/multichain/transports/mwp/index.ts index 16e48888..309154ff 100644 --- a/packages/connect-multichain/src/multichain/transports/mwp/index.ts +++ b/packages/connect-multichain/src/multichain/transports/mwp/index.ts @@ -824,6 +824,7 @@ export class MWPTransport implements ExtendedTransport { const timeoutPromise = new Promise((_resolve, reject) => { setTimeout(() => { unsubscribe(); + this.removeStoredSessionRequest(); reject(new TransportTimeoutError()); }, this.options.resumeTimeout); }); From aad3e60925bc0605f5d68a6c1d203481afede1e3 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 18 Feb 2026 13:57:13 -0800 Subject: [PATCH 078/103] restore throw error on connect if already connecting --- packages/connect-multichain/src/multichain/index.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/connect-multichain/src/multichain/index.ts b/packages/connect-multichain/src/multichain/index.ts index f0b4c3e3..31de7ba8 100644 --- a/packages/connect-multichain/src/multichain/index.ts +++ b/packages/connect-multichain/src/multichain/index.ts @@ -706,8 +706,13 @@ export class MetaMaskConnectMultichain extends MultichainCore { sessionProperties?: SessionProperties, forceRequest?: boolean, ): Promise { - if (this.status !== 'connected') { - await this.disconnect(); + if ( + this.status === 'connecting' && + this.transportType === TransportType.MWP + ) { + throw new Error( + 'Existing connection is pending. Please check your MetaMask Mobile app to continue.', + ); } const { ui } = this.options; const platformType = getPlatformType(); From b29459a9d9a59de150fe4c8efe29e94e8df45bea Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 18 Feb 2026 14:15:47 -0800 Subject: [PATCH 079/103] changelog --- packages/connect-evm/CHANGELOG.md | 1 + packages/connect-multichain/CHANGELOG.md | 6 ++++++ playground/browser-playground/CHANGELOG.md | 10 ++++++++++ 3 files changed, 17 insertions(+) diff --git a/packages/connect-evm/CHANGELOG.md b/packages/connect-evm/CHANGELOG.md index 0c0c0494..49d76ef8 100644 --- a/packages/connect-evm/CHANGELOG.md +++ b/packages/connect-evm/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The `debug` option param used by `createEVMClient()` now enables console debug logs of the underlying `MultichainClient` instance ([#149](https://github.com/MetaMask/connect-monorepo/pull/149)) - update `connect()` and `createEVMClient()` typings to be more accurate ([#153](https://github.com/MetaMask/connect-monorepo/pull/153)) - update `switchChain()` to return `Promise` ([#153](https://github.com/MetaMask/connect-monorepo/pull/153)) +- Make `ConnectEvm` rely on `wallet_sessionChanged` events from `ConnectMultichain` rather than explicit connect/disconnect events ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) ### Fixed diff --git a/packages/connect-multichain/CHANGELOG.md b/packages/connect-multichain/CHANGELOG.md index a63ed169..2f3987ba 100644 --- a/packages/connect-multichain/CHANGELOG.md +++ b/packages/connect-multichain/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed +- **BREAKING** `createMultichainClient()` now returns a singleton. Any incoming constructor params on subsequent calls to `createMultichainClient()` will be applied to the existing singleton instance except for the `dapp`, `storage`, and `ui.factory` param options. ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) +- `ConnectMultichain.connect()` now throws an `'Existing connection is pending. Please check your MetaMask Mobile app to continue.'` error if there is already an ongoing connection attempt. Previously it would abort that ongoing connection in favor of a new one. ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) +- `ConnectMultichain.connect()` adds newly requested scopes and accounts onto any existing permissions rather than fully replacing them. ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) +- `ConnectMultichain.disconnect()` accepts an optional array of scopes. When provided, only those scopes will be revoked from the existing permissions. If no scopes remain after a partial revoke, then the underly connection is fully discarded. If no scopes are specified ()`[]`), then all scopes will be removed. By default all scopes will be removed. ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) + ### Fixed - Fix `beforeunload` event listener not being properly removed on disconnect due to `.bind()` creating different function references, causing a listener leak on each connect/disconnect cycle ([#170](https://github.com/MetaMask/connect-monorepo/pull/170)) diff --git a/playground/browser-playground/CHANGELOG.md b/playground/browser-playground/CHANGELOG.md index 0ef14afe..9fe5c5fe 100644 --- a/playground/browser-playground/CHANGELOG.md +++ b/playground/browser-playground/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Add disconnect buttons to cards ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) + +### Fixed +- Fixes EVM Provider setChainId and setAccounts to also use the connect event ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) +- Multichain Card and button properly reflect initial instantiation connecting status ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) + +### Removed +- Remove the explicit ActiveProviderStorage pattern. Now all providers (cards) are "Active" even without user input to connect to a specific ecosystem ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) + ## [0.2.0] ### Added From 3f3cdb532a80e2222b8071fb4386cf7e2b3bd57b Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 18 Feb 2026 14:32:43 -0800 Subject: [PATCH 080/103] lint --- packages/connect-evm/src/connect.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/connect-evm/src/connect.test.ts b/packages/connect-evm/src/connect.test.ts index 54b36a39..b6099426 100644 --- a/packages/connect-evm/src/connect.test.ts +++ b/packages/connect-evm/src/connect.test.ts @@ -36,7 +36,9 @@ type MockCore = MultichainCore & { }; /** + * Creates a mock MultichainCore for testing. * + * @returns A mock core instance implementing MockCore. */ function createMockCore(): MockCore { const handlers: Record void)[]> = {}; @@ -55,8 +57,9 @@ function createMockCore(): MockCore { const storageSet = vi.fn().mockResolvedValue(undefined); const mockCore = { + // eslint-disable-next-line @typescript-eslint/naming-convention -- mock mirrors real class _status _status: _status as Status, - get status() { + get status(): Status { return this._status; }, set status(value: Status) { From 25b3ef09b7547b46547a2266ff6f2c202c22f995 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 18 Feb 2026 14:35:24 -0800 Subject: [PATCH 081/103] build prereq packages in connect-evm --- packages/connect-evm/package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/connect-evm/package.json b/packages/connect-evm/package.json index 38bb4208..eddf1df7 100644 --- a/packages/connect-evm/package.json +++ b/packages/connect-evm/package.json @@ -47,8 +47,9 @@ "dev": "tsup --watch ", "publish:preview": "yarn npm publish --tag preview", "since-latest-release": "../../scripts/since-latest-release.sh", - "test": "vitest run", - "test:ci": "vitest run --coverage --coverage.reporter=text --silent", + "pretest": "yarn workspace @metamask/analytics run build && yarn workspace @metamask/multichain-ui run build && yarn workspace @metamask/connect-multichain run build", + "test": "yarn pretest && vitest run", + "test:ci": "yarn pretest && vitest run --coverage --coverage.reporter=text --silent", "test:unit": "vitest run", "test:verbose": "vitest run --reporter=verbose", "test:watch": "vitest watch" From 241713cbbf3efa071f32c1ea8a7669c9f868e77f Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 18 Feb 2026 14:48:20 -0800 Subject: [PATCH 082/103] fix missing api options in spec --- packages/connect-multichain/src/connect.test.ts | 4 ++++ packages/connect-multichain/src/init.test.ts | 4 ++++ packages/connect-multichain/src/invoke.test.ts | 4 ++++ packages/connect-multichain/src/session.test.ts | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/packages/connect-multichain/src/connect.test.ts b/packages/connect-multichain/src/connect.test.ts index fd28ca24..ee3d700c 100644 --- a/packages/connect-multichain/src/connect.test.ts +++ b/packages/connect-multichain/src/connect.test.ts @@ -118,6 +118,10 @@ function testSuite({ // Set the transport type as a string in storage (this is how it's stored) testOptions = { ...originalSdkOptions, + api: { + ...originalSdkOptions.api, + supportedNetworks: {}, + }, analytics: { ...originalSdkOptions.analytics, enabled: platform !== 'node', diff --git a/packages/connect-multichain/src/init.test.ts b/packages/connect-multichain/src/init.test.ts index 55b30c0a..2ff964d5 100644 --- a/packages/connect-multichain/src/init.test.ts +++ b/packages/connect-multichain/src/init.test.ts @@ -57,6 +57,10 @@ function testSuite({ testOptions = { ...originalSdkOptions, ui: uiOptions, + api: { + ...originalSdkOptions.api, + supportedNetworks: {}, + }, analytics: { ...originalSdkOptions.analytics, enabled: platform === 'web' || platform === 'web-mobile', diff --git a/packages/connect-multichain/src/invoke.test.ts b/packages/connect-multichain/src/invoke.test.ts index 506e6770..3ab2192d 100644 --- a/packages/connect-multichain/src/invoke.test.ts +++ b/packages/connect-multichain/src/invoke.test.ts @@ -115,6 +115,10 @@ function testSuite({ // Set the transport type as a string in storage (this is how it's stored) testOptions = { ...originalSdkOptions, + api: { + ...originalSdkOptions.api, + supportedNetworks: {}, + }, analytics: { ...originalSdkOptions.analytics, enabled: platform !== 'node', diff --git a/packages/connect-multichain/src/session.test.ts b/packages/connect-multichain/src/session.test.ts index 70799385..21008248 100644 --- a/packages/connect-multichain/src/session.test.ts +++ b/packages/connect-multichain/src/session.test.ts @@ -63,6 +63,10 @@ function testSuite({ // Set the transport type as a string in storage (this is how it's stored) testOptions = { ...originalSdkOptions, + api: { + ...originalSdkOptions.api, + supportedNetworks: {}, + }, analytics: { ...originalSdkOptions.analytics, enabled: platform !== 'node', From bcfbd542dda2874d892d21940a8be47f200afdba Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 18 Feb 2026 15:01:57 -0800 Subject: [PATCH 083/103] fix session test --- packages/connect-multichain/src/session.test.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/connect-multichain/src/session.test.ts b/packages/connect-multichain/src/session.test.ts index 21008248..242428f0 100644 --- a/packages/connect-multichain/src/session.test.ts +++ b/packages/connect-multichain/src/session.test.ts @@ -165,17 +165,8 @@ function testSuite({ t.expect.objectContaining({ method: 'wallet_getSession', }), - { timeout: 60 * 1000 }, ); - t.expect(mockedData.mockDefaultTransport.request).toHaveBeenCalledWith( - t.expect.objectContaining({ - method: 'wallet_revokeSession', - params: mockSessionData, - }), - { timeout: 60 * 1000 }, - ); - t.expect(mockedData.mockDefaultTransport.request).toHaveBeenCalledWith( t.expect.objectContaining({ method: 'wallet_createSession', From b9087d5386690cb35446638748778d8b6b946495 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 18 Feb 2026 15:02:06 -0800 Subject: [PATCH 084/103] Fix singleton test reset --- .../src/polyfills/buffer-shim.ts | 14 ++------------ packages/connect-multichain/tests/fixtures.test.ts | 8 ++++++++ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/connect-multichain/src/polyfills/buffer-shim.ts b/packages/connect-multichain/src/polyfills/buffer-shim.ts index 9fa04274..42c4d308 100644 --- a/packages/connect-multichain/src/polyfills/buffer-shim.ts +++ b/packages/connect-multichain/src/polyfills/buffer-shim.ts @@ -1,6 +1,3 @@ -/* eslint-disable no-restricted-globals -- Polyfill intentionally uses global/window */ -/* eslint-disable no-negated-condition -- Ternary chain is clearer here */ -/* eslint-disable no-nested-ternary -- Environment detection requires chained ternary */ /* eslint-disable import-x/no-nodejs-modules -- Buffer polyfill requires Node.js module */ /** * Buffer polyfill for browser and React Native environments. @@ -12,17 +9,10 @@ */ import { Buffer } from 'buffer'; -// Get the appropriate global object for the current environment -const globalObj = - typeof globalThis !== 'undefined' - ? globalThis - : typeof global !== 'undefined' - ? global - : typeof window !== 'undefined' - ? window - : ({} as typeof globalThis); +import { getGlobalObject } from '../multichain/utils'; // Only set Buffer if it's not already defined (avoid overwriting Node.js native Buffer) +const globalObj = getGlobalObject(); if (!globalObj.Buffer) { globalObj.Buffer = Buffer; } diff --git a/packages/connect-multichain/tests/fixtures.test.ts b/packages/connect-multichain/tests/fixtures.test.ts index 2af9cae6..638721dc 100644 --- a/packages/connect-multichain/tests/fixtures.test.ts +++ b/packages/connect-multichain/tests/fixtures.test.ts @@ -53,6 +53,7 @@ import type { CreateTestFN, } from './types'; import type { MultichainOptions } from '../src/domain'; +import { getGlobalObject } from '../src/multichain/utils'; // Import createSDK functions for convenience import { createMultichainClient as createMetamaskSDKWeb } from '../src/index.browser'; @@ -173,8 +174,15 @@ export const createTest: CreateTestFN = ({ /** * */ + /** Singleton key used by MetaMaskConnectMultichain.create() - clear so each test gets a fresh instance */ + const MULTICHAIN_SINGLETON_KEY = '__METAMASK_CONNECT_MULTICHAIN_SINGLETON__'; + async function beforeEach() { try { + // Clear singleton so each test gets a new SDK instance (avoids state leaking between tests) + const globalObject = getGlobalObject(); + globalObject[MULTICHAIN_SINGLETON_KEY] = undefined; + pendingRequests = new Map(); nativeStorageStub.data.clear(); From a99c7b92f605e7cc96fa3a6f2dbf8f2728355ceb Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 18 Feb 2026 16:16:46 -0800 Subject: [PATCH 085/103] fix handle disconnect error spec --- packages/connect-multichain/src/connect.test.ts | 8 ++++++++ .../src/multichain/transports/mwp/index.ts | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/connect-multichain/src/connect.test.ts b/packages/connect-multichain/src/connect.test.ts index ee3d700c..7229fdab 100644 --- a/packages/connect-multichain/src/connect.test.ts +++ b/packages/connect-multichain/src/connect.test.ts @@ -561,6 +561,14 @@ function testSuite({ t.expect(sdk.provider).toBeDefined(); t.expect(sdk.transport).toBeDefined(); + if (platform === 'web') { + mockedData.mockWalletRevokeSession.mockImplementation( + async () => { + mockedData.mockWalletGetSession.mockResolvedValue({ sessionScopes: {} }); + }, + ); + } + await t .expect(sdk.disconnect()) .rejects.toThrow('Failed to disconnect transport'); diff --git a/packages/connect-multichain/src/multichain/transports/mwp/index.ts b/packages/connect-multichain/src/multichain/transports/mwp/index.ts index 9f29dc44..2c003868 100644 --- a/packages/connect-multichain/src/multichain/transports/mwp/index.ts +++ b/packages/connect-multichain/src/multichain/transports/mwp/index.ts @@ -576,7 +576,7 @@ export class MWPTransport implements ExtendedTransport { this.windowFocusHandler = undefined; } - this.dappClient.disconnect(); + await this.dappClient.disconnect(); } this.notifyCallbacks({ From 4ab38c3ca07f3d2d2b9afe2076b3b34b23db29a9 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 18 Feb 2026 16:18:55 -0800 Subject: [PATCH 086/103] Fix last connect test --- packages/connect-multichain/src/connect.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/connect-multichain/src/connect.test.ts b/packages/connect-multichain/src/connect.test.ts index 7229fdab..2331b646 100644 --- a/packages/connect-multichain/src/connect.test.ts +++ b/packages/connect-multichain/src/connect.test.ts @@ -522,6 +522,15 @@ function testSuite({ ); sdk = await createSDK(testOptions); + + if (platform === 'web') { + mockedData.mockWalletRevokeSession.mockImplementation( + async () => { + mockedData.mockWalletGetSession.mockResolvedValue({ sessionScopes: {} }); + }, + ); + } + await sdk.disconnect(); if (platform === 'web') { From 7b0f15cc3334dd049b6af58483817ade4178e97b Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 18 Feb 2026 14:15:47 -0800 Subject: [PATCH 087/103] changelog --- packages/connect-evm/CHANGELOG.md | 1 + packages/connect-multichain/CHANGELOG.md | 6 ++++++ playground/browser-playground/CHANGELOG.md | 10 ++++++++++ 3 files changed, 17 insertions(+) diff --git a/packages/connect-evm/CHANGELOG.md b/packages/connect-evm/CHANGELOG.md index 0c0c0494..49d76ef8 100644 --- a/packages/connect-evm/CHANGELOG.md +++ b/packages/connect-evm/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The `debug` option param used by `createEVMClient()` now enables console debug logs of the underlying `MultichainClient` instance ([#149](https://github.com/MetaMask/connect-monorepo/pull/149)) - update `connect()` and `createEVMClient()` typings to be more accurate ([#153](https://github.com/MetaMask/connect-monorepo/pull/153)) - update `switchChain()` to return `Promise` ([#153](https://github.com/MetaMask/connect-monorepo/pull/153)) +- Make `ConnectEvm` rely on `wallet_sessionChanged` events from `ConnectMultichain` rather than explicit connect/disconnect events ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) ### Fixed diff --git a/packages/connect-multichain/CHANGELOG.md b/packages/connect-multichain/CHANGELOG.md index a63ed169..2f3987ba 100644 --- a/packages/connect-multichain/CHANGELOG.md +++ b/packages/connect-multichain/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed +- **BREAKING** `createMultichainClient()` now returns a singleton. Any incoming constructor params on subsequent calls to `createMultichainClient()` will be applied to the existing singleton instance except for the `dapp`, `storage`, and `ui.factory` param options. ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) +- `ConnectMultichain.connect()` now throws an `'Existing connection is pending. Please check your MetaMask Mobile app to continue.'` error if there is already an ongoing connection attempt. Previously it would abort that ongoing connection in favor of a new one. ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) +- `ConnectMultichain.connect()` adds newly requested scopes and accounts onto any existing permissions rather than fully replacing them. ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) +- `ConnectMultichain.disconnect()` accepts an optional array of scopes. When provided, only those scopes will be revoked from the existing permissions. If no scopes remain after a partial revoke, then the underly connection is fully discarded. If no scopes are specified ()`[]`), then all scopes will be removed. By default all scopes will be removed. ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) + ### Fixed - Fix `beforeunload` event listener not being properly removed on disconnect due to `.bind()` creating different function references, causing a listener leak on each connect/disconnect cycle ([#170](https://github.com/MetaMask/connect-monorepo/pull/170)) diff --git a/playground/browser-playground/CHANGELOG.md b/playground/browser-playground/CHANGELOG.md index 0ef14afe..9fe5c5fe 100644 --- a/playground/browser-playground/CHANGELOG.md +++ b/playground/browser-playground/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Add disconnect buttons to cards ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) + +### Fixed +- Fixes EVM Provider setChainId and setAccounts to also use the connect event ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) +- Multichain Card and button properly reflect initial instantiation connecting status ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) + +### Removed +- Remove the explicit ActiveProviderStorage pattern. Now all providers (cards) are "Active" even without user input to connect to a specific ecosystem ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) + ## [0.2.0] ### Added From 0281b2d0a697f8eb86a981844236a2391a89512e Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 18 Feb 2026 16:31:35 -0800 Subject: [PATCH 088/103] lint --- packages/connect-multichain/CHANGELOG.md | 1 - .../connect-multichain/src/connect.test.ts | 20 +++++++++---------- .../src/polyfills/buffer-shim.ts | 4 +--- .../connect-multichain/tests/fixtures.test.ts | 7 ++++++- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/packages/connect-multichain/CHANGELOG.md b/packages/connect-multichain/CHANGELOG.md index 2f3987ba..cdccde88 100644 --- a/packages/connect-multichain/CHANGELOG.md +++ b/packages/connect-multichain/CHANGELOG.md @@ -6,7 +6,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] - ### Changed - **BREAKING** `createMultichainClient()` now returns a singleton. Any incoming constructor params on subsequent calls to `createMultichainClient()` will be applied to the existing singleton instance except for the `dapp`, `storage`, and `ui.factory` param options. ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) - `ConnectMultichain.connect()` now throws an `'Existing connection is pending. Please check your MetaMask Mobile app to continue.'` error if there is already an ongoing connection attempt. Previously it would abort that ongoing connection in favor of a new one. ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) diff --git a/packages/connect-multichain/src/connect.test.ts b/packages/connect-multichain/src/connect.test.ts index 2331b646..f50d32a0 100644 --- a/packages/connect-multichain/src/connect.test.ts +++ b/packages/connect-multichain/src/connect.test.ts @@ -524,11 +524,11 @@ function testSuite({ sdk = await createSDK(testOptions); if (platform === 'web') { - mockedData.mockWalletRevokeSession.mockImplementation( - async () => { - mockedData.mockWalletGetSession.mockResolvedValue({ sessionScopes: {} }); - }, - ); + mockedData.mockWalletRevokeSession.mockImplementation(async () => { + mockedData.mockWalletGetSession.mockResolvedValue({ + sessionScopes: {}, + }); + }); } await sdk.disconnect(); @@ -571,11 +571,11 @@ function testSuite({ t.expect(sdk.transport).toBeDefined(); if (platform === 'web') { - mockedData.mockWalletRevokeSession.mockImplementation( - async () => { - mockedData.mockWalletGetSession.mockResolvedValue({ sessionScopes: {} }); - }, - ); + mockedData.mockWalletRevokeSession.mockImplementation(async () => { + mockedData.mockWalletGetSession.mockResolvedValue({ + sessionScopes: {}, + }); + }); } await t diff --git a/packages/connect-multichain/src/polyfills/buffer-shim.ts b/packages/connect-multichain/src/polyfills/buffer-shim.ts index 42c4d308..a7963a12 100644 --- a/packages/connect-multichain/src/polyfills/buffer-shim.ts +++ b/packages/connect-multichain/src/polyfills/buffer-shim.ts @@ -13,6 +13,4 @@ import { getGlobalObject } from '../multichain/utils'; // Only set Buffer if it's not already defined (avoid overwriting Node.js native Buffer) const globalObj = getGlobalObject(); -if (!globalObj.Buffer) { - globalObj.Buffer = Buffer; -} +globalObj.Buffer ??= Buffer; diff --git a/packages/connect-multichain/tests/fixtures.test.ts b/packages/connect-multichain/tests/fixtures.test.ts index 638721dc..e8b8922e 100644 --- a/packages/connect-multichain/tests/fixtures.test.ts +++ b/packages/connect-multichain/tests/fixtures.test.ts @@ -12,7 +12,7 @@ /* eslint-disable import-x/order -- Mock imports need specific order */ /* eslint-disable @typescript-eslint/no-use-before-define -- Function hoisting */ /* eslint-disable @typescript-eslint/prefer-promise-reject-errors -- Test rejection patterns */ -/* eslint-disable jsdoc/require-returns -- Test helpers */ + /* eslint-disable no-plusplus -- Test loops */ /* eslint-disable no-invalid-this -- Test context */ /* eslint-disable no-useless-catch -- Test error handling */ @@ -177,6 +177,11 @@ export const createTest: CreateTestFN = ({ /** Singleton key used by MetaMaskConnectMultichain.create() - clear so each test gets a fresh instance */ const MULTICHAIN_SINGLETON_KEY = '__METAMASK_CONNECT_MULTICHAIN_SINGLETON__'; + /** + * Sets up mocks and clears singleton before each test. + * + * @returns Promise resolving to the mocked data for the test. + */ async function beforeEach() { try { // Clear singleton so each test gets a new SDK instance (avoids state leaking between tests) From f20b443ddf11880e7218f5d50013acdc1545309e Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 18 Feb 2026 16:36:39 -0800 Subject: [PATCH 089/103] lint --- packages/connect-multichain/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/connect-multichain/CHANGELOG.md b/packages/connect-multichain/CHANGELOG.md index cdccde88..7fe1248f 100644 --- a/packages/connect-multichain/CHANGELOG.md +++ b/packages/connect-multichain/CHANGELOG.md @@ -6,7 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + ### Changed + - **BREAKING** `createMultichainClient()` now returns a singleton. Any incoming constructor params on subsequent calls to `createMultichainClient()` will be applied to the existing singleton instance except for the `dapp`, `storage`, and `ui.factory` param options. ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) - `ConnectMultichain.connect()` now throws an `'Existing connection is pending. Please check your MetaMask Mobile app to continue.'` error if there is already an ongoing connection attempt. Previously it would abort that ongoing connection in favor of a new one. ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) - `ConnectMultichain.connect()` adds newly requested scopes and accounts onto any existing permissions rather than fully replacing them. ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) From 8d0dd516370cd53dc6a1b177b903303003822357 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 18 Feb 2026 16:44:14 -0800 Subject: [PATCH 090/103] lint --- playground/browser-playground/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/playground/browser-playground/CHANGELOG.md b/playground/browser-playground/CHANGELOG.md index 9fe5c5fe..cbb70804 100644 --- a/playground/browser-playground/CHANGELOG.md +++ b/playground/browser-playground/CHANGELOG.md @@ -8,13 +8,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added + - Add disconnect buttons to cards ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) ### Fixed + - Fixes EVM Provider setChainId and setAccounts to also use the connect event ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) - Multichain Card and button properly reflect initial instantiation connecting status ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) ### Removed + - Remove the explicit ActiveProviderStorage pattern. Now all providers (cards) are "Active" even without user input to connect to a specific ecosystem ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) ## [0.2.0] From 32f67dfe7ff728e9c7e46c949350ae27525536e4 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 18 Feb 2026 16:48:28 -0800 Subject: [PATCH 091/103] lint --- playground/browser-playground/CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/playground/browser-playground/CHANGELOG.md b/playground/browser-playground/CHANGELOG.md index cbb70804..79cb1a3b 100644 --- a/playground/browser-playground/CHANGELOG.md +++ b/playground/browser-playground/CHANGELOG.md @@ -11,15 +11,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add disconnect buttons to cards ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) +### Removed + +- Remove the explicit ActiveProviderStorage pattern. Now all providers (cards) are "Active" even without user input to connect to a specific ecosystem ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) + ### Fixed - Fixes EVM Provider setChainId and setAccounts to also use the connect event ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) - Multichain Card and button properly reflect initial instantiation connecting status ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) -### Removed - -- Remove the explicit ActiveProviderStorage pattern. Now all providers (cards) are "Active" even without user input to connect to a specific ecosystem ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) - ## [0.2.0] ### Added From 026d8cac471964de70fbd66c32a73530f54894e2 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 19 Feb 2026 09:16:02 -0800 Subject: [PATCH 092/103] make openConnectDeeplinkIfNeeded private --- .../connect-multichain/src/domain/multichain/index.test.ts | 2 -- packages/connect-multichain/src/domain/multichain/index.ts | 2 -- packages/connect-multichain/src/multichain/index.ts | 4 ++-- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/connect-multichain/src/domain/multichain/index.test.ts b/packages/connect-multichain/src/domain/multichain/index.test.ts index 36003bb6..e6ad9148 100644 --- a/packages/connect-multichain/src/domain/multichain/index.test.ts +++ b/packages/connect-multichain/src/domain/multichain/index.test.ts @@ -31,8 +31,6 @@ class MockMultichainCore extends MultichainCore { openSimpleDeeplinkIfNeeded = (): void => undefined; - openConnectDeeplinkIfNeeded = async (): Promise => Promise.resolve(); - emitSessionChanged = async (): Promise => Promise.resolve(); /** diff --git a/packages/connect-multichain/src/domain/multichain/index.ts b/packages/connect-multichain/src/domain/multichain/index.ts index ac135071..da624dac 100644 --- a/packages/connect-multichain/src/domain/multichain/index.ts +++ b/packages/connect-multichain/src/domain/multichain/index.ts @@ -74,8 +74,6 @@ export abstract class MultichainCore extends EventEmitter { abstract openSimpleDeeplinkIfNeeded(): void; - abstract openConnectDeeplinkIfNeeded(): Promise; - abstract emitSessionChanged(): Promise; constructor(protected options: MultichainOptions) { diff --git a/packages/connect-multichain/src/multichain/index.ts b/packages/connect-multichain/src/multichain/index.ts index f18550c3..0fbb1ef3 100644 --- a/packages/connect-multichain/src/multichain/index.ts +++ b/packages/connect-multichain/src/multichain/index.ts @@ -710,7 +710,7 @@ export class MetaMaskConnectMultichain extends MultichainCore { this.status === 'connecting' && this.transportType === TransportType.MWP ) { - await this.openConnectDeeplinkIfNeeded(); + await this.#openConnectDeeplinkIfNeeded(); throw new Error( 'Existing connection is pending. Please check your MetaMask Mobile app to continue.', ); @@ -938,7 +938,7 @@ export class MetaMaskConnectMultichain extends MultichainCore { } } - async openConnectDeeplinkIfNeeded(): Promise { + async #openConnectDeeplinkIfNeeded(): Promise { const { ui } = this.options; const { showInstallModal = false } = ui ?? {}; const secure = isSecure(); From 21dea4c8e30d1ba1c5ac86a7869a60e58f263ec1 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 19 Feb 2026 09:18:49 -0800 Subject: [PATCH 093/103] changelog --- packages/connect-evm/CHANGELOG.md | 1 + packages/connect-multichain/CHANGELOG.md | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/packages/connect-evm/CHANGELOG.md b/packages/connect-evm/CHANGELOG.md index 49d76ef8..17ad6d58 100644 --- a/packages/connect-evm/CHANGELOG.md +++ b/packages/connect-evm/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - update `connect()` and `createEVMClient()` typings to be more accurate ([#153](https://github.com/MetaMask/connect-monorepo/pull/153)) - update `switchChain()` to return `Promise` ([#153](https://github.com/MetaMask/connect-monorepo/pull/153)) - Make `ConnectEvm` rely on `wallet_sessionChanged` events from `ConnectMultichain` rather than explicit connect/disconnect events ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) +- Chain add/switch deeplink now calls `openSimpleDeeplinkIfNeeded()` instead of `openDeeplinkIfNeeded()` to align with `@metamask/connect-multichain` changes ([#176](https://github.com/MetaMask/connect-monorepo/pull/176)) ### Fixed diff --git a/packages/connect-multichain/CHANGELOG.md b/packages/connect-multichain/CHANGELOG.md index 7fe1248f..7fdd284c 100644 --- a/packages/connect-multichain/CHANGELOG.md +++ b/packages/connect-multichain/CHANGELOG.md @@ -7,9 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- When `ConnectMultichain.connect()` is called while a connection is already pending, the user is re-prompted with the pending connection deeplink. ([#176](https://github.com/MetaMask/connect-monorepo/pull/176)) + ### Changed - **BREAKING** `createMultichainClient()` now returns a singleton. Any incoming constructor params on subsequent calls to `createMultichainClient()` will be applied to the existing singleton instance except for the `dapp`, `storage`, and `ui.factory` param options. ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) +- **BREAKING** `ConnectMultichain.openDeeplinkIfNeeded()` is renamed to `openSimpleDeeplinkIfNeeded()` ([#176](https://github.com/MetaMask/connect-monorepo/pull/176)) - `ConnectMultichain.connect()` now throws an `'Existing connection is pending. Please check your MetaMask Mobile app to continue.'` error if there is already an ongoing connection attempt. Previously it would abort that ongoing connection in favor of a new one. ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) - `ConnectMultichain.connect()` adds newly requested scopes and accounts onto any existing permissions rather than fully replacing them. ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) - `ConnectMultichain.disconnect()` accepts an optional array of scopes. When provided, only those scopes will be revoked from the existing permissions. If no scopes remain after a partial revoke, then the underly connection is fully discarded. If no scopes are specified ()`[]`), then all scopes will be removed. By default all scopes will be removed. ([#157](https://github.com/MetaMask/connect-monorepo/pull/157)) From 7a74583c8d3b21b0291dffe88a1907b6f9930e4a Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 19 Feb 2026 15:27:55 -0800 Subject: [PATCH 094/103] fix typo setupTransportNotifcationListener --- packages/connect-multichain/src/multichain/index.ts | 8 ++++---- .../transports/multichainApiClientWrapper/index.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/connect-multichain/src/multichain/index.ts b/packages/connect-multichain/src/multichain/index.ts index 31de7ba8..54df3d83 100644 --- a/packages/connect-multichain/src/multichain/index.ts +++ b/packages/connect-multichain/src/multichain/index.ts @@ -247,7 +247,7 @@ export class MetaMaskConnectMultichain extends MultichainCore { if (hasExtensionInstalled) { const apiTransport = new DefaultTransport(); this.#transport = apiTransport; - this.#providerTransportWrapper.setupTransportNotifcationListener(); + this.#providerTransportWrapper.setupTransportNotificationListener(); this.#listener = apiTransport.onNotification( this.#onTransportNotification.bind(this), ); @@ -259,7 +259,7 @@ export class MetaMaskConnectMultichain extends MultichainCore { const apiTransport = new MWPTransport(dappClient, kvstore); this.#dappClient = dappClient; this.#transport = apiTransport; - this.#providerTransportWrapper.setupTransportNotifcationListener(); + this.#providerTransportWrapper.setupTransportNotificationListener(); this.#listener = apiTransport.onNotification( this.#onTransportNotification.bind(this), ); @@ -337,7 +337,7 @@ export class MetaMaskConnectMultichain extends MultichainCore { this.#dappClient = dappClient; const apiTransport = new MWPTransport(dappClient, kvstore); this.#transport = apiTransport; - this.#providerTransportWrapper.setupTransportNotifcationListener(); + this.#providerTransportWrapper.setupTransportNotificationListener(); this.#listener = this.transport.onNotification( this.#onTransportNotification.bind(this), ); @@ -558,7 +558,7 @@ export class MetaMaskConnectMultichain extends MultichainCore { this.#onTransportNotification.bind(this), ); this.#transport = transport; - this.#providerTransportWrapper.setupTransportNotifcationListener(); + this.#providerTransportWrapper.setupTransportNotificationListener(); return transport; } diff --git a/packages/connect-multichain/src/multichain/transports/multichainApiClientWrapper/index.ts b/packages/connect-multichain/src/multichain/transports/multichainApiClientWrapper/index.ts index 2be359cb..470acdba 100644 --- a/packages/connect-multichain/src/multichain/transports/multichainApiClientWrapper/index.ts +++ b/packages/connect-multichain/src/multichain/transports/multichainApiClientWrapper/index.ts @@ -61,7 +61,7 @@ export class MultichainApiClientWrapperTransport implements Transport { this.notificationListener = undefined; } - setupTransportNotifcationListener(): void { + setupTransportNotificationListener(): void { if (!this.isTransportDefined() || this.notificationListener) { return; } @@ -115,7 +115,7 @@ export class MultichainApiClientWrapperTransport implements Transport { } onNotification(callback: (data: unknown) => void): () => void { - this.setupTransportNotifcationListener(); + this.setupTransportNotificationListener(); this.#notificationCallbacks.add(callback); return () => { this.#notificationCallbacks.delete(callback); From f3ba9ceeb32ab75d7782f0f3180413d147f7071a Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 19 Feb 2026 15:39:19 -0800 Subject: [PATCH 095/103] fix typo clearTransportNotifcationListener --- packages/connect-multichain/src/multichain/index.ts | 2 +- .../multichain/transports/multichainApiClientWrapper/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/connect-multichain/src/multichain/index.ts b/packages/connect-multichain/src/multichain/index.ts index 54df3d83..f39b4325 100644 --- a/packages/connect-multichain/src/multichain/index.ts +++ b/packages/connect-multichain/src/multichain/index.ts @@ -898,7 +898,7 @@ export class MetaMaskConnectMultichain extends MultichainCore { this.#listener = undefined; this.#beforeUnloadListener = undefined; this.#transport = undefined; - this.#providerTransportWrapper.clearTransportNotifcationListener(); + this.#providerTransportWrapper.clearTransportNotificationListener(); this.#dappClient = undefined; this.status = 'disconnected'; } diff --git a/packages/connect-multichain/src/multichain/transports/multichainApiClientWrapper/index.ts b/packages/connect-multichain/src/multichain/transports/multichainApiClientWrapper/index.ts index 470acdba..d9247c5f 100644 --- a/packages/connect-multichain/src/multichain/transports/multichainApiClientWrapper/index.ts +++ b/packages/connect-multichain/src/multichain/transports/multichainApiClientWrapper/index.ts @@ -56,7 +56,7 @@ export class MultichainApiClientWrapperTransport implements Transport { }); } - clearTransportNotifcationListener(): void { + clearTransportNotificationListener(): void { this.notificationListener?.(); this.notificationListener = undefined; } From abb2d689cfbadf850a680a0f8390287b95a05edd Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 19 Feb 2026 15:41:50 -0800 Subject: [PATCH 096/103] rename to mergedScopes/CaipAccountsIds/SessionProperties --- .../src/multichain/index.ts | 26 +++++++++---------- .../src/multichain/utils/index.ts | 18 ++++++------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/connect-multichain/src/multichain/index.ts b/packages/connect-multichain/src/multichain/index.ts index f39b4325..52602c65 100644 --- a/packages/connect-multichain/src/multichain/index.ts +++ b/packages/connect-multichain/src/multichain/index.ts @@ -755,9 +755,9 @@ export class MetaMaskConnectMultichain extends MultichainCore { const sessionData = await this.#getCaipSession(); const { - requestedScopes, - requestedCaipAccountIds, - requestedSessionProperties, + mergedScopes, + mergedCaipAccountIds, + mergedSessionProperties, } = mergeRequestedSessionWithExisting( sessionData, scopes, @@ -767,16 +767,16 @@ export class MetaMaskConnectMultichain extends MultichainCore { // Needed because empty object will cause wallet_createSession to return an error const nonEmptySessionProperties = - Object.keys(requestedSessionProperties ?? {}).length > 0 - ? requestedSessionProperties + Object.keys(mergedSessionProperties ?? {}).length > 0 + ? mergedSessionProperties : undefined; if (this.#transport?.isConnected() && !secure) { return this.#handleConnection( this.#transport .connect({ - scopes: requestedScopes, - caipAccountIds: requestedCaipAccountIds, + scopes: mergedScopes, + caipAccountIds: mergedCaipAccountIds, sessionProperties: nonEmptySessionProperties, forceRequest, }) @@ -796,8 +796,8 @@ export class MetaMaskConnectMultichain extends MultichainCore { const defaultTransport = await this.#setupDefaultTransport(); return this.#handleConnection( defaultTransport.connect({ - scopes: requestedScopes, - caipAccountIds: requestedCaipAccountIds, + scopes: mergedScopes, + caipAccountIds: mergedCaipAccountIds, sessionProperties: nonEmptySessionProperties, forceRequest, }), @@ -812,8 +812,8 @@ export class MetaMaskConnectMultichain extends MultichainCore { // Web transport has no initial payload return this.#handleConnection( defaultTransport.connect({ - scopes: requestedScopes, - caipAccountIds: requestedCaipAccountIds, + scopes: mergedScopes, + caipAccountIds: mergedCaipAccountIds, sessionProperties: nonEmptySessionProperties, forceRequest, }), @@ -847,8 +847,8 @@ export class MetaMaskConnectMultichain extends MultichainCore { return this.#handleConnection( this.#showInstallModal( shouldShowInstallModal, - requestedScopes, - requestedCaipAccountIds, + mergedScopes, + mergedCaipAccountIds, nonEmptySessionProperties, ), scopes, diff --git a/packages/connect-multichain/src/multichain/utils/index.ts b/packages/connect-multichain/src/multichain/utils/index.ts index 547f7dbf..21f1743b 100644 --- a/packages/connect-multichain/src/multichain/utils/index.ts +++ b/packages/connect-multichain/src/multichain/utils/index.ts @@ -135,9 +135,9 @@ export function mergeRequestedSessionWithExisting( caipAccountIds: CaipAccountId[], sessionProperties?: SessionProperties, ): { - requestedScopes: Scope[]; - requestedCaipAccountIds: CaipAccountId[]; - requestedSessionProperties: SessionProperties; + mergedScopes: Scope[]; + mergedCaipAccountIds: CaipAccountId[]; + mergedSessionProperties: SessionProperties; } { const existingCaipChainIds = Object.keys(sessionData.sessionScopes); const existingCaipAccountIds: string[] = []; @@ -149,20 +149,20 @@ export function mergeRequestedSessionWithExisting( } }); - const requestedScopes = Array.from( + const mergedScopes = Array.from( new Set([...existingCaipChainIds, ...scopes]), ) as Scope[]; - const requestedCaipAccountIds = Array.from( + const mergedCaipAccountIds = Array.from( new Set([...existingCaipAccountIds, ...caipAccountIds]), ) as CaipAccountId[]; - const requestedSessionProperties = { + const mergedSessionProperties = { ...sessionData.sessionProperties, ...sessionProperties, }; return { - requestedScopes, - requestedCaipAccountIds, - requestedSessionProperties, + mergedScopes, + mergedCaipAccountIds, + mergedSessionProperties, }; } From 64a78f8fc1f395cc0a681ff159c8063617c7f41b Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 19 Feb 2026 15:43:43 -0800 Subject: [PATCH 097/103] use mergedScopes and caipAccountIds for deeplink initiated MWP --- packages/connect-multichain/src/multichain/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/connect-multichain/src/multichain/index.ts b/packages/connect-multichain/src/multichain/index.ts index 52602c65..13477fcb 100644 --- a/packages/connect-multichain/src/multichain/index.ts +++ b/packages/connect-multichain/src/multichain/index.ts @@ -834,8 +834,8 @@ export class MetaMaskConnectMultichain extends MultichainCore { // Desktop is not preferred option, so we use deeplinks (mobile web) return this.#handleConnection( this.#deeplinkConnect( - scopes, - caipAccountIds, + mergedScopes, + mergedCaipAccountIds, nonEmptySessionProperties, ), scopes, From 638a938b3cdd4c5d4294eeea64d512a3107c3872 Mon Sep 17 00:00:00 2001 From: jiexi Date: Fri, 20 Feb 2026 08:15:58 -0800 Subject: [PATCH 098/103] Update packages/connect-multichain/src/multichain/transports/multichainApiClientWrapper/index.ts Co-authored-by: Tamas --- .../multichain/transports/multichainApiClientWrapper/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/connect-multichain/src/multichain/transports/multichainApiClientWrapper/index.ts b/packages/connect-multichain/src/multichain/transports/multichainApiClientWrapper/index.ts index d9247c5f..5b18a47d 100644 --- a/packages/connect-multichain/src/multichain/transports/multichainApiClientWrapper/index.ts +++ b/packages/connect-multichain/src/multichain/transports/multichainApiClientWrapper/index.ts @@ -182,7 +182,7 @@ export class MultichainApiClientWrapperTransport implements Transport { const scopes = revokeSessionParams?.scopes ?? []; try { - this.metamaskConnectMultichain.disconnect(scopes as Scope[]); + await this.metamaskConnectMultichain.disconnect(scopes as Scope[]); return { jsonrpc: '2.0', id: request.id, result: true }; } catch (_error) { return { jsonrpc: '2.0', id: request.id, result: false }; From 7441fbc20f30a86b3814a87a2d6ba0140aeb6b6f Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 20 Feb 2026 09:03:48 -0800 Subject: [PATCH 099/103] Fix connect evm status --- packages/connect-evm/src/connect.test.ts | 20 ++++++-------------- packages/connect-evm/src/connect.ts | 4 +++- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/packages/connect-evm/src/connect.test.ts b/packages/connect-evm/src/connect.test.ts index b6099426..aea46ce3 100644 --- a/packages/connect-evm/src/connect.test.ts +++ b/packages/connect-evm/src/connect.test.ts @@ -2,19 +2,11 @@ import type { SessionData, MultichainCore } from '@metamask/connect-multichain'; import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'; -import { MetamaskConnectEVM } from './connect'; +import { ConnectEvmStatus, MetamaskConnectEVM } from './connect'; -type Status = - | 'connected' - | 'disconnected' - | 'connecting' - | 'loaded' - | 'pending'; - -/** Mock core type so storage/transport mocks keep .mockResolvedValue in tests */ type MockCore = MultichainCore & { emit: (event: string, ...args: unknown[]) => void; - _status: Status; + _status: ConnectEvmStatus; storage: MultichainCore['storage'] & { adapter: { get: Mock<(key: string) => Promise>; @@ -42,7 +34,7 @@ type MockCore = MultichainCore & { */ function createMockCore(): MockCore { const handlers: Record void)[]> = {}; - const _status: Status = 'disconnected'; + const _status: ConnectEvmStatus = 'disconnected'; const sendEip1193Message = vi.fn().mockResolvedValue({ result: [] as string[], @@ -58,11 +50,11 @@ function createMockCore(): MockCore { const mockCore = { // eslint-disable-next-line @typescript-eslint/naming-convention -- mock mirrors real class _status - _status: _status as Status, - get status(): Status { + _status: _status as ConnectEvmStatus, + get status(): ConnectEvmStatus { return this._status; }, - set status(value: Status) { + set status(value: ConnectEvmStatus) { this._status = value; }, on(event: string, handler: (...args: unknown[]) => void): void { diff --git a/packages/connect-evm/src/connect.ts b/packages/connect-evm/src/connect.ts index 2d626098..224a2bac 100644 --- a/packages/connect-evm/src/connect.ts +++ b/packages/connect-evm/src/connect.ts @@ -52,6 +52,8 @@ type ConnectOptions = { chainIds?: Hex[]; }; +export type ConnectEvmStatus = 'disconnected' | 'connected' | 'connecting'; + /** * The MetamaskConnectEVM class provides an EIP-1193 compatible interface for connecting * to MetaMask and interacting with Ethereum Virtual Machine (EVM) networks. @@ -103,7 +105,7 @@ export class MetamaskConnectEVM { #removeNotificationHandler?: () => void; /** The current connection status */ - #status: 'disconnected' | 'connected' | 'connecting' = 'disconnected'; + #status: ConnectEvmStatus = 'disconnected'; /** * Creates a new MetamaskConnectEVM instance. From aa366a55791eb97c391dc4cb28b1e97daf27b298 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 20 Feb 2026 09:17:55 -0800 Subject: [PATCH 100/103] add emitSessionChanged comments --- packages/connect-multichain/src/multichain/index.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/connect-multichain/src/multichain/index.ts b/packages/connect-multichain/src/multichain/index.ts index 13477fcb..0adaa4cf 100644 --- a/packages/connect-multichain/src/multichain/index.ts +++ b/packages/connect-multichain/src/multichain/index.ts @@ -937,18 +937,25 @@ export class MetaMaskConnectMultichain extends MultichainCore { } } + // Provides a way for ecosystem clients (EVM, Solana, etc.) to get the current CAIP session data + // when instantiating themselves (as they would have already missed any initial sessionChanged events emitted by ConnectMultichain) + // without having to concern themselves with the current transport connection status. async emitSessionChanged(): Promise { const emptySession = { sessionScopes: {} }; if (this.status !== 'connected' && this.status !== 'connecting') { + // If we aren't connected or connecting, there definitely is no active CAIP session + // so we optimistically emit an empty session to signify that to the ecosystem client consumers (EVM, Solana, etc.) this.emit('wallet_sessionChanged', emptySession); return; } + // Otherwise, we need to fetch the current CAIP session from the wallet const response = await this.transport.request({ method: 'wallet_getSession', }); + // And then simulate a sessionChanged event with the current CAIP session data this.emit('wallet_sessionChanged', response.result ?? emptySession); } } From d31249d3fc5ce3e27c01201ab11bc3943781242d Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 20 Feb 2026 10:00:40 -0800 Subject: [PATCH 101/103] add comment to create() --- packages/connect-multichain/src/multichain/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/connect-multichain/src/multichain/index.ts b/packages/connect-multichain/src/multichain/index.ts index 0adaa4cf..53705149 100644 --- a/packages/connect-multichain/src/multichain/index.ts +++ b/packages/connect-multichain/src/multichain/index.ts @@ -160,6 +160,10 @@ export class MetaMaskConnectMultichain extends MultichainCore { }); } + // Creates a singleton instance of MetaMaskConnectMultichain. + // If the singleton already exists, it merges the incoming options with the + // existing singleton options for the following keys: `api.supportedNetworks`, + // `ui.*`, `mobile.*`, `transport.extensionId`, `debug` static async create( options: MultichainOptions, ): Promise { From bef3c8018b31c8efaf413a67ac5cca67d70384b9 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 20 Feb 2026 10:10:03 -0800 Subject: [PATCH 102/103] elaborate on which params are not merged --- packages/connect-multichain/src/domain/multichain/index.ts | 2 ++ packages/connect-multichain/src/multichain/index.ts | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/connect-multichain/src/domain/multichain/index.ts b/packages/connect-multichain/src/domain/multichain/index.ts index e5066530..17c0ad1d 100644 --- a/packages/connect-multichain/src/domain/multichain/index.ts +++ b/packages/connect-multichain/src/domain/multichain/index.ts @@ -83,6 +83,8 @@ export abstract class MultichainCore extends EventEmitter { /** * Merges the given options into the current instance options. * Only the mergeable keys are updated (api.supportedNetworks, ui.*, mobile.*, transport.extensionId, debug). + * The main thing to note is that the value for `dapp` is not merged as it does not make sense for + * subsequent calls to `createMultichainClient` to have a different `dapp` value. * Used when createMultichainClient is called with an existing singleton. * * @param partial - Options to merge/overwrite onto the current instance diff --git a/packages/connect-multichain/src/multichain/index.ts b/packages/connect-multichain/src/multichain/index.ts index 53705149..68db90dd 100644 --- a/packages/connect-multichain/src/multichain/index.ts +++ b/packages/connect-multichain/src/multichain/index.ts @@ -163,7 +163,9 @@ export class MetaMaskConnectMultichain extends MultichainCore { // Creates a singleton instance of MetaMaskConnectMultichain. // If the singleton already exists, it merges the incoming options with the // existing singleton options for the following keys: `api.supportedNetworks`, - // `ui.*`, `mobile.*`, `transport.extensionId`, `debug` + // `ui.*`, `mobile.*`, `transport.extensionId`, `debug`. Take note that the + // value for `dapp` is not merged as it does not make sense for subsequent calls to + // `createMultichainClient` to have a different `dapp` value. static async create( options: MultichainOptions, ): Promise { From 5d4b50a7c6518ffc1973d0eb2737c94cdae5a52a Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 20 Feb 2026 10:23:07 -0800 Subject: [PATCH 103/103] clean up sendEip1193message eth_accounts cast --- packages/connect-evm/src/connect.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/connect-evm/src/connect.ts b/packages/connect-evm/src/connect.ts index 224a2bac..3dcc7930 100644 --- a/packages/connect-evm/src/connect.ts +++ b/packages/connect-evm/src/connect.ts @@ -734,11 +734,8 @@ export class MetamaskConnectEVM { let initialAccounts: Address[] = []; if (this.#core.status === 'connected') { const ethAccountsResponse = - await this.#core.transport.sendEip1193Message< - { method: 'eth_accounts'; params: [] }, - { result: Address[]; id: number; jsonrpc: '2.0' } - >({ method: 'eth_accounts', params: [] }); - initialAccounts = ethAccountsResponse.result; + await this.#core.transport.sendEip1193Message({ method: 'eth_accounts', params: [] }); + initialAccounts = ethAccountsResponse.result as Address[]; } else { initialAccounts = getEthAccounts(this.#sessionScopes); }