From 8e7b85f6d8e7031d883b5ace28b9be5e35a7ad2c Mon Sep 17 00:00:00 2001 From: QSchlegel Date: Wed, 19 Nov 2025 17:25:05 +0100 Subject: [PATCH] Enhance wallet connection and asset fetching logic - Improved auto-connection logic for wallets, ensuring checks for existing connections and wallet availability. - Added handling for simultaneous asset and network ID fetching to prevent race conditions. - Enhanced user experience by providing loading states and error handling during wallet initialization and asset fetching. - Updated user creation and update logic to invalidate queries upon success, ensuring fresh data retrieval. --- .../common/cardano-objects/connect-wallet.tsx | 194 +++++++++++++++--- .../common/overall-layout/layout.tsx | 115 +++++++++-- 2 files changed, 261 insertions(+), 48 deletions(-) diff --git a/src/components/common/cardano-objects/connect-wallet.tsx b/src/components/common/cardano-objects/connect-wallet.tsx index 332d3f3d..7f4f6e2c 100644 --- a/src/components/common/cardano-objects/connect-wallet.tsx +++ b/src/components/common/cardano-objects/connect-wallet.tsx @@ -10,7 +10,7 @@ import { } from "@/components/ui/dropdown-menu"; import { useWallet, useWalletList } from "@meshsdk/react"; import { useSiteStore } from "@/lib/zustand/site"; -import { useEffect } from "react"; +import { useEffect, useRef } from "react"; import useUser from "@/hooks/useUser"; import { useUserStore } from "@/lib/zustand/user"; import { getProvider } from "@/utils/get-provider"; @@ -24,11 +24,17 @@ export default function ConnectWallet() { const setUserAssetMetadata = useUserStore( (state) => state.setUserAssetMetadata, ); - const { user } = useUser(); + const { user, isLoading: userLoading } = useUser(); const wallets = useWalletList(); const { connect, connected, wallet, name } = useWallet(); const network = useSiteStore((state) => state.network); + const connectingRef = useRef(false); + const fetchingAssetsRef = useRef(false); + const lastWalletIdRef = useRef(null); + const fetchingNetworkRef = useRef(false); + const lastNetworkWalletRef = useRef(null); + const userAssets = useUserStore((state) => state.userAssets); async function connectWallet(walletId: string) { setPastWallet(walletId); @@ -37,70 +43,204 @@ export default function ConnectWallet() { /** * Try to connect the wallet when the user loads the application, if user had connected before, + * but only if: + * 1. The wallet list has been loaded (wallets.length > 0) + * 2. The pastWallet exists in the available wallets + * 3. We're not already connected + * 4. We're not already attempting to connect */ useEffect(() => { async function handleAutoWalletConnect() { - if (pastWallet && !connected) { - try { - await connect(pastWallet); - } catch (e) { - setPastWallet(undefined); - } + // Don't attempt if already connected or already connecting + if (connected || connectingRef.current) { + return; + } + + // Don't attempt if no pastWallet is stored + if (!pastWallet) { + return; + } + + // Wait for wallet list to be available + // If wallets array is empty, wallets might still be loading + // The effect will re-run when wallets become available + if (wallets.length === 0) { + return; + } + + // Check if the pastWallet exists in the available wallets + const walletExists = wallets.some((w) => w.id === pastWallet); + if (!walletExists) { + console.warn( + `Stored wallet "${pastWallet}" not found in available wallets. Clearing stored wallet.`, + ); + setPastWallet(undefined); + return; + } + + // Attempt to connect + connectingRef.current = true; + try { + console.log(`Attempting to auto-connect wallet: ${pastWallet}`); + await connect(pastWallet); + console.log(`Successfully auto-connected wallet: ${pastWallet}`); + } catch (e) { + console.error( + `Failed to auto-connect wallet "${pastWallet}":`, + e instanceof Error ? e.message : e, + ); + setPastWallet(undefined); + } finally { + connectingRef.current = false; } } + handleAutoWalletConnect(); - }, [pastWallet, connected]); + }, [pastWallet, connected, wallets, connect, setPastWallet]); useEffect(() => { async function lookupWalletAssets() { if (!wallet) return; + + // Prevent multiple simultaneous calls + if (fetchingAssetsRef.current) { + console.log("Assets fetch already in progress, skipping..."); + return; + } + + // Use wallet name as identifier (doesn't require API call) + const walletId = name || "unknown"; + + // Skip if we've already fetched for this wallet and have assets + if (lastWalletIdRef.current === walletId && userAssets.length > 0) { + console.log("Assets already loaded for this wallet, skipping fetch"); + return; + } + + fetchingAssetsRef.current = true; + lastWalletIdRef.current = walletId; + try { + console.log("Fetching wallet balance..."); const assets = await wallet.getBalance(); - const provider = getProvider(await wallet.getNetworkId()); - const userAssets: Asset[] = []; + // Use network from store if available, otherwise fetch it + let networkId = network; + if (!networkId) { + try { + networkId = await wallet.getNetworkId(); + setNetwork(networkId); + } catch (networkError) { + console.error("Error getting network ID for provider:", networkError); + // Use default network if we can't get it + networkId = 0; // Mainnet default + } + } + const provider = getProvider(networkId); + const fetchedAssets: Asset[] = []; if (assets) { for (const asset of assets) { - userAssets.push({ + fetchedAssets.push({ unit: asset.unit, quantity: asset.quantity, }); if (asset.unit === "lovelace") continue; - const assetInfo = await provider.get(`/assets/${asset.unit}`); - setUserAssetMetadata( - asset.unit, - assetInfo?.metadata?.name || - assetInfo?.onchain_metadata?.name || + try { + const assetInfo = await provider.get(`/assets/${asset.unit}`); + setUserAssetMetadata( asset.unit, - assetInfo?.metadata?.decimals || 0, - ); + assetInfo?.metadata?.name || + assetInfo?.onchain_metadata?.name || + asset.unit, + assetInfo?.metadata?.decimals || 0, + ); + } catch (assetError) { + // If asset metadata fetch fails, continue with other assets + console.warn(`Failed to fetch metadata for asset ${asset.unit}:`, assetError); + } } - setUserAssets(userAssets); + setUserAssets(fetchedAssets); + console.log("Successfully fetched wallet assets"); } + // Reset the fetching flag after successful fetch + fetchingAssetsRef.current = false; } catch (error) { console.error("Error looking up wallet assets:", error); + // If it's a rate limit error, don't clear the ref immediately + // to prevent rapid retries - wait 5 seconds before allowing retry + if (error instanceof Error && error.message.includes("too many requests")) { + console.warn("Rate limit hit, will retry after delay"); + setTimeout(() => { + fetchingAssetsRef.current = false; + }, 5000); + return; + } + // For other errors, reset immediately so we can retry + fetchingAssetsRef.current = false; } } + async function handleNetworkChange() { - if (connected) { - setNetwork(await wallet.getNetworkId()); + if (!connected || !wallet) return; + + // Prevent multiple simultaneous network ID fetches + if (fetchingNetworkRef.current) { + console.log("Network ID fetch already in progress, skipping..."); + return; + } + + // Use wallet name as identifier (doesn't require API call) + const walletId = name || "unknown"; + + // Skip if we've already fetched network for this wallet + if (lastNetworkWalletRef.current === walletId && network !== undefined) { + console.log("Network ID already fetched for this wallet, skipping"); + return; + } + + fetchingNetworkRef.current = true; + lastNetworkWalletRef.current = walletId; + + try { + console.log("Fetching network ID..."); + const networkId = await wallet.getNetworkId(); + setNetwork(networkId); + console.log("Successfully fetched network ID:", networkId); + fetchingNetworkRef.current = false; + } catch (error) { + console.error("Error getting network ID:", error); + // If rate limited, wait before retry + if (error instanceof Error && error.message.includes("too many requests")) { + console.warn("Rate limit hit for network ID, will retry after delay"); + setTimeout(() => { + fetchingNetworkRef.current = false; + }, 5000); + return; + } + fetchingNetworkRef.current = false; } } + async function getWalletAssets() { if (wallet && connected) { await lookupWalletAssets(); } } - handleNetworkChange(); - getWalletAssets(); - }, [connected, wallet, setNetwork]); + + // Only run if wallet and connected state are available + if (wallet && connected) { + handleNetworkChange(); + getWalletAssets(); + } + }, [connected, wallet, name, setNetwork, setUserAssets, setUserAssetMetadata]); return ( diff --git a/src/components/common/overall-layout/layout.tsx b/src/components/common/overall-layout/layout.tsx index 914bd2f1..18a138aa 100644 --- a/src/components/common/overall-layout/layout.tsx +++ b/src/components/common/overall-layout/layout.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, Component, ReactNode } from "react"; +import React, { useEffect, useRef, Component, ReactNode } from "react"; import { useRouter } from "next/router"; import Link from "next/link"; import { useNostrChat } from "@jinglescode/nostr-chat-plugin"; @@ -73,6 +73,9 @@ export default function RootLayout({ const userAddress = useUserStore((state) => state.userAddress); const setUserAddress = useUserStore((state) => state.setUserAddress); + const ctx = api.useUtils(); + const initializingWalletRef = useRef(false); + const lastInitializedWalletRef = useRef(null); // Global error handler for unhandled promise rejections useEffect(() => { @@ -99,10 +102,23 @@ export default function RootLayout({ }, []); const { mutate: createUser } = api.user.createUser.useMutation({ - onError: (e) => console.error(e), + onSuccess: (_, variables) => { + console.log("User created/updated successfully, invalidating user query"); + // Invalidate the user query so it refetches the newly created user + void ctx.user.getUserByAddress.invalidate({ address: variables.address }); + }, + onError: (e) => { + console.error("Error creating user:", e); + }, }); const { mutate: updateUser } = api.user.updateUser.useMutation({ - onError: (e) => console.error(e), + onSuccess: (_, variables) => { + console.log("User updated successfully, invalidating user query"); + void ctx.user.getUserByAddress.invalidate({ address: variables.address }); + }, + onError: (e) => { + console.error("Error updating user:", e); + }, }); // Single effect for address + user creation @@ -110,16 +126,57 @@ export default function RootLayout({ (async () => { if (!connected || !wallet) return; + // Don't run if user is already loaded (to avoid unnecessary re-runs) + if (user) return; + + // Prevent multiple simultaneous initializations + if (initializingWalletRef.current) { + console.log("Layout: Wallet initialization already in progress, skipping..."); + return; + } + + // Skip if we've already initialized this wallet and have userAddress + if (userAddress && lastInitializedWalletRef.current === userAddress) { + console.log("Layout: Wallet already initialized, skipping"); + return; + } + + initializingWalletRef.current = true; + try { + console.log("Layout: Starting wallet initialization"); + // 1) Set user address in store - let address = (await wallet.getUsedAddresses())[0]; - if (!address) address = (await wallet.getUnusedAddresses())[0]; - if (address) setUserAddress(address); + let address: string | undefined; + try { + const usedAddresses = await wallet.getUsedAddresses(); + address = usedAddresses[0]; + } catch (e) { + // If used addresses fail, try unused addresses + try { + const unusedAddresses = await wallet.getUnusedAddresses(); + address = unusedAddresses[0]; + } catch (e2) { + console.error("Layout: Could not get addresses:", e2); + initializingWalletRef.current = false; + return; + } + } + + if (address) { + console.log("Layout: Setting user address:", address); + setUserAddress(address); + lastInitializedWalletRef.current = address; + } else { + console.error("Layout: No address found from wallet"); + initializingWalletRef.current = false; + return; + } // 2) Get stake address const stakeAddress = (await wallet.getRewardAddresses())[0]; if (!stakeAddress || !address) { - console.error("No stake address or payment address found"); + console.error("Layout: No stake address or payment address found"); return; } @@ -131,33 +188,49 @@ export default function RootLayout({ drepKeyHash = dRepKey.publicKeyHash; } } catch (error) { + // DRep key is optional, so we can ignore errors } // 4) Create or update user (upsert pattern handles both cases) - if (!isLoading) { - const nostrKey = generateNsec(); - createUser({ - address, - stakeAddress, - drepKeyHash, - nostrKey: JSON.stringify(nostrKey), - }); - } + // Remove the isLoading check - we should create user regardless + console.log("Layout: Creating/updating user"); + const nostrKey = generateNsec(); + createUser({ + address, + stakeAddress, + drepKeyHash, + nostrKey: JSON.stringify(nostrKey), + }); + console.log("Layout: Wallet initialization completed successfully"); } catch (error) { - console.error("Error in wallet initialization effect:", error); + console.error("Layout: Error in wallet initialization effect:", error); // If we get an "account changed" error, reload the page if (error instanceof Error && error.message.includes("account changed")) { - console.log("Account changed detected, reloading page..."); + console.log("Layout: Account changed detected, reloading page..."); window.location.reload(); return; } + + // If rate limited, wait before allowing retry + if (error instanceof Error && error.message.includes("too many requests")) { + console.warn("Layout: Rate limit hit, will retry after delay"); + setTimeout(() => { + initializingWalletRef.current = false; + }, 5000); + return; + } - // For other errors, don't throw to prevent app crash - // The user can retry by reconnecting their wallet + // For other errors, reset so we can retry + initializingWalletRef.current = false; + } finally { + // Reset if not rate limited (rate limit errors return early) + if (initializingWalletRef.current) { + initializingWalletRef.current = false; + } } })(); - }, [connected, wallet, user, isLoading, createUser, generateNsec, setUserAddress]); + }, [connected, wallet, user, userAddress, createUser, generateNsec, setUserAddress]); const isWalletPath = router.pathname.includes("/wallets/[wallet]"); const walletPageRoute = router.pathname.split("/wallets/[wallet]/")[1];