Skip to content

Commit

Permalink
feat: update MetaMask version and fix switch network (#4471)
Browse files Browse the repository at this point in the history
* fix: update metamask sdk

* Refactor: MetaMask switchNetwork consistently emit a 'change' event

* Refactor: remove duplicated MetaMask switchNetwork logic

* Add MetaMask to vite-vue example

* refactor: reorg

* chore: changeset

---------

Co-authored-by: Baptiste Marchand <[email protected]>
Co-authored-by: Tom Meagher <[email protected]>
  • Loading branch information
3 people authored Dec 20, 2024
1 parent 3892ebd commit 9c8c35a
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 120 deletions.
5 changes: 5 additions & 0 deletions .changeset/afraid-cows-mix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@wagmi/connectors": patch
---

Improved MetaMask chain switching behavior.
2 changes: 1 addition & 1 deletion packages/connectors/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
110 changes: 45 additions & 65 deletions packages/connectors/src/metaMask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,106 +329,86 @@ 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',

This comment has been minimized.

Copy link
@Jeremiegmoore

Jeremiegmoore Dec 21, 2024

packages/connectors/src/metaMask.ts

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<void>((resolve) => {
async function sendAndWaitForChangeEvent(chainId: number) {
await new Promise<void>((resolve) => {
const listener = ((data) => {
if ('chainId' in data && data.chainId === chainId) {
config.emitter.off('change', listener)
resolve()
}
}) satisfies Parameters<typeof config.emitter.on>[1]
config.emitter.on('change', listener)
}),
])
config.emitter.emit('change', { chainId })
})
}

return chain
} catch (err) {
const error = err as RpcError
Expand Down
3 changes: 2 additions & 1 deletion playgrounds/vite-vue/src/wagmi.ts
Original file line number Diff line number Diff line change
@@ -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],
Expand All @@ -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: {
Expand Down
93 changes: 40 additions & 53 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 9c8c35a

Please sign in to comment.