diff --git a/.changeset/afraid-cows-mix.md b/.changeset/afraid-cows-mix.md new file mode 100644 index 0000000000..243c2e53e2 --- /dev/null +++ b/.changeset/afraid-cows-mix.md @@ -0,0 +1,5 @@ +--- +"@wagmi/connectors": patch +--- + +Improved MetaMask chain switching behavior. diff --git a/packages/connectors/package.json b/packages/connectors/package.json index 924d90a770..57f953281a 100644 --- a/packages/connectors/package.json +++ b/packages/connectors/package.json @@ -46,7 +46,7 @@ }, "dependencies": { "@coinbase/wallet-sdk": "4.2.3", - "@metamask/sdk": "0.31.2", + "@metamask/sdk": "0.31.4", "@safe-global/safe-apps-provider": "0.18.5", "@safe-global/safe-apps-sdk": "9.1.0", "@walletconnect/ethereum-provider": "2.17.0", diff --git a/packages/connectors/src/metaMask.ts b/packages/connectors/src/metaMask.ts index e2214b6494..50f81e68ab 100644 --- a/packages/connectors/src/metaMask.ts +++ b/packages/connectors/src/metaMask.ts @@ -329,97 +329,75 @@ export function metaMask(parameters: MetaMaskParameters = {}) { })() // Avoid back and forth on mobile by using `'wallet_addEthereumChain'` for non-default chains - if (!isDefaultChain) - try { - const blockExplorerUrls = (() => { - const { default: blockExplorer, ...blockExplorers } = - chain.blockExplorers ?? {} - if (addEthereumChainParameter?.blockExplorerUrls) - return addEthereumChainParameter.blockExplorerUrls - if (blockExplorer) - return [ - blockExplorer.url, - ...Object.values(blockExplorers).map((x) => x.url), - ] - return - })() - - const rpcUrls = (() => { - if (addEthereumChainParameter?.rpcUrls?.length) - return addEthereumChainParameter.rpcUrls - return [chain.rpcUrls.default?.http[0] ?? ''] - })() - + try { + if (!isDefaultChain) await provider.request({ method: 'wallet_addEthereumChain', params: [ { - blockExplorerUrls, + blockExplorerUrls: (() => { + const { default: blockExplorer, ...blockExplorers } = + chain.blockExplorers ?? {} + if (addEthereumChainParameter?.blockExplorerUrls) + return addEthereumChainParameter.blockExplorerUrls + if (blockExplorer) + return [ + blockExplorer.url, + ...Object.values(blockExplorers).map((x) => x.url), + ] + return + })(), chainId: numberToHex(chainId), chainName: addEthereumChainParameter?.chainName ?? chain.name, iconUrls: addEthereumChainParameter?.iconUrls, nativeCurrency: addEthereumChainParameter?.nativeCurrency ?? chain.nativeCurrency, - rpcUrls, + rpcUrls: (() => { + if (addEthereumChainParameter?.rpcUrls?.length) + return addEthereumChainParameter.rpcUrls + return [chain.rpcUrls.default?.http[0] ?? ''] + })(), } satisfies AddEthereumChainParameter, ], }) + else + await provider.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: numberToHex(chainId) }], + }) + + // During `'wallet_switchEthereumChain'`, MetaMask makes a `'net_version'` RPC call to the target chain. + // If this request fails, MetaMask does not emit the `'chainChanged'` event, but will still switch the chain. + // To counter this behavior, we request and emit the current chain ID to confirm the chain switch either via + // this callback or an externally emitted `'chainChanged'` event. + // https://github.com/MetaMask/metamask-extension/issues/24247 + await waitForChainIdToSync() + await sendAndWaitForChangeEvent(chainId) + async function waitForChainIdToSync() { // On mobile, there is a race condition between the result of `'wallet_addEthereumChain'` and `'eth_chainId'`. - // (`'eth_chainId'` from the MetaMask relay server). // To avoid this, we wait for `'eth_chainId'` to return the expected chain ID with a retry loop. - let retryCount = 0 - const currentChainId = await withRetry( + await withRetry( async () => { - retryCount += 1 const value = hexToNumber( // `'eth_chainId'` is cached by the MetaMask SDK side to avoid unnecessary deeplinks (await provider.request({ method: 'eth_chainId' })) as Hex, ) - if (value !== chainId) { - if (retryCount === 5) return -1 - // `value` doesn't match expected `chainId`, throw to trigger retry - throw new Error('Chain ID mismatch') - } + // `value` doesn't match expected `chainId`, throw to trigger retry + if (value !== chainId) + throw new Error('User rejected switch after adding network.') return value }, { - delay: 100, - retryCount: 5, // android device encryption is slower + delay: 50, + retryCount: 20, // android device encryption is slower }, ) - - if (currentChainId !== chainId) - throw new Error('User rejected switch after adding network.') - - return chain - } catch (err) { - const error = err as RpcError - if (error.code === UserRejectedRequestError.code) - throw new UserRejectedRequestError(error) - throw new SwitchChainError(error) } - // Use to `'wallet_switchEthereumChain'` for default chains - try { - await Promise.all([ - provider - .request({ - method: 'wallet_switchEthereumChain', - params: [{ chainId: numberToHex(chainId) }], - }) - // During `'wallet_switchEthereumChain'`, MetaMask makes a `'net_version'` RPC call to the target chain. - // If this request fails, MetaMask does not emit the `'chainChanged'` event, but will still switch the chain. - // To counter this behavior, we request and emit the current chain ID to confirm the chain switch either via - // this callback or an externally emitted `'chainChanged'` event. - // https://github.com/MetaMask/metamask-extension/issues/24247 - .then(async () => { - const currentChainId = await this.getChainId() - if (currentChainId === chainId) - config.emitter.emit('change', { chainId }) - }), - new Promise((resolve) => { + async function sendAndWaitForChangeEvent(chainId: number) { + await new Promise((resolve) => { const listener = ((data) => { if ('chainId' in data && data.chainId === chainId) { config.emitter.off('change', listener) @@ -427,8 +405,10 @@ export function metaMask(parameters: MetaMaskParameters = {}) { } }) satisfies Parameters[1] config.emitter.on('change', listener) - }), - ]) + config.emitter.emit('change', { chainId }) + }) + } + return chain } catch (err) { const error = err as RpcError diff --git a/playgrounds/vite-vue/src/wagmi.ts b/playgrounds/vite-vue/src/wagmi.ts index f0282e9490..f5b3f286cc 100644 --- a/playgrounds/vite-vue/src/wagmi.ts +++ b/playgrounds/vite-vue/src/wagmi.ts @@ -1,6 +1,6 @@ import { http, createConfig, createStorage } from '@wagmi/vue' import { mainnet, optimism, sepolia } from '@wagmi/vue/chains' -import { coinbaseWallet, walletConnect } from '@wagmi/vue/connectors' +import { coinbaseWallet, metaMask, walletConnect } from '@wagmi/vue/connectors' export const config = createConfig({ chains: [mainnet, sepolia, optimism], @@ -9,6 +9,7 @@ export const config = createConfig({ projectId: import.meta.env.VITE_WC_PROJECT_ID, }), coinbaseWallet({ appName: 'Vite Vue Playground', darkMode: true }), + metaMask(), ], storage: createStorage({ storage: localStorage, key: 'vite-vue' }), transports: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5f79c39cb7..fd3c355fc4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,39 +4,6 @@ settings: autoInstallPeers: false excludeLinksFromLockfile: false -catalogs: - default: - '@tanstack/query-core': - specifier: 5.49.1 - version: 5.49.1 - '@tanstack/react-query': - specifier: 5.49.2 - version: 5.49.2 - '@tanstack/vue-query': - specifier: 5.49.1 - version: 5.49.1 - '@testing-library/dom': - specifier: 10.4.0 - version: 10.4.0 - '@testing-library/react': - specifier: 16.0.1 - version: 16.0.1 - '@types/react': - specifier: 18.3.1 - version: 18.3.1 - '@types/react-dom': - specifier: 18.3.0 - version: 18.3.0 - react: - specifier: 18.3.1 - version: 18.3.1 - react-dom: - specifier: 18.3.1 - version: 18.3.1 - vue: - specifier: 3.4.27 - version: 3.4.27 - importers: .: @@ -175,8 +142,8 @@ importers: specifier: 4.2.3 version: 4.2.3 '@metamask/sdk': - specifier: 0.31.2 - version: 0.31.2(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) + specifier: 0.31.4 + version: 0.31.4(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) '@safe-global/safe-apps-provider': specifier: 0.18.5 version: 0.18.5(bufferutil@4.0.8)(encoding@0.1.13)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4) @@ -906,10 +873,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.23.4': - resolution: {integrity: sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg==} - engines: {node: '>=6.9.0'} - '@babel/runtime@7.26.0': resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} engines: {node: '>=6.9.0'} @@ -1765,8 +1728,8 @@ packages: '@metamask/sdk-install-modal-web@0.31.2': resolution: {integrity: sha512-KPv36kQjmTwErU8g2neuHHSgkD5+1hp4D6ERfk5Kc2r73aOYNCdG9wDGRUmFmcY2MKkeK1EuDyZfJ4FPU30fxQ==} - '@metamask/sdk@0.31.2': - resolution: {integrity: sha512-6MWON2g1j7XwAHWam4trusGxeyhQweNLEHPsfuIxSwcsXoEm08Jj80OglJxQI4KwjcDnjSWBkQGG3mmK6ug/cA==} + '@metamask/sdk@0.31.4': + resolution: {integrity: sha512-HLUN4IZGdyiy5YeebXmXi+ndpmrl6zslCQLdR2QHplIy4JmUL/eDyKNFiK7eBLVKXVVIDYFIb6g1iSEb+i8Kew==} '@metamask/utils@5.0.2': resolution: {integrity: sha512-yfmE79bRQtnMzarnKfX7AEJBwFTxvTyw3nBQlu/5rmGXrjAeAMltoGxO62TFurxrQAFMNa/fEjIHNvungZp0+g==} @@ -7460,9 +7423,6 @@ packages: tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} - tslib@2.5.0: - resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} - tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -8285,6 +8245,39 @@ packages: zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} +catalogs: + default: + '@tanstack/query-core': + specifier: 5.49.1 + version: 5.49.1 + '@tanstack/react-query': + specifier: 5.49.2 + version: 5.49.2 + '@tanstack/vue-query': + specifier: 5.49.1 + version: 5.49.1 + '@testing-library/dom': + specifier: 10.4.0 + version: 10.4.0 + '@testing-library/react': + specifier: 16.0.1 + version: 16.0.1 + '@types/react': + specifier: 18.3.1 + version: 18.3.1 + '@types/react-dom': + specifier: 18.3.0 + version: 18.3.0 + react: + specifier: 18.3.1 + version: 18.3.1 + react-dom: + specifier: 18.3.1 + version: 18.3.1 + vue: + specifier: 3.4.27 + version: 3.4.27 + snapshots: '@adraffy/ens-normalize@1.10.0': {} @@ -8656,10 +8649,6 @@ snapshots: '@babel/plugin-transform-modules-commonjs': 7.24.1(@babel/core@7.24.5) '@babel/plugin-transform-typescript': 7.24.5(@babel/core@7.24.5) - '@babel/runtime@7.23.4': - dependencies: - regenerator-runtime: 0.14.0 - '@babel/runtime@7.26.0': dependencies: regenerator-runtime: 0.14.0 @@ -9453,7 +9442,7 @@ snapshots: '@manypkg/get-packages@1.1.3': dependencies: - '@babel/runtime': 7.23.4 + '@babel/runtime': 7.26.0 '@changesets/types': 4.1.0 '@manypkg/find-root': 1.1.0 fs-extra: 8.1.0 @@ -9574,7 +9563,7 @@ snapshots: dependencies: '@paulmillr/qr': 0.2.1 - '@metamask/sdk@0.31.2(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)': + '@metamask/sdk@0.31.4(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)': dependencies: '@babel/runtime': 7.26.0 '@metamask/onboarding': 1.0.1 @@ -10976,7 +10965,7 @@ snapshots: '@swc/helpers@0.5.5': dependencies: '@swc/counter': 0.1.3 - tslib: 2.5.0 + tslib: 2.8.1 '@tanstack/match-sorter-utils@8.15.1': dependencies: @@ -16984,8 +16973,6 @@ snapshots: tslib@1.14.1: {} - tslib@2.5.0: {} - tslib@2.8.1: {} tsort@0.0.1: {}