From 2313ccc1f07b8e8dec5641f565f4b77aaf07c892 Mon Sep 17 00:00:00 2001 From: Edouard Bougon <15703023+EdouardBougon@users.noreply.github.com> Date: Thu, 19 Dec 2024 17:34:18 +0100 Subject: [PATCH 1/6] fix: update metamask sdk --- packages/connectors/package.json | 2 +- pnpm-lock.yaml | 93 ++++++++++++++------------------ 2 files changed, 41 insertions(+), 54 deletions(-) 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/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: {} From 2740398818607e5da8b687c4ab214e4858b4f70b Mon Sep 17 00:00:00 2001 From: Baptiste Marchand <75846779+baptiste-marchand@users.noreply.github.com> Date: Thu, 19 Dec 2024 19:20:46 +0100 Subject: [PATCH 2/6] Refactor: MetaMask switchNetwork consistently emit a 'change' event --- packages/connectors/src/metaMask.ts | 102 ++++++++++++++-------------- 1 file changed, 50 insertions(+), 52 deletions(-) diff --git a/packages/connectors/src/metaMask.ts b/packages/connectors/src/metaMask.ts index e2214b6494..35f20e4fab 100644 --- a/packages/connectors/src/metaMask.ts +++ b/packages/connectors/src/metaMask.ts @@ -315,6 +315,41 @@ export function metaMask(parameters: MetaMaskParameters = {}) { const chain = config.chains.find((x) => x.id === chainId) if (!chain) throw new SwitchChainError(new ChainNotConfiguredError()) + const sendAndWaitForChangeEvent = async (chainId: number) => { + await new Promise((resolve) => { + const listener = ((data) => { + if ('chainId' in data && data.chainId === chainId) { + config.emitter.off('change', listener) + resolve() + } + }) satisfies Parameters[1] + config.emitter.on('change', listener) + config.emitter.emit('change', { chainId }) + }) + } + + // On mobile, there is a race condition between the result of `'wallet_addEthereumChain'` and `'eth_chainId'`. + // To avoid this, we wait for `'eth_chainId'` to return the expected chain ID with a retry loop. + const waitForChainIdToSync = async () => { + await withRetry( + async () => { + 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) { + // `value` doesn't match expected `chainId`, throw to trigger retry + throw new Error('User rejected switch after adding network.') + } + return value + }, + { + delay: 50, + retryCount: 20, // android device encryption is slower + }, + ) + } + // Default chains cannot be added or removed const isDefaultChain = (() => { const metaMaskDefaultChains = [ @@ -366,32 +401,9 @@ export function metaMask(parameters: MetaMaskParameters = {}) { ], }) - // 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( - 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') - } - return value - }, - { - delay: 100, - retryCount: 5, // android device encryption is slower - }, - ) - - if (currentChainId !== chainId) - throw new Error('User rejected switch after adding network.') + await waitForChainIdToSync() + + await sendAndWaitForChangeEvent(chainId) return chain } catch (err) { @@ -403,32 +415,18 @@ export function metaMask(parameters: MetaMaskParameters = {}) { // 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) => { - const listener = ((data) => { - if ('chainId' in data && data.chainId === chainId) { - config.emitter.off('change', listener) - resolve() - } - }) satisfies Parameters[1] - config.emitter.on('change', listener) - }), - ]) + 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) + return chain } catch (err) { const error = err as RpcError From daf5b37878cbc04ed297041cee8c1c7ad3c70851 Mon Sep 17 00:00:00 2001 From: Baptiste Marchand <75846779+baptiste-marchand@users.noreply.github.com> Date: Thu, 19 Dec 2024 19:21:14 +0100 Subject: [PATCH 3/6] Refactor: remove duplicated MetaMask switchNetwork logic --- packages/connectors/src/metaMask.ts | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/packages/connectors/src/metaMask.ts b/packages/connectors/src/metaMask.ts index 35f20e4fab..255ba0950b 100644 --- a/packages/connectors/src/metaMask.ts +++ b/packages/connectors/src/metaMask.ts @@ -364,8 +364,8 @@ export function metaMask(parameters: MetaMaskParameters = {}) { })() // Avoid back and forth on mobile by using `'wallet_addEthereumChain'` for non-default chains - if (!isDefaultChain) - try { + try { + if (!isDefaultChain) { const blockExplorerUrls = (() => { const { default: blockExplorer, ...blockExplorers } = chain.blockExplorers ?? {} @@ -400,25 +400,12 @@ export function metaMask(parameters: MetaMaskParameters = {}) { } satisfies AddEthereumChainParameter, ], }) - - await waitForChainIdToSync() - - await sendAndWaitForChangeEvent(chainId) - - return chain - } catch (err) { - const error = err as RpcError - if (error.code === UserRejectedRequestError.code) - throw new UserRejectedRequestError(error) - throw new SwitchChainError(error) + } else { + await provider.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: numberToHex(chainId) }], + }) } - - // Use to `'wallet_switchEthereumChain'` for default chains - try { - 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 From 4838ea5f76ea5a333c53d0c9299a60ebf106fe3e Mon Sep 17 00:00:00 2001 From: Baptiste Marchand <75846779+baptiste-marchand@users.noreply.github.com> Date: Thu, 19 Dec 2024 19:21:31 +0100 Subject: [PATCH 4/6] Add MetaMask to vite-vue example --- playgrounds/vite-vue/src/wagmi.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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: { From 32237122b2a8e16e5beb5ce82a5afa1368b461cc Mon Sep 17 00:00:00 2001 From: Tom Meagher Date: Fri, 20 Dec 2024 18:23:43 -0500 Subject: [PATCH 5/6] refactor: reorg --- packages/connectors/src/metaMask.ts | 113 +++++++++++++--------------- 1 file changed, 54 insertions(+), 59 deletions(-) diff --git a/packages/connectors/src/metaMask.ts b/packages/connectors/src/metaMask.ts index 255ba0950b..50f81e68ab 100644 --- a/packages/connectors/src/metaMask.ts +++ b/packages/connectors/src/metaMask.ts @@ -315,41 +315,6 @@ export function metaMask(parameters: MetaMaskParameters = {}) { const chain = config.chains.find((x) => x.id === chainId) if (!chain) throw new SwitchChainError(new ChainNotConfiguredError()) - const sendAndWaitForChangeEvent = async (chainId: number) => { - await new Promise((resolve) => { - const listener = ((data) => { - if ('chainId' in data && data.chainId === chainId) { - config.emitter.off('change', listener) - resolve() - } - }) satisfies Parameters[1] - config.emitter.on('change', listener) - config.emitter.emit('change', { chainId }) - }) - } - - // On mobile, there is a race condition between the result of `'wallet_addEthereumChain'` and `'eth_chainId'`. - // To avoid this, we wait for `'eth_chainId'` to return the expected chain ID with a retry loop. - const waitForChainIdToSync = async () => { - await withRetry( - async () => { - 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) { - // `value` doesn't match expected `chainId`, throw to trigger retry - throw new Error('User rejected switch after adding network.') - } - return value - }, - { - delay: 50, - retryCount: 20, // android device encryption is slower - }, - ) - } - // Default chains cannot be added or removed const isDefaultChain = (() => { const metaMaskDefaultChains = [ @@ -365,47 +330,43 @@ export function metaMask(parameters: MetaMaskParameters = {}) { // Avoid back and forth on mobile by using `'wallet_addEthereumChain'` for non-default chains try { - if (!isDefaultChain) { - 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] ?? ''] - })() - + 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 { + 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 @@ -414,6 +375,40 @@ export function metaMask(parameters: MetaMaskParameters = {}) { await waitForChainIdToSync() await sendAndWaitForChangeEvent(chainId) + async function waitForChainIdToSync() { + // On mobile, there is a race condition between the result of `'wallet_addEthereumChain'` and `'eth_chainId'`. + // To avoid this, we wait for `'eth_chainId'` to return the expected chain ID with a retry loop. + await withRetry( + async () => { + const value = hexToNumber( + // `'eth_chainId'` is cached by the MetaMask SDK side to avoid unnecessary deeplinks + (await provider.request({ method: 'eth_chainId' })) as Hex, + ) + // `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: 50, + retryCount: 20, // android device encryption is slower + }, + ) + } + + async function sendAndWaitForChangeEvent(chainId: number) { + await new Promise((resolve) => { + const listener = ((data) => { + if ('chainId' in data && data.chainId === chainId) { + config.emitter.off('change', listener) + resolve() + } + }) satisfies Parameters[1] + config.emitter.on('change', listener) + config.emitter.emit('change', { chainId }) + }) + } + return chain } catch (err) { const error = err as RpcError From 6f8befd63c216a45d3a6c6febb2c88254036f418 Mon Sep 17 00:00:00 2001 From: Tom Meagher Date: Fri, 20 Dec 2024 18:34:10 -0500 Subject: [PATCH 6/6] chore: changeset --- .changeset/afraid-cows-mix.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/afraid-cows-mix.md 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.