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.
This commit is contained in:
M3gA-Mind
2026-02-10 15:52:59 +05:30
parent a1ae34ad2f
commit a6da2eb291
4 changed files with 156 additions and 71 deletions
+8 -5
View File
@@ -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';
}
+126 -64
View File
@@ -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<string | null>(null);
const [balance, setBalance] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const isConnected = walletStatus === 'connected' && !!primaryAddress;
const cancelledRef = useRef(false);
const retryTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const fetchWalletInfoRef = useRef<(address: string, attempt?: number) => Promise<void>>(null!);
const fetchWalletInfo = useCallback(async (address: string, attempt = 0): Promise<void> => {
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;
+19
View File
@@ -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<void> {
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
// -----------------------------------------------------------------------
+3 -2
View File
@@ -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');