diff --git a/src/utils/networks.ts b/src/utils/networks.ts index 3328451..0ee8cf7 100644 --- a/src/utils/networks.ts +++ b/src/utils/networks.ts @@ -29,7 +29,6 @@ export const networkParams = { export async function isNetworkAdded(network: NetworkType): Promise { if (!(window as any).ethereum) return false; - try { const chainId = await (window as any).ethereum.request({ method: "eth_chainId", @@ -45,6 +44,14 @@ export async function isNetworkAdded(network: NetworkType): Promise { export type UseNetworkResponse = { isWalletInstalled: boolean; + // NOTE on semantics: `isAdded` is a best-effort heuristic, not a wallet + // guarantee. Neither eth_chainId nor chainChanged can tell us "is this + // chain known to the wallet" independent of it being the *active* chain + // (EIP-3326 / EIP-3085 expose no such query). `isAdded` is therefore true + // only when: (a) this chain is currently active, or (b) we personally + // observed a successful wallet_addEthereumChain / wallet_switchEthereumChain + // for it during this hook's lifetime. It is scoped per `network` and does + // NOT persist across a `network` change. isAdded: boolean; isSelected: boolean; addNetwork: () => Promise; @@ -58,22 +65,38 @@ export function useNetwork(network: NetworkType): UseNetworkResponse { // Check if network is added and selected on mount and when network changes useEffect(() => { + // Passive-path fix: reset per-network state whenever `network` changes, + // so a sticky `true` from a previously selected network can't leak into + // the newly requested one. + setIsAdded(false); + setIsSelected(false); + + // Cancellation guard: checkNetwork is async, so a call started for the + // previous `network` can still resolve after this effect re-runs for a + // new `network` and write stale results into state. `cancelled` blocks + // any such late write. + let cancelled = false; + const checkNetwork = async () => { if (window.ethereum) { try { const chainId = await window.ethereum.request({ method: "eth_chainId", }); + if (cancelled) return; const isCurrentNetwork = chainId.toLowerCase() === networkParams[network].chainId.toLowerCase(); setIsSelected(isCurrentNetwork); - setIsAdded((prev) => isCurrentNetwork || prev); // If we're on the network, it must be added + // Sticky-OR is fine *within* a single network's lifetime (e.g. + // chainChanged firing away and back), just not across network + // switches, which the reset above now guarantees. + setIsAdded((prev) => isCurrentNetwork || prev); } catch (error) { - console.error("Error checking network:", error); + if (!cancelled) console.error("Error checking network:", error); } } - setIsWalletInstalled(window.ethereum !== undefined); + if (!cancelled) setIsWalletInstalled(window.ethereum !== undefined); }; checkNetwork(); @@ -82,21 +105,38 @@ export function useNetwork(network: NetworkType): UseNetworkResponse { if (window.ethereum) { window.ethereum.on("chainChanged", checkNetwork); return () => { + cancelled = true; window.ethereum.removeListener("chainChanged", checkNetwork); }; } + return () => { + cancelled = true; + }; }, [network]); async function addNetwork(): Promise { if (!window.ethereum) return; - try { await window.ethereum.request({ method: "wallet_addEthereumChain", params: [networkParams[network]], }); + // EIP-3085 only guarantees the chain is now known to the wallet; it + // does NOT guarantee the wallet switches to it. Some wallets do this + // as a UX convenience, but it isn't part of the spec, so we call + // wallet_switchEthereumChain explicitly rather than assuming it + // happened. This call is not recursive with selectNetwork(): it's a + // direct request, so there's no addNetwork() <-> selectNetwork() cycle. setIsAdded(true); - await selectNetwork(); // Automatically switch to the network after adding + try { + await window.ethereum.request({ + method: "wallet_switchEthereumChain", + params: [{ chainId: networkParams[network].chainId }], + }); + setIsSelected(true); + } catch (switchError) { + console.error("Error selecting network after add:", switchError); + } } catch (error) { console.error("Error adding network:", error); } @@ -104,14 +144,22 @@ export function useNetwork(network: NetworkType): UseNetworkResponse { async function selectNetwork(): Promise { if (!window.ethereum) return; - try { await window.ethereum.request({ method: "wallet_switchEthereumChain", params: [{ chainId: networkParams[network].chainId }], }); setIsSelected(true); - } catch (error) { + setIsAdded(true); + } catch (error: any) { + // Active-path fix: 4902 means the wallet doesn't know this chain yet. + // Fall back to adding it. addNetwork() makes its own direct + // wallet_switchEthereumChain call rather than calling selectNetwork(), + // so this can't recurse. + if (error?.code === 4902) { + await addNetwork(); + return; + } console.error("Error switching network:", error); } }