From 242a67f221a1d2cefbfb1ae5f1cacef63dbab30e Mon Sep 17 00:00:00 2001 From: lwin Date: Thu, 28 May 2026 17:14:30 +0800 Subject: [PATCH 1/8] fix: do not support 7702/5792 for 4337 AA accounts --- .../providers/AccountAbstractionProvider.ts | 16 ++++++++- .../rpc/ethRpcMiddlewares.ts | 33 +++++++++++++++++-- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/packages/no-modal/src/providers/account-abstraction-provider/providers/AccountAbstractionProvider.ts b/packages/no-modal/src/providers/account-abstraction-provider/providers/AccountAbstractionProvider.ts index e63361c85..226b3597b 100644 --- a/packages/no-modal/src/providers/account-abstraction-provider/providers/AccountAbstractionProvider.ts +++ b/packages/no-modal/src/providers/account-abstraction-provider/providers/AccountAbstractionProvider.ts @@ -2,6 +2,8 @@ import { type AccountAbstractionMultiChainConfig, type BiconomySmartAccountConfig, type BundlerConfig, + EIP_5792_METHODS, + EIP_7702_METHODS, type ISmartAccount, type KernelSmartAccountConfig, type MetamaskSmartAccountConfig, @@ -154,7 +156,19 @@ class AccountAbstractionProvider extends BaseProvider [ + method, + async () => { + throw providerErrors.unsupportedMethod(`${method} is not supported for account abstraction provider`); + }, + ]) + ); + + const eoaMiddleware = providerAsMiddleware(eoaProvider, overrideEip5792And7702MethodsHandlers); const engine = JRPCEngineV2.create({ middleware: [aaMiddleware, eoaMiddleware] }); const provider = providerFromEngineV2(engine); this.updateProviderEngineProxy(provider); diff --git a/packages/no-modal/src/providers/account-abstraction-provider/rpc/ethRpcMiddlewares.ts b/packages/no-modal/src/providers/account-abstraction-provider/rpc/ethRpcMiddlewares.ts index 49d01cda6..6c3095f47 100644 --- a/packages/no-modal/src/providers/account-abstraction-provider/rpc/ethRpcMiddlewares.ts +++ b/packages/no-modal/src/providers/account-abstraction-provider/rpc/ethRpcMiddlewares.ts @@ -1,5 +1,13 @@ import { METHOD_TYPES } from "@toruslabs/ethereum-controllers"; -import { createScaffoldMiddlewareV2, type JRPCRequest, type MiddlewareConstraint, type MiddlewareParams, rpcErrors } from "@web3auth/auth"; +import { + cloneDeep, + createScaffoldMiddlewareV2, + type JRPCRequest, + Json, + type MiddlewareConstraint, + type MiddlewareParams, + rpcErrors, +} from "@web3auth/auth"; import { IProvider } from "../../../base"; import { IEthProviderHandlers, MessageParams, TransactionParams, TypedMessageParams } from "../../ethereum-provider"; @@ -210,8 +218,29 @@ export async function createEoaMiddleware({ aaProvider }: { aaProvider: IProvide }); } -export function providerAsMiddleware(provider: IProvider): MiddlewareConstraint { +/** + * Creates a middleware that forwards the request to the given provider. + * + * If `overrideHandlers` is provided, the middleware will check if the request method is in the `overrideHandlers` object. + * If it is, the middleware will call the handler function with the request and return the result. + * If it is not, the middleware will forward the request to the given provider. + * + * @param provider - The provider to use. + * @param overrideHandlers - The handlers to override. + * @returns The middleware. + */ +export function providerAsMiddleware( + provider: IProvider, + overrideHandlers?: Record) => Promise> +): MiddlewareConstraint { return async ({ request }) => { + if (overrideHandlers) { + const handler = overrideHandlers[request.method as keyof IEthProviderHandlers]; + if (handler) { + const mutableRequest = cloneDeep(request); + return handler(mutableRequest as JRPCRequest) as Promise; + } + } return provider.request({ method: request.method, params: request.params }); }; } From e48f76ff372bf75ae257dd88761f48851dcd0576 Mon Sep 17 00:00:00 2001 From: lwin Date: Thu, 28 May 2026 18:45:19 +0800 Subject: [PATCH 2/8] fix: fix x402 and demo --- .../vue-app-new/src/components/X402Tester.vue | 14 ++-- .../metamask-connector/metamaskConnector.ts | 71 ++++++++++++++----- .../providers/AccountAbstractionProvider.ts | 7 +- 3 files changed, 66 insertions(+), 26 deletions(-) diff --git a/demo/vue-app-new/src/components/X402Tester.vue b/demo/vue-app-new/src/components/X402Tester.vue index 1a152e83d..cd7a23088 100644 --- a/demo/vue-app-new/src/components/X402Tester.vue +++ b/demo/vue-app-new/src/components/X402Tester.vue @@ -4,12 +4,12 @@ import { useSwitchChain } from "@wagmi/vue"; import { useX402Fetch } from "@web3auth/modal/x402/vue"; import { useChain, useWeb3Auth } from "@web3auth/modal/vue"; import { CHAIN_NAMESPACES } from "@web3auth/no-modal"; -import { ref, watch } from "vue"; +import { computed, ref, watch } from "vue"; const BASE_SEPOLIA_CHAIN_ID = "0x14a34"; // 84532 const SOLANA_DEVNET_CHAIN_ID = "0x67"; // 103 const SOLANA_DEVNET_CAIP_CHAIN_ID = `solana:${Number(SOLANA_DEVNET_CHAIN_ID)}`; -const DEFAULT_X402_URL = import.meta.env.VITE_APP_X402_TEST_CONTENT_URL || "https://x402.org/protected"; +const DEFAULT_X402_URL = "https://web3auth-dev-demo-x420.sapphire-dev-2-1.authnetwork.dev"; const { isConnected, connection, web3Auth } = useWeb3Auth(); const { chainId, chainNamespace } = useChain(); @@ -19,6 +19,8 @@ const emit = defineEmits<{ (e: "print-to-console", title: string, payload?: unknown): void; }>(); +const eip155Chains = computed(() => web3Auth.value?.coreOptions.chains?.filter((c) => c.chainNamespace === CHAIN_NAMESPACES.EIP155) || []); + const url = ref(DEFAULT_X402_URL); const fetchLoading = ref(false); @@ -45,9 +47,13 @@ watch( const onSwitchToBaseSepolia = async () => { fetchLoading.value = true; try { - await switchChainAsync({ chainId: parseInt(BASE_SEPOLIA_CHAIN_ID, 16) }); + const newChain = eip155Chains.value.find((c) => c.chainId === BASE_SEPOLIA_CHAIN_ID); + if (!newChain) throw new Error(`Unsupported chainId: ${BASE_SEPOLIA_CHAIN_ID}`); + const data = await switchChainAsync({ chainId: Number(newChain.chainId) }); + emit("print-to-console", "switchedChain", { chainId: data.id }); } catch (err) { - emit("print-to-console", "x402 network error", err instanceof Error ? err.message : String(err)); + console.error("Error", err); + emit("print-to-console", "switchedChain error", err instanceof Error ? err.message : String(err)); } finally { fetchLoading.value = false; } diff --git a/packages/no-modal/src/connectors/metamask-connector/metamaskConnector.ts b/packages/no-modal/src/connectors/metamask-connector/metamaskConnector.ts index a85aa9e77..99d9fed8e 100644 --- a/packages/no-modal/src/connectors/metamask-connector/metamaskConnector.ts +++ b/packages/no-modal/src/connectors/metamask-connector/metamaskConnector.ts @@ -214,7 +214,7 @@ class MetaMaskConnector extends BaseConnector { }, }); - this.evmProvider = this.evmClient.getProvider() as unknown as IProvider; + this.evmProvider = this.createEvmProviderBridge(this.evmClient.getProvider() as unknown as IProvider); } // Create the Solana client only when Solana chains are configured @@ -457,24 +457,7 @@ class MetaMaskConnector extends BaseConnector { throw WalletLoginError.unsupportedOperation("switchChain requires an EVM client, but no EVM chains are configured."); } - const chainConfig = this.coreOptions.chains.find( - (x) => x.chainId === params.chainId && ([CHAIN_NAMESPACES.EIP155] as ChainNamespaceType[]).includes(x.chainNamespace) - ); - - const chainConfiguration = chainConfig - ? { - chainId: params.chainId, - chainName: chainConfig.displayName, - rpcUrls: [chainConfig.rpcTarget], - blockExplorerUrls: chainConfig.blockExplorerUrl ? [chainConfig.blockExplorerUrl] : undefined, - nativeCurrency: { - name: chainConfig.tickerName, - symbol: chainConfig.ticker, - decimals: chainConfig.decimals || 18, - }, - iconUrls: chainConfig.logo ? [chainConfig.logo] : undefined, - } - : undefined; + const chainConfiguration = this.getEvmChainConfiguration(params.chainId); await this.evmClient.switchChain({ chainId: params.chainId as Hex, chainConfiguration }); } @@ -550,6 +533,56 @@ class MetaMaskConnector extends BaseConnector { } await this.initializationPromise; } + + private getEvmChainConfiguration(chainId: string) { + const chainConfig = this.coreOptions.chains.find( + (x) => x.chainId === chainId && ([CHAIN_NAMESPACES.EIP155] as ChainNamespaceType[]).includes(x.chainNamespace) + ); + + return chainConfig + ? { + chainId, + chainName: chainConfig.displayName, + rpcUrls: [chainConfig.rpcTarget], + blockExplorerUrls: chainConfig.blockExplorerUrl ? [chainConfig.blockExplorerUrl] : undefined, + nativeCurrency: { + name: chainConfig.tickerName, + symbol: chainConfig.ticker, + decimals: chainConfig.decimals || 18, + }, + iconUrls: chainConfig.logo ? [chainConfig.logo] : undefined, + } + : undefined; + } + + private createEvmProviderBridge(provider: IProvider): IProvider { + return new Proxy(provider, { + get: (target, prop, receiver) => { + if (prop === "request") { + return async (args: { method: string; params?: S }) => { + // handle `wallet_switchEthereumChain` request from the clients which uses the EVM provider directly (e.g. wagmi actions) + // we already handle this method, `wallet_switchEthereumChain` in other connectors, but metamask lacks of this + if (args?.method === "wallet_switchEthereumChain") { + const chainParams = Array.isArray(args.params) ? args.params[0] : undefined; + const chainId = + chainParams && typeof chainParams === "object" && "chainId" in chainParams ? (chainParams.chainId as string | undefined) : undefined; + + if (chainId && this.evmClient) { + const chainConfiguration = this.getEvmChainConfiguration(chainId); + await this.evmClient.switchChain({ chainId: chainId as Hex, chainConfiguration }); + return null as R; + } + } + + return target.request(args); + }; + } + + const value = Reflect.get(target, prop, receiver); + return typeof value === "function" ? value.bind(target) : value; + }, + }) as IProvider; + } } /** diff --git a/packages/no-modal/src/providers/account-abstraction-provider/providers/AccountAbstractionProvider.ts b/packages/no-modal/src/providers/account-abstraction-provider/providers/AccountAbstractionProvider.ts index 226b3597b..fcd936ae0 100644 --- a/packages/no-modal/src/providers/account-abstraction-provider/providers/AccountAbstractionProvider.ts +++ b/packages/no-modal/src/providers/account-abstraction-provider/providers/AccountAbstractionProvider.ts @@ -82,9 +82,10 @@ class AccountAbstractionProvider extends BaseProvider { - const { currentChain } = this; - const { chainNamespace } = currentChain; - if (chainNamespace !== this.PROVIDER_CHAIN_NAMESPACE) throw WalletInitializationError.incompatibleChainNameSpace("Invalid chain namespace"); + const currentChain = this.currentChain; + const chainNamespace = currentChain?.chainNamespace; + if (chainNamespace && chainNamespace !== this.PROVIDER_CHAIN_NAMESPACE) + throw WalletInitializationError.incompatibleChainNameSpace("Invalid chain namespace"); const bundlerAndPaymasterConfig = this.config.smartAccountChainsConfig.find((config) => config.chainId === currentChain.chainId); if (!bundlerAndPaymasterConfig) throw WalletInitializationError.invalidProviderConfigError(`Bundler and paymaster config not found for chain ${currentChain.chainId}`); From 0f8553a3d5b57f8f1dd767da4e5c6f3308d3ef87 Mon Sep 17 00:00:00 2001 From: lwin Date: Thu, 28 May 2026 23:25:08 +0800 Subject: [PATCH 3/8] chore: addressed comments --- .../metamask-connector/metamaskConnector.ts | 6 ++- .../providers/AccountAbstractionProvider.ts | 29 +++++--------- .../rpc/ethRpcMiddlewares.ts | 40 +++++++------------ 3 files changed, 28 insertions(+), 47 deletions(-) diff --git a/packages/no-modal/src/connectors/metamask-connector/metamaskConnector.ts b/packages/no-modal/src/connectors/metamask-connector/metamaskConnector.ts index 99d9fed8e..d0b5c1c10 100644 --- a/packages/no-modal/src/connectors/metamask-connector/metamaskConnector.ts +++ b/packages/no-modal/src/connectors/metamask-connector/metamaskConnector.ts @@ -557,7 +557,7 @@ class MetaMaskConnector extends BaseConnector { private createEvmProviderBridge(provider: IProvider): IProvider { return new Proxy(provider, { - get: (target, prop, receiver) => { + get: (target, prop) => { if (prop === "request") { return async (args: { method: string; params?: S }) => { // handle `wallet_switchEthereumChain` request from the clients which uses the EVM provider directly (e.g. wagmi actions) @@ -578,7 +578,9 @@ class MetaMaskConnector extends BaseConnector { }; } - const value = Reflect.get(target, prop, receiver); + // Use the original provider as the receiver so SDK getters that rely on + // private fields (`#field`) keep the correct brand check context. + const value = Reflect.get(target, prop, target); return typeof value === "function" ? value.bind(target) : value; }, }) as IProvider; diff --git a/packages/no-modal/src/providers/account-abstraction-provider/providers/AccountAbstractionProvider.ts b/packages/no-modal/src/providers/account-abstraction-provider/providers/AccountAbstractionProvider.ts index fcd936ae0..d87ae4b48 100644 --- a/packages/no-modal/src/providers/account-abstraction-provider/providers/AccountAbstractionProvider.ts +++ b/packages/no-modal/src/providers/account-abstraction-provider/providers/AccountAbstractionProvider.ts @@ -2,8 +2,6 @@ import { type AccountAbstractionMultiChainConfig, type BiconomySmartAccountConfig, type BundlerConfig, - EIP_5792_METHODS, - EIP_7702_METHODS, type ISmartAccount, type KernelSmartAccountConfig, type MetamaskSmartAccountConfig, @@ -20,7 +18,7 @@ import { type BundlerClient, createBundlerClient, createPaymasterClient, type Pa import { CHAIN_NAMESPACES, type CustomChainConfig, type IProvider, WalletInitializationError } from "../../../base"; import { BaseProvider, type BaseProviderConfig, type BaseProviderState } from "../../../providers/base-provider"; -import { createAaMiddleware, createEoaMiddleware, providerAsMiddleware } from "../rpc/ethRpcMiddlewares"; +import { createAaMiddleware, createEip7702And5792MiddlewareForAaProvider, createEoaMiddleware, providerAsMiddleware } from "../rpc/ethRpcMiddlewares"; import { getProviderHandlers } from "./utils"; interface AccountAbstractionProviderConfig extends BaseProviderConfig { @@ -82,10 +80,9 @@ class AccountAbstractionProvider extends BaseProvider { - const currentChain = this.currentChain; - const chainNamespace = currentChain?.chainNamespace; - if (chainNamespace && chainNamespace !== this.PROVIDER_CHAIN_NAMESPACE) - throw WalletInitializationError.incompatibleChainNameSpace("Invalid chain namespace"); + const { currentChain } = this; + const { chainNamespace } = currentChain; + if (chainNamespace !== this.PROVIDER_CHAIN_NAMESPACE) throw WalletInitializationError.incompatibleChainNameSpace("Invalid chain namespace"); const bundlerAndPaymasterConfig = this.config.smartAccountChainsConfig.find((config) => config.chainId === currentChain.chainId); if (!bundlerAndPaymasterConfig) throw WalletInitializationError.invalidProviderConfigError(`Bundler and paymaster config not found for chain ${currentChain.chainId}`); @@ -158,19 +155,11 @@ class AccountAbstractionProvider extends BaseProvider [ - method, - async () => { - throw providerErrors.unsupportedMethod(`${method} is not supported for account abstraction provider`); - }, - ]) - ); - - const eoaMiddleware = providerAsMiddleware(eoaProvider, overrideEip5792And7702MethodsHandlers); - const engine = JRPCEngineV2.create({ middleware: [aaMiddleware, eoaMiddleware] }); + // middleware to handle EIP-7702 and EIP-5792 methods, + // currently, we do not support EIP-7702 and EIP-5792 methods for account abstraction provider + const eip7702And5792Middleware = await createEip7702And5792MiddlewareForAaProvider(); + const eoaMiddleware = providerAsMiddleware(eoaProvider); + const engine = JRPCEngineV2.create({ middleware: [aaMiddleware, eip7702And5792Middleware, eoaMiddleware] }); const provider = providerFromEngineV2(engine); this.updateProviderEngineProxy(provider); eoaProvider.once("chainChanged", (chainId) => { diff --git a/packages/no-modal/src/providers/account-abstraction-provider/rpc/ethRpcMiddlewares.ts b/packages/no-modal/src/providers/account-abstraction-provider/rpc/ethRpcMiddlewares.ts index 6c3095f47..d74b139a4 100644 --- a/packages/no-modal/src/providers/account-abstraction-provider/rpc/ethRpcMiddlewares.ts +++ b/packages/no-modal/src/providers/account-abstraction-provider/rpc/ethRpcMiddlewares.ts @@ -1,11 +1,10 @@ -import { METHOD_TYPES } from "@toruslabs/ethereum-controllers"; +import { EIP_5792_METHODS, EIP_7702_METHODS, METHOD_TYPES } from "@toruslabs/ethereum-controllers"; import { - cloneDeep, createScaffoldMiddlewareV2, type JRPCRequest, - Json, type MiddlewareConstraint, type MiddlewareParams, + providerErrors, rpcErrors, } from "@web3auth/auth"; @@ -218,29 +217,20 @@ export async function createEoaMiddleware({ aaProvider }: { aaProvider: IProvide }); } -/** - * Creates a middleware that forwards the request to the given provider. - * - * If `overrideHandlers` is provided, the middleware will check if the request method is in the `overrideHandlers` object. - * If it is, the middleware will call the handler function with the request and return the result. - * If it is not, the middleware will forward the request to the given provider. - * - * @param provider - The provider to use. - * @param overrideHandlers - The handlers to override. - * @returns The middleware. - */ -export function providerAsMiddleware( - provider: IProvider, - overrideHandlers?: Record) => Promise> -): MiddlewareConstraint { - return async ({ request }) => { - if (overrideHandlers) { - const handler = overrideHandlers[request.method as keyof IEthProviderHandlers]; - if (handler) { - const mutableRequest = cloneDeep(request); - return handler(mutableRequest as JRPCRequest) as Promise; - } +export async function createEip7702And5792MiddlewareForAaProvider(): Promise { + const eip5792Methods = Object.values(EIP_5792_METHODS); + const eip7702Methods = Object.values(EIP_7702_METHODS); + const eip7702And5792Methods: string[] = [...eip5792Methods, ...eip7702Methods]; + return async ({ request, next }) => { + if (eip7702And5792Methods.includes(request.method as string)) { + throw providerErrors.unsupportedMethod(`${request.method} is not supported for account abstraction provider`); } + return next(request); + }; +} + +export function providerAsMiddleware(provider: IProvider): MiddlewareConstraint { + return async ({ request }) => { return provider.request({ method: request.method, params: request.params }); }; } From ce3efcb1ab27a8ad4a9cf4428a30e809b5e47e42 Mon Sep 17 00:00:00 2001 From: lwin Date: Fri, 29 May 2026 18:46:49 +0800 Subject: [PATCH 4/8] feat: added SwitchChainMiddleware for metamaskConnector --- .../metamask-connector/metamaskConnector.ts | 64 +++++++++++-------- .../providers/AccountAbstractionProvider.ts | 5 +- 2 files changed, 43 insertions(+), 26 deletions(-) diff --git a/packages/no-modal/src/connectors/metamask-connector/metamaskConnector.ts b/packages/no-modal/src/connectors/metamask-connector/metamaskConnector.ts index d0b5c1c10..9b5339176 100644 --- a/packages/no-modal/src/connectors/metamask-connector/metamaskConnector.ts +++ b/packages/no-modal/src/connectors/metamask-connector/metamaskConnector.ts @@ -5,6 +5,15 @@ import { getErrorAnalyticsProperties, signChallenge } from "@toruslabs/base-cont import { bytesToHexPrefixedString, utf8ToBytes } from "@toruslabs/metadata-helpers"; import type { Wallet } from "@wallet-standard/base"; import { StandardConnect, StandardConnectFeature } from "@wallet-standard/features"; +import { + createScaffoldMiddlewareV2, + JRPCEngineV2, + type JRPCRequest, + type MiddlewareConstraint, + type MiddlewareParams, + providerFromEngineV2, + rpcErrors, +} from "@web3auth/auth"; import { EVM_METHOD_TYPES } from "@web3auth/ws-embed"; import { generateSiweNonce } from "viem/siwe"; @@ -556,34 +565,39 @@ class MetaMaskConnector extends BaseConnector { } private createEvmProviderBridge(provider: IProvider): IProvider { - return new Proxy(provider, { - get: (target, prop) => { - if (prop === "request") { - return async (args: { method: string; params?: S }) => { - // handle `wallet_switchEthereumChain` request from the clients which uses the EVM provider directly (e.g. wagmi actions) - // we already handle this method, `wallet_switchEthereumChain` in other connectors, but metamask lacks of this - if (args?.method === "wallet_switchEthereumChain") { - const chainParams = Array.isArray(args.params) ? args.params[0] : undefined; - const chainId = - chainParams && typeof chainParams === "object" && "chainId" in chainParams ? (chainParams.chainId as string | undefined) : undefined; - - if (chainId && this.evmClient) { - const chainConfiguration = this.getEvmChainConfiguration(chainId); - await this.evmClient.switchChain({ chainId: chainId as Hex, chainConfiguration }); - return null as R; - } - } + const switchChainMiddleware = createScaffoldMiddlewareV2({ + wallet_switchEthereumChain: async (params: MiddlewareParams>): Promise => { + const chainParams = params.request.params?.length ? params.request.params[0] : undefined; + const chainId = chainParams?.chainId; - return target.request(args); - }; - } + if (!chainId) throw rpcErrors.invalidParams("Missing chainId"); + if (!this.evmClient) throw WalletLoginError.unsupportedOperation("MetaMask EVM client is not initialized"); + + const chainConfiguration = this.getEvmChainConfiguration(chainId); + await this.evmClient.switchChain({ chainId: chainId as Hex, chainConfiguration }); + return null; + }, + }); + const forwardMiddleware: MiddlewareConstraint = async ({ request }) => { + return provider.request({ method: request.method, params: request.params }); + }; + const engine = JRPCEngineV2.create({ middleware: [switchChainMiddleware, forwardMiddleware] }); + const engineProvider = providerFromEngineV2(engine) as IProvider; - // Use the original provider as the receiver so SDK getters that rely on - // private fields (`#field`) keep the correct brand check context. - const value = Reflect.get(target, prop, target); - return typeof value === "function" ? value.bind(target) : value; + return { + get chainId() { + return provider.chainId; }, - }) as IProvider; + request: engineProvider.request.bind(engineProvider), + sendAsync: engineProvider.sendAsync.bind(engineProvider), + send: engineProvider.send.bind(engineProvider), + on: provider.on.bind(provider), + once: provider.once.bind(provider), + removeListener: provider.removeListener.bind(provider), + off: typeof provider.off === "function" ? provider.off.bind(provider) : undefined, + emit: typeof provider.emit === "function" ? provider.emit.bind(provider) : undefined, + removeAllListeners: typeof provider.removeAllListeners === "function" ? provider.removeAllListeners.bind(provider) : undefined, + } as IProvider; } } diff --git a/packages/no-modal/src/providers/account-abstraction-provider/providers/AccountAbstractionProvider.ts b/packages/no-modal/src/providers/account-abstraction-provider/providers/AccountAbstractionProvider.ts index d87ae4b48..75de666f8 100644 --- a/packages/no-modal/src/providers/account-abstraction-provider/providers/AccountAbstractionProvider.ts +++ b/packages/no-modal/src/providers/account-abstraction-provider/providers/AccountAbstractionProvider.ts @@ -80,7 +80,10 @@ class AccountAbstractionProvider extends BaseProvider { - const { currentChain } = this; + const currentChain = this.currentChain; + if (!currentChain) { + throw WalletInitializationError.invalidProviderConfigError(`AA chain config not found for chain ${this.chainId}`); + } const { chainNamespace } = currentChain; if (chainNamespace !== this.PROVIDER_CHAIN_NAMESPACE) throw WalletInitializationError.incompatibleChainNameSpace("Invalid chain namespace"); const bundlerAndPaymasterConfig = this.config.smartAccountChainsConfig.find((config) => config.chainId === currentChain.chainId); From 3725c909f99de43c4b1952158f0d2d7327ccd154 Mon Sep 17 00:00:00 2001 From: lwin Date: Fri, 29 May 2026 22:10:21 +0800 Subject: [PATCH 5/8] chore: addressed comments --- .../metamask-connector/metamaskConnector.ts | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/packages/no-modal/src/connectors/metamask-connector/metamaskConnector.ts b/packages/no-modal/src/connectors/metamask-connector/metamaskConnector.ts index 9b5339176..118f72ba5 100644 --- a/packages/no-modal/src/connectors/metamask-connector/metamaskConnector.ts +++ b/packages/no-modal/src/connectors/metamask-connector/metamaskConnector.ts @@ -584,20 +584,7 @@ class MetaMaskConnector extends BaseConnector { const engine = JRPCEngineV2.create({ middleware: [switchChainMiddleware, forwardMiddleware] }); const engineProvider = providerFromEngineV2(engine) as IProvider; - return { - get chainId() { - return provider.chainId; - }, - request: engineProvider.request.bind(engineProvider), - sendAsync: engineProvider.sendAsync.bind(engineProvider), - send: engineProvider.send.bind(engineProvider), - on: provider.on.bind(provider), - once: provider.once.bind(provider), - removeListener: provider.removeListener.bind(provider), - off: typeof provider.off === "function" ? provider.off.bind(provider) : undefined, - emit: typeof provider.emit === "function" ? provider.emit.bind(provider) : undefined, - removeAllListeners: typeof provider.removeAllListeners === "function" ? provider.removeAllListeners.bind(provider) : undefined, - } as IProvider; + return engineProvider; } } From b199357438a5fc6478d0a831d59d33e0050bd425 Mon Sep 17 00:00:00 2001 From: lwin Date: Fri, 29 May 2026 22:25:33 +0800 Subject: [PATCH 6/8] fix: fixed metamask evm provider binding for event listeners --- .../metamask-connector/metamaskConnector.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/no-modal/src/connectors/metamask-connector/metamaskConnector.ts b/packages/no-modal/src/connectors/metamask-connector/metamaskConnector.ts index 118f72ba5..2fab8ffa9 100644 --- a/packages/no-modal/src/connectors/metamask-connector/metamaskConnector.ts +++ b/packages/no-modal/src/connectors/metamask-connector/metamaskConnector.ts @@ -584,7 +584,17 @@ class MetaMaskConnector extends BaseConnector { const engine = JRPCEngineV2.create({ middleware: [switchChainMiddleware, forwardMiddleware] }); const engineProvider = providerFromEngineV2(engine) as IProvider; - return engineProvider; + return { + ...engineProvider, + // bind the provider events to the engine provider + // without the binding, the engine provider could not forward events to the original provider + on: provider.on.bind(provider), + once: provider.once.bind(provider), + removeListener: provider.removeListener.bind(provider), + off: typeof provider.off === "function" ? provider.off.bind(provider) : undefined, + emit: typeof provider.emit === "function" ? provider.emit.bind(provider) : undefined, + removeAllListeners: typeof provider.removeAllListeners === "function" ? provider.removeAllListeners.bind(provider) : undefined, + } as IProvider; } } From c4980aebe22903b0569854b810d703f3c9d4ef4a Mon Sep 17 00:00:00 2001 From: lwin Date: Sat, 30 May 2026 01:10:22 +0800 Subject: [PATCH 7/8] chore: address cursor comment --- .../src/connectors/metamask-connector/metamaskConnector.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/no-modal/src/connectors/metamask-connector/metamaskConnector.ts b/packages/no-modal/src/connectors/metamask-connector/metamaskConnector.ts index 2fab8ffa9..79bcf08c0 100644 --- a/packages/no-modal/src/connectors/metamask-connector/metamaskConnector.ts +++ b/packages/no-modal/src/connectors/metamask-connector/metamaskConnector.ts @@ -586,6 +586,10 @@ class MetaMaskConnector extends BaseConnector { return { ...engineProvider, + // get the chainId from the original provider (i.e. metamask) + get chainId() { + return provider.chainId; + }, // bind the provider events to the engine provider // without the binding, the engine provider could not forward events to the original provider on: provider.on.bind(provider), From e9312b1bddd0adf6207c7ac9308f6d7aebbc757e Mon Sep 17 00:00:00 2001 From: lwin Date: Sat, 30 May 2026 16:31:42 +0800 Subject: [PATCH 8/8] chore: clean events on bridge metamask connector --- .../metamask-connector/metamaskConnector.ts | 52 ++++++++++++++----- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/packages/no-modal/src/connectors/metamask-connector/metamaskConnector.ts b/packages/no-modal/src/connectors/metamask-connector/metamaskConnector.ts index 79bcf08c0..bf8abaf40 100644 --- a/packages/no-modal/src/connectors/metamask-connector/metamaskConnector.ts +++ b/packages/no-modal/src/connectors/metamask-connector/metamaskConnector.ts @@ -43,6 +43,7 @@ import { getSolanaChainByChainConfig, type IProvider, isUserRejectedError, + type ProviderEvents, type UserInfo, WALLET_CONNECTOR_TYPE, WALLET_CONNECTORS, @@ -77,6 +78,8 @@ export interface MetaMaskConnectorOptions extends BaseConnectorSettings { connectorSettings?: MetaMaskConnectorSettings; } +const EVM_PROVIDER_EVENTS = ["accountsChanged", "chainChanged", "connect", "disconnect"] as const satisfies (keyof ProviderEvents)[]; + class MetaMaskConnector extends BaseConnector { readonly connectorNamespace: ConnectorNamespaceType = CONNECTOR_NAMESPACES.MULTICHAIN; @@ -104,6 +107,8 @@ class MetaMaskConnector extends BaseConnector { private analytics?: Analytics; + private evmProviderEventBridgeRemovers: (() => void)[] = []; + constructor(connectorOptions: MetaMaskConnectorOptions) { super(connectorOptions); this.connectorSettings = connectorOptions.connectorSettings; @@ -303,7 +308,7 @@ class MetaMaskConnector extends BaseConnector { this.emit(CONNECTOR_EVENTS.CONNECTING, { connector: WALLET_CONNECTORS.METAMASK }); const evmConnectedPromise = new Promise((resolve) => { - if (this.evmClient.status === "connected") { + if (!this.evmClient || this.evmClient.status === "connected") { resolve(); } else { // Wait for EVM provider to be ready @@ -401,6 +406,7 @@ class MetaMaskConnector extends BaseConnector { this.initializationPromise = null; this.multichainClient = null; this.evmClient = null; + this.clearEvmProviderEventBridges(); this.evmProvider = null; this.solanaClient = null; this.solanaProvider = null; @@ -584,21 +590,39 @@ class MetaMaskConnector extends BaseConnector { const engine = JRPCEngineV2.create({ middleware: [switchChainMiddleware, forwardMiddleware] }); const engineProvider = providerFromEngineV2(engine) as IProvider; - return { - ...engineProvider, - // get the chainId from the original provider (i.e. metamask) - get chainId() { + // providerFromEngineV2 wraps requests with its own event emitter, while MetaMask + // emits wallet state changes on the original provider. + // so we need to bridge the events from the original provider to the engine provider + this.bridgeProviderEvents(provider, engineProvider); + + Object.defineProperty(engineProvider, "chainId", { + configurable: true, + enumerable: true, + get() { return provider.chainId; }, - // bind the provider events to the engine provider - // without the binding, the engine provider could not forward events to the original provider - on: provider.on.bind(provider), - once: provider.once.bind(provider), - removeListener: provider.removeListener.bind(provider), - off: typeof provider.off === "function" ? provider.off.bind(provider) : undefined, - emit: typeof provider.emit === "function" ? provider.emit.bind(provider) : undefined, - removeAllListeners: typeof provider.removeAllListeners === "function" ? provider.removeAllListeners.bind(provider) : undefined, - } as IProvider; + }); + + return engineProvider; + } + + private bridgeProviderEvents(sourceProvider: IProvider, targetProvider: IProvider): void { + // clean up any existing event bridges before creating new ones + this.clearEvmProviderEventBridges(); + this.evmProviderEventBridgeRemovers = EVM_PROVIDER_EVENTS.map((event) => this.bridgeProviderEvent(sourceProvider, targetProvider, event)); + } + + private bridgeProviderEvent(sourceProvider: IProvider, targetProvider: IProvider, event: K): () => void { + const handler = ((...args: Parameters) => { + targetProvider.emit(event, ...args); + }) as unknown as ProviderEvents[K]; + sourceProvider.on(event, handler); + return () => sourceProvider.removeListener(event, handler); + } + + private clearEvmProviderEventBridges(): void { + this.evmProviderEventBridgeRemovers.forEach((remove) => remove()); + this.evmProviderEventBridgeRemovers = []; } }