From a6da2eb29122b0604a111f7937710e1469fd6355 Mon Sep 17 00:00:00 2001 From: M3gA-Mind Date: Tue, 10 Feb 2026 15:52:59 +0530 Subject: [PATCH] Refactor wallet information fetching and connection status handling - Updated `WalletInfoSection` to improve loading state management and error handling during wallet info retrieval. - Introduced a retry mechanism for fetching wallet information with a maximum of 5 attempts. - Enhanced connection status logic in `SkillsGrid` to treat missing authentication status as connected. - Added a new method in `SkillManager` to set the wallet address and notify the wallet skill accordingly. --- src/components/SkillsGrid.tsx | 13 +- src/components/WalletInfoSection.tsx | 190 ++++++++++++++++++--------- src/lib/skills/manager.ts | 19 +++ src/pages/Mnemonic.tsx | 5 +- 4 files changed, 156 insertions(+), 71 deletions(-) diff --git a/src/components/SkillsGrid.tsx b/src/components/SkillsGrid.tsx index ca49e03d8..ed98a2b26 100644 --- a/src/components/SkillsGrid.tsx +++ b/src/components/SkillsGrid.tsx @@ -145,15 +145,18 @@ function deriveConnectionStatus( if (connStatus === 'error' || authStatus === 'error') { return 'error'; } - if (connStatus === 'connected' && authStatus === 'authenticated') { - return 'connected'; + // Connected: treat missing auth_status as connected (e.g. wallet skill doesn't set auth_status) + if (connStatus === 'connected') { + if (!authStatus || authStatus === 'authenticated') { + return 'connected'; + } + if (authStatus === 'not_authenticated') { + return 'not_authenticated'; + } } if (connStatus === 'connecting' || authStatus === 'authenticating') { return 'connecting'; } - if (connStatus === 'connected' && authStatus === 'not_authenticated') { - return 'not_authenticated'; - } if (connStatus === 'disconnected') { return setupComplete ? 'disconnected' : 'setup_required'; } diff --git a/src/components/WalletInfoSection.tsx b/src/components/WalletInfoSection.tsx index b1f28943e..102759586 100644 --- a/src/components/WalletInfoSection.tsx +++ b/src/components/WalletInfoSection.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { skillManager } from '../lib/skills/manager'; import { useSkillConnectionStatus } from '../lib/skills/hooks'; @@ -19,13 +19,132 @@ export default function WalletInfoSection() { const [networkName, setNetworkName] = useState(null); const [balance, setBalance] = useState(null); - const [loading, setLoading] = useState(true); + const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const isConnected = walletStatus === 'connected' && !!primaryAddress; + const cancelledRef = useRef(false); + const retryTimerRef = useRef | null>(null); + const fetchWalletInfoRef = useRef<(address: string, attempt?: number) => Promise>(null!); + + const fetchWalletInfo = useCallback(async (address: string, attempt = 0): Promise => { + if (cancelledRef.current) return; + + if (!skillManager.isSkillRunning('wallet')) { + if (attempt < 5 && !cancelledRef.current) { + retryTimerRef.current = setTimeout( + () => fetchWalletInfoRef.current(address, attempt + 1), + 1500 + ); + return; + } + if (!cancelledRef.current) { + setLoading(false); + setNetworkName(null); + setBalance(null); + setError(null); + } + return; + } + + try { + // Wallet skill only supports Ethereum Mainnet (chain_id "1") + const listRes = await skillManager.callTool('wallet', 'list_networks', {}); + if (cancelledRef.current) return; + const listText = listRes.content?.[0]?.text; + if (!listText || listRes.isError) { + if (!cancelledRef.current) { + setError('Could not load networks'); + setLoading(false); + } + return; + } + const listData = JSON.parse(listText) as { + networks?: Array<{ chain_id?: string; name?: string; chain_type?: string }>; + }; + const networks = Array.isArray(listData.networks) ? listData.networks : []; + // Prefer Ethereum Mainnet (skill always has it); else first EVM, else first network + const ethMainnet = networks.find((n) => n?.chain_id === '1' && n?.chain_type === 'evm'); + const firstEvm = networks.find((n) => n && n.chain_type === 'evm'); + const chosen = ethMainnet ?? firstEvm ?? networks.find(Boolean); + if (!chosen || cancelledRef.current) { + if (!cancelledRef.current) setLoading(false); + return; + } + + const networkNameVal = chosen.name ?? chosen.chain_id ?? 'Unknown'; + if (!cancelledRef.current) setNetworkName(networkNameVal); + + const chainId = chosen.chain_id ?? ''; + if (!chainId) { + if (!cancelledRef.current) { + setBalance('—'); + setLoading(false); + } + return; + } + + const balanceRes = await skillManager.callTool('wallet', 'get_balance', { + address, + chain_id: chainId, + chain_type: chosen.chain_type ?? 'evm', + }); + if (cancelledRef.current) return; + const balanceText = balanceRes.content?.[0]?.text; + if (!balanceText || balanceRes.isError) { + if (!cancelledRef.current) { + setBalance('—'); + setLoading(false); + } + return; + } + const balanceData = JSON.parse(balanceText) as { + balance_eth?: string; + symbol?: string; + error?: string; + }; + if (balanceData.error) { + if (!cancelledRef.current) { + setBalance('—'); + setLoading(false); + } + return; + } + const eth = balanceData.balance_eth ?? '0'; + const symbol = balanceData.symbol ?? 'ETH'; + const value = parseFloat(eth); + const display = value < 0.0001 ? '0' : value.toFixed(4); + if (!cancelledRef.current) { + setBalance(`${display} ${symbol}`); + setLoading(false); + } + } catch (e) { + if (!cancelledRef.current) { + const msg = e instanceof Error ? e.message : String(e); + const isTransient = msg.includes('not running') || msg.includes('not started') || msg.includes('transport'); + if (isTransient && attempt < 3) { + retryTimerRef.current = setTimeout(() => fetchWalletInfoRef.current(address, attempt + 1), 2000); + return; + } + console.error('[WalletInfoSection] Failed to load wallet info:', e); + setError('Failed to load wallet info'); + setBalance(null); + setNetworkName(null); + setLoading(false); + } + } + }, []); + + fetchWalletInfoRef.current = fetchWalletInfo; useEffect(() => { - if (!isConnected || !primaryAddress || !skillManager.isSkillRunning('wallet')) { + cancelledRef.current = false; + if (retryTimerRef.current) { + clearTimeout(retryTimerRef.current); + retryTimerRef.current = null; + } + + if (!isConnected || !primaryAddress) { setLoading(false); setNetworkName(null); setBalance(null); @@ -33,73 +152,16 @@ export default function WalletInfoSection() { return; } - let cancelled = false; setLoading(true); setError(null); - (async () => { - try { - const listRes = await skillManager.callTool('wallet', 'list_networks', {}); - const listText = listRes.content?.[0]?.text; - if (!listText || listRes.isError) { - if (!cancelled) setError('Could not load networks'); - return; - } - const listData = JSON.parse(listText) as { networks?: Array<{ chain_id?: string; name?: string; chain_type?: string }> }; - const networks = Array.isArray(listData.networks) ? listData.networks : []; - const firstEvm = networks.find((n) => n && n.chain_type === 'evm'); - const first = firstEvm ?? networks.find(Boolean); - if (!first || cancelled) return; - - const networkNameVal = first.name ?? first.chain_id ?? 'Unknown'; - if (!cancelled) setNetworkName(networkNameVal); - - const chainId = first.chain_id ?? ''; - if (!chainId) { - if (!cancelled) setBalance('—'); - return; - } - - const balanceRes = await skillManager.callTool('wallet', 'get_balance', { - address: primaryAddress, - chain_id: chainId, - chain_type: first.chain_type ?? 'evm', - }); - const balanceText = balanceRes.content?.[0]?.text; - if (!balanceText || balanceRes.isError) { - if (!cancelled) setBalance('—'); - return; - } - const balanceData = JSON.parse(balanceText) as { - balance_eth?: string; - symbol?: string; - error?: string; - }; - if (balanceData.error && !cancelled) { - setBalance('—'); - return; - } - const eth = balanceData.balance_eth ?? '0'; - const symbol = balanceData.symbol ?? 'ETH'; - const value = parseFloat(eth); - const display = value < 0.0001 ? '0' : value.toFixed(4); - if (!cancelled) setBalance(`${display} ${symbol}`); - } catch (e) { - console.error(e); - if (!cancelled) { - setError(e instanceof Error ? e.message : 'Failed to load wallet info'); - setBalance(null); - setNetworkName(null); - } - } finally { - if (!cancelled) setLoading(false); - } - })(); + fetchWalletInfo(primaryAddress); return () => { - cancelled = true; + cancelledRef.current = true; + if (retryTimerRef.current) clearTimeout(retryTimerRef.current); }; - }, [isConnected, primaryAddress]); + }, [isConnected, primaryAddress, fetchWalletInfo]); if (!isConnected) return null; diff --git a/src/lib/skills/manager.ts b/src/lib/skills/manager.ts index e3140204c..a3091ed9b 100644 --- a/src/lib/skills/manager.ts +++ b/src/lib/skills/manager.ts @@ -17,6 +17,7 @@ import type { SkillOptionDefinition, } from "./types"; import { store } from "../../store"; +import { setPrimaryWalletAddressForUser } from "../../store/authSlice"; import { addSkill, setSkillStatus, @@ -375,6 +376,24 @@ class SkillManager { } } + /** + * Set the wallet address in the frontend app and notify the wallet skill (onLoad). + * Updates Redux (primaryWalletAddressByUser) and, if the wallet skill is running, + * sends load params so the skill receives onLoad({ walletAddress }). + */ + async setWalletAddress(address: string): Promise { + const state = store.getState(); + const userId = state.user.user?._id; + if (!userId) { + return; + } + store.dispatch(setPrimaryWalletAddressForUser({ userId, address })); + const runtime = this.runtimes.get("wallet"); + if (runtime?.isRunning) { + await runtime.load({ walletAddress: address }); + } + } + // ----------------------------------------------------------------------- // Reverse RPC handling // ----------------------------------------------------------------------- diff --git a/src/pages/Mnemonic.tsx b/src/pages/Mnemonic.tsx index 4cf197837..25cd9bcfa 100644 --- a/src/pages/Mnemonic.tsx +++ b/src/pages/Mnemonic.tsx @@ -2,7 +2,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import LottieAnimation from '../components/LottieAnimation'; -import { setEncryptionKeyForUser, setPrimaryWalletAddressForUser } from '../store/authSlice'; +import { skillManager } from '../lib/skills/manager'; +import { setEncryptionKeyForUser } from '../store/authSlice'; import { useAppDispatch, useAppSelector } from '../store/hooks'; import { deriveAesKeyFromMnemonic, @@ -151,7 +152,7 @@ const Mnemonic = () => { if (user?._id) { dispatch(setEncryptionKeyForUser({ userId: user._id, key: aesKey })); - dispatch(setPrimaryWalletAddressForUser({ userId: user._id, address: walletAddress })); + await skillManager.setWalletAddress(walletAddress); } navigate('/home');