Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 56 additions & 8 deletions src/utils/networks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ export const networkParams = {

export async function isNetworkAdded(network: NetworkType): Promise<boolean> {
if (!(window as any).ethereum) return false;

try {
const chainId = await (window as any).ethereum.request({
method: "eth_chainId",
Expand All @@ -45,6 +44,14 @@ export async function isNetworkAdded(network: NetworkType): Promise<boolean> {

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<void>;
Expand All @@ -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();
Expand All @@ -82,36 +105,61 @@ 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<void> {
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);
}
}

async function selectNetwork(): Promise<void> {
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);
}
}
Expand Down