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..bf8abaf40 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"; @@ -34,6 +43,7 @@ import { getSolanaChainByChainConfig, type IProvider, isUserRejectedError, + type ProviderEvents, type UserInfo, WALLET_CONNECTOR_TYPE, WALLET_CONNECTORS, @@ -68,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; @@ -95,6 +107,8 @@ class MetaMaskConnector extends BaseConnector { private analytics?: Analytics; + private evmProviderEventBridgeRemovers: (() => void)[] = []; + constructor(connectorOptions: MetaMaskConnectorOptions) { super(connectorOptions); this.connectorSettings = connectorOptions.connectorSettings; @@ -214,7 +228,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 @@ -294,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 @@ -392,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; @@ -457,24 +472,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 +548,82 @@ 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 { + const switchChainMiddleware = createScaffoldMiddlewareV2({ + wallet_switchEthereumChain: async (params: MiddlewareParams>): Promise => { + const chainParams = params.request.params?.length ? params.request.params[0] : undefined; + const chainId = chainParams?.chainId; + + 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; + + // 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; + }, + }); + + 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 = []; + } } /** 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..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 @@ -18,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 { @@ -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); @@ -154,8 +157,12 @@ class AccountAbstractionProvider extends BaseProvider { 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..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,5 +1,12 @@ -import { METHOD_TYPES } from "@toruslabs/ethereum-controllers"; -import { createScaffoldMiddlewareV2, type JRPCRequest, type MiddlewareConstraint, type MiddlewareParams, rpcErrors } from "@web3auth/auth"; +import { EIP_5792_METHODS, EIP_7702_METHODS, METHOD_TYPES } from "@toruslabs/ethereum-controllers"; +import { + createScaffoldMiddlewareV2, + type JRPCRequest, + type MiddlewareConstraint, + type MiddlewareParams, + providerErrors, + rpcErrors, +} from "@web3auth/auth"; import { IProvider } from "../../../base"; import { IEthProviderHandlers, MessageParams, TransactionParams, TypedMessageParams } from "../../ethereum-provider"; @@ -210,6 +217,18 @@ export async function createEoaMiddleware({ aaProvider }: { aaProvider: IProvide }); } +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 });