Skip to content
Merged
Show file tree
Hide file tree
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
295 changes: 41 additions & 254 deletions src/components/common/cardano-objects/connect-wallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useWallet, useWalletList } from "@meshsdk/react";
import { useWallet, useWalletList, useNetwork, useAssets } from "@meshsdk/react";
import { useSiteStore } from "@/lib/zustand/site";
import { useEffect, useRef, useState } from "react";
import { useEffect } from "react";
import useUser from "@/hooks/useUser";
import { useUserStore } from "@/lib/zustand/user";
import { getProvider } from "@/utils/get-provider";
import { Asset } from "@meshsdk/core";

export default function ConnectWallet() {
const setNetwork = useSiteStore((state) => state.setNetwork);
Expand All @@ -28,277 +27,69 @@ export default function ConnectWallet() {

const wallets = useWalletList();
const { connect, connected, wallet, name } = useWallet();
const networkId = useNetwork();
const assets = useAssets();
const network = useSiteStore((state) => state.network);
const connectingRef = useRef(false);
const fetchingAssetsRef = useRef(false);
const lastWalletIdRef = useRef<string | null>(null);
const fetchingNetworkRef = useRef(false);
const lastNetworkWalletRef = useRef<string | null>(null);
const userAssets = useUserStore((state) => state.userAssets);
const [walletsLoading, setWalletsLoading] = useState(true);
const [isMounted, setIsMounted] = useState(false);
const walletsRetryTimeoutRef = useRef<NodeJS.Timeout | null>(null);

// Ensure component only runs on client side (important for SSR/production)
useEffect(() => {
setIsMounted(true);
}, []);

async function connectWallet(walletId: string) {
setPastWallet(walletId);
await connect(walletId);
}

// Monitor wallet list loading state and retry if empty
// Auto-connect if user had connected before
useEffect(() => {
// Only run on client side
if (!isMounted) return;

if (wallets.length > 0) {
setWalletsLoading(false);
// Clear any pending retry timeout
if (walletsRetryTimeoutRef.current) {
clearTimeout(walletsRetryTimeoutRef.current);
walletsRetryTimeoutRef.current = null;
}
} else if (!connected) {
// Only show loading state if not connected (wallets might load after connection)
// If wallets are empty, wait a bit and check again
// This handles cases where wallet extensions load asynchronously
if (walletsRetryTimeoutRef.current === null) {
setWalletsLoading(true);
let retryCount = 0;
const maxRetries = 10; // Try for up to 10 seconds in production (longer timeout)

const checkWallets = () => {
retryCount++;
// Re-check wallets array length (it might have updated)
if (wallets.length > 0) {
setWalletsLoading(false);
walletsRetryTimeoutRef.current = null;
return;
}

if (retryCount < maxRetries) {
walletsRetryTimeoutRef.current = setTimeout(checkWallets, 1000); // Check every second
} else {
console.warn("Wallet list still empty after retries, wallets may not be available");
setWalletsLoading(false); // Stop showing loading state
walletsRetryTimeoutRef.current = null;
}
};

walletsRetryTimeoutRef.current = setTimeout(checkWallets, 1000);
}
} else {
// If connected but no wallets, they're probably not needed
setWalletsLoading(false);
}

return () => {
if (walletsRetryTimeoutRef.current) {
clearTimeout(walletsRetryTimeoutRef.current);
walletsRetryTimeoutRef.current = null;
}
};
}, [wallets, connected, isMounted]);

/**
* Try to connect the wallet when the user loads the application, if user had connected before,
* but only if:
* 1. Component is mounted (client-side only)
* 2. The wallet list has been loaded (wallets.length > 0)
* 3. The pastWallet exists in the available wallets
* 4. We're not already connected
* 5. We're not already attempting to connect
*/
useEffect(() => {
// Only run on client side
if (!isMounted) return;

async function handleAutoWalletConnect() {
// 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) {
console.log("Waiting for wallets to load...");
return;
}

// Check if the pastWallet exists in the available wallets
if (pastWallet && !connected && wallets.length > 0) {
const walletExists = wallets.some((w) => w.id === pastWallet);
if (!walletExists) {
console.warn(
`Stored wallet "${pastWallet}" not found in available wallets. Clearing stored wallet.`,
);
if (walletExists) {
connect(pastWallet).catch(() => {
setPastWallet(undefined);
});
} else {
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;
}
}
}, [pastWallet, connected, wallets, connect, setPastWallet]);

handleAutoWalletConnect();
}, [pastWallet, connected, wallets, connect, setPastWallet, isMounted]);

// Sync network from hook to store
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;
}
if (networkId !== undefined && networkId !== null) {
setNetwork(networkId);
}
}, [networkId, setNetwork]);

fetchingAssetsRef.current = true;
lastWalletIdRef.current = walletId;
// Process assets and fetch metadata
useEffect(() => {
if (!connected || !assets || assets.length === 0 || networkId === undefined || networkId === null) return;

async function processAssets() {
if (!assets || networkId === undefined || networkId === null) return;

try {
console.log("Fetching wallet balance...");
const assets = await wallet.getBalance();
// 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) {
fetchedAssets.push({
unit: asset.unit,
quantity: asset.quantity,
});
if (asset.unit === "lovelace") continue;
try {
const assetInfo = await provider.get(`/assets/${asset.unit}`);
setUserAssetMetadata(
setUserAssets(assets);

for (const asset of assets) {
if (asset.unit === "lovelace") continue;
try {
const assetInfo = await provider.get(`/assets/${asset.unit}`);
setUserAssetMetadata(
asset.unit,
assetInfo?.metadata?.name ||
assetInfo?.onchain_metadata?.name ||
asset.unit,
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);
}
assetInfo?.metadata?.decimals || 0,
);
} catch (error) {
// Continue if asset metadata fetch fails
}
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 || !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;
console.error("Error processing assets:", error);
}
}

async function getWalletAssets() {
if (wallet && connected) {
await lookupWalletAssets();
}
}

// Only run if wallet and connected state are available, and component is mounted
if (isMounted && wallet && connected) {
handleNetworkChange();
getWalletAssets();
}
}, [connected, wallet, name, setNetwork, setUserAssets, setUserAssetMetadata, isMounted]);
processAssets();
}, [assets, connected, networkId, setUserAssets, setUserAssetMetadata]);

return (
<DropdownMenu>
Expand All @@ -313,11 +104,7 @@ export default function ConnectWallet() {
<DropdownMenuContent align="end">
<DropdownMenuLabel>Select Wallet</DropdownMenuLabel>
<DropdownMenuSeparator />
{walletsLoading && wallets.length === 0 ? (
<DropdownMenuItem disabled>
<span className="text-muted-foreground">Loading wallets...</span>
</DropdownMenuItem>
) : wallets.length === 0 ? (
{wallets.length === 0 ? (
<DropdownMenuItem disabled>
<span className="text-muted-foreground">
No wallets available. Please install a Cardano wallet extension.
Expand Down
Loading
Loading