Skip to content

Commit a257e8d

Browse files
EdouardBougonbaptiste-marchandtmm
authoredJan 22, 2025··
Fix chain switch: back to consistent switchChain before addChain (#4512)
* Fix chain switch: back to consistent switchChain before addChain * chore: changeset --------- Co-authored-by: Baptiste Marchand <75846779+baptiste-marchand@users.noreply.github.com> Co-authored-by: Tom Meagher <tom@meagher.co>
1 parent 6079b6f commit a257e8d

File tree

2 files changed

+98
-85
lines changed

2 files changed

+98
-85
lines changed
 

‎.changeset/eighty-socks-roll.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@wagmi/connectors": patch
3+
---
4+
5+
Fixed MetaMask switchChain/addChain handling.

‎packages/connectors/src/metaMask.ts

+93-85
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
createConnector,
1212
extractRpcUrls,
1313
} from '@wagmi/core'
14-
import type { linea, lineaSepolia, mainnet, sepolia } from '@wagmi/core/chains'
1514
import type {
1615
Compute,
1716
ExactPartial,
@@ -24,6 +23,7 @@ import {
2423
type Address,
2524
type Hex,
2625
type ProviderConnectInfo,
26+
type ProviderRpcError,
2727
ResourceUnavailableRpcError,
2828
type RpcError,
2929
SwitchChainError,
@@ -315,57 +315,11 @@ export function metaMask(parameters: MetaMaskParameters = {}) {
315315
const chain = config.chains.find((x) => x.id === chainId)
316316
if (!chain) throw new SwitchChainError(new ChainNotConfiguredError())
317317

318-
// Default chains cannot be added or removed
319-
const isDefaultChain = (() => {
320-
const metaMaskDefaultChains = [
321-
1, 11_155_111, 59_144, 59_141,
322-
] satisfies [
323-
typeof mainnet.id,
324-
typeof sepolia.id,
325-
typeof linea.id,
326-
typeof lineaSepolia.id,
327-
]
328-
return metaMaskDefaultChains.find((x) => x === chainId)
329-
})()
330-
331-
// Avoid back and forth on mobile by using `'wallet_addEthereumChain'` for non-default chains
332318
try {
333-
if (!isDefaultChain)
334-
await provider.request({
335-
method: 'wallet_addEthereumChain',
336-
params: [
337-
{
338-
blockExplorerUrls: (() => {
339-
const { default: blockExplorer, ...blockExplorers } =
340-
chain.blockExplorers ?? {}
341-
if (addEthereumChainParameter?.blockExplorerUrls)
342-
return addEthereumChainParameter.blockExplorerUrls
343-
if (blockExplorer)
344-
return [
345-
blockExplorer.url,
346-
...Object.values(blockExplorers).map((x) => x.url),
347-
]
348-
return
349-
})(),
350-
chainId: numberToHex(chainId),
351-
chainName: addEthereumChainParameter?.chainName ?? chain.name,
352-
iconUrls: addEthereumChainParameter?.iconUrls,
353-
nativeCurrency:
354-
addEthereumChainParameter?.nativeCurrency ??
355-
chain.nativeCurrency,
356-
rpcUrls: (() => {
357-
if (addEthereumChainParameter?.rpcUrls?.length)
358-
return addEthereumChainParameter.rpcUrls
359-
return [chain.rpcUrls.default?.http[0] ?? '']
360-
})(),
361-
} satisfies AddEthereumChainParameter,
362-
],
363-
})
364-
else
365-
await provider.request({
366-
method: 'wallet_switchEthereumChain',
367-
params: [{ chainId: numberToHex(chainId) }],
368-
})
319+
await provider.request({
320+
method: 'wallet_switchEthereumChain',
321+
params: [{ chainId: numberToHex(chainId) }],
322+
})
369323

370324
// During `'wallet_switchEthereumChain'`, MetaMask makes a `'net_version'` RPC call to the target chain.
371325
// If this request fails, MetaMask does not emit the `'chainChanged'` event, but will still switch the chain.
@@ -375,47 +329,101 @@ export function metaMask(parameters: MetaMaskParameters = {}) {
375329
await waitForChainIdToSync()
376330
await sendAndWaitForChangeEvent(chainId)
377331

378-
async function waitForChainIdToSync() {
379-
// On mobile, there is a race condition between the result of `'wallet_addEthereumChain'` and `'eth_chainId'`.
380-
// To avoid this, we wait for `'eth_chainId'` to return the expected chain ID with a retry loop.
381-
await withRetry(
382-
async () => {
383-
const value = hexToNumber(
384-
// `'eth_chainId'` is cached by the MetaMask SDK side to avoid unnecessary deeplinks
385-
(await provider.request({ method: 'eth_chainId' })) as Hex,
386-
)
387-
// `value` doesn't match expected `chainId`, throw to trigger retry
388-
if (value !== chainId)
389-
throw new Error('User rejected switch after adding network.')
390-
return value
391-
},
392-
{
393-
delay: 50,
394-
retryCount: 20, // android device encryption is slower
395-
},
396-
)
397-
}
398-
399-
async function sendAndWaitForChangeEvent(chainId: number) {
400-
await new Promise<void>((resolve) => {
401-
const listener = ((data) => {
402-
if ('chainId' in data && data.chainId === chainId) {
403-
config.emitter.off('change', listener)
404-
resolve()
405-
}
406-
}) satisfies Parameters<typeof config.emitter.on>[1]
407-
config.emitter.on('change', listener)
408-
config.emitter.emit('change', { chainId })
409-
})
410-
}
411-
412332
return chain
413333
} catch (err) {
414334
const error = err as RpcError
335+
415336
if (error.code === UserRejectedRequestError.code)
416337
throw new UserRejectedRequestError(error)
338+
339+
// Indicates chain is not added to provider
340+
if (
341+
error.code === 4902 ||
342+
// Unwrapping for MetaMask Mobile
343+
// https://github.com/MetaMask/metamask-mobile/issues/2944#issuecomment-976988719
344+
(error as ProviderRpcError<{ originalError?: { code: number } }>)
345+
?.data?.originalError?.code === 4902
346+
) {
347+
try {
348+
await provider.request({
349+
method: 'wallet_addEthereumChain',
350+
params: [
351+
{
352+
blockExplorerUrls: (() => {
353+
const { default: blockExplorer, ...blockExplorers } =
354+
chain.blockExplorers ?? {}
355+
if (addEthereumChainParameter?.blockExplorerUrls)
356+
return addEthereumChainParameter.blockExplorerUrls
357+
if (blockExplorer)
358+
return [
359+
blockExplorer.url,
360+
...Object.values(blockExplorers).map((x) => x.url),
361+
]
362+
return
363+
})(),
364+
chainId: numberToHex(chainId),
365+
chainName: addEthereumChainParameter?.chainName ?? chain.name,
366+
iconUrls: addEthereumChainParameter?.iconUrls,
367+
nativeCurrency:
368+
addEthereumChainParameter?.nativeCurrency ??
369+
chain.nativeCurrency,
370+
rpcUrls: (() => {
371+
if (addEthereumChainParameter?.rpcUrls?.length)
372+
return addEthereumChainParameter.rpcUrls
373+
return [chain.rpcUrls.default?.http[0] ?? '']
374+
})(),
375+
} satisfies AddEthereumChainParameter,
376+
],
377+
})
378+
379+
await waitForChainIdToSync()
380+
await sendAndWaitForChangeEvent(chainId)
381+
382+
return chain
383+
} catch (err) {
384+
const error = err as RpcError
385+
if (error.code === UserRejectedRequestError.code)
386+
throw new UserRejectedRequestError(error)
387+
throw new SwitchChainError(error)
388+
}
389+
}
390+
417391
throw new SwitchChainError(error)
418392
}
393+
394+
async function waitForChainIdToSync() {
395+
// On mobile, there is a race condition between the result of `'wallet_addEthereumChain'` and `'eth_chainId'`.
396+
// To avoid this, we wait for `'eth_chainId'` to return the expected chain ID with a retry loop.
397+
await withRetry(
398+
async () => {
399+
const value = hexToNumber(
400+
// `'eth_chainId'` is cached by the MetaMask SDK side to avoid unnecessary deeplinks
401+
(await provider.request({ method: 'eth_chainId' })) as Hex,
402+
)
403+
// `value` doesn't match expected `chainId`, throw to trigger retry
404+
if (value !== chainId)
405+
throw new Error('User rejected switch after adding network.')
406+
return value
407+
},
408+
{
409+
delay: 50,
410+
retryCount: 20, // android device encryption is slower
411+
},
412+
)
413+
}
414+
415+
async function sendAndWaitForChangeEvent(chainId: number) {
416+
await new Promise<void>((resolve) => {
417+
const listener = ((data) => {
418+
if ('chainId' in data && data.chainId === chainId) {
419+
config.emitter.off('change', listener)
420+
resolve()
421+
}
422+
}) satisfies Parameters<typeof config.emitter.on>[1]
423+
config.emitter.on('change', listener)
424+
config.emitter.emit('change', { chainId })
425+
})
426+
}
419427
},
420428
async onAccountsChanged(accounts) {
421429
// Disconnect if there are no accounts

0 commit comments

Comments
 (0)
Please sign in to comment.