Skip to content

Commit 5bc8dd7

Browse files
committed
Add comprehensive wallet health monitoring and fix API call spamming
- Implement wallet health check API integration with /panel-health endpoint - Add real-time wallet status indicators with proper theming - Fix transaction size calculation spamming by optimizing useEffect dependencies - Add request deduplication to prevent multiple simultaneous API calls - Remove wallet lock requirement (healthy + synced = ready to transact) - Improve authentication retry logic for both health checks and transaction calculations - Add proper error handling and user feedback for wallet connectivity issues
1 parent 780ac69 commit 5bc8dd7

File tree

2 files changed

+244
-6
lines changed

2 files changed

+244
-6
lines changed

src/components/relay-dashboard/Balance/components/SendForm/SendForm.tsx

Lines changed: 169 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { BaseInput } from '@app/components/common/inputs/BaseInput/BaseInput';
33
import { BaseRow } from '@app/components/common/BaseRow/BaseRow';
44
import { BaseButton } from '@app/components/common/BaseButton/BaseButton';
55
import { BaseSpin } from '@app/components/common/BaseSpin/BaseSpin';
6+
import { Alert } from 'antd';
67
import * as S from './SendForm.styles';
78
import { truncateString } from '@app/utils/utils';
89
import useBalanceData from '@app/hooks/useBalanceData';
@@ -29,7 +30,7 @@ export type tiers = 'low' | 'med' | 'high';
2930

3031
const SendForm: React.FC<SendFormProps> = ({ onSend }) => {
3132
const { balanceData, isLoading } = useBalanceData();
32-
const { isAuthenticated, login, token, loading: authLoading } = useWalletAuth(); // Use the auth hook
33+
const { isAuthenticated, login, token, loading: authLoading, checkWalletHealth, walletHealth, healthLoading } = useWalletAuth(); // Use the auth hook
3334

3435
const [loading, setLoading] = useState(false);
3536

@@ -48,6 +49,7 @@ const SendForm: React.FC<SendFormProps> = ({ onSend }) => {
4849
});
4950

5051
const [txSize, setTxSize] = useState<number | null>(null);
52+
const [txSizeCalculating, setTxSizeCalculating] = useState(false);
5153

5254
const [enableRBF, setEnableRBF] = useState(false); // Default to false
5355

@@ -59,18 +61,42 @@ const SendForm: React.FC<SendFormProps> = ({ onSend }) => {
5961
return validateBech32Address(address);
6062
}, []);
6163

64+
// Health check when component mounts or when authentication status changes
65+
useEffect(() => {
66+
if (isAuthenticated) {
67+
checkWalletHealth();
68+
}
69+
// eslint-disable-next-line react-hooks/exhaustive-deps
70+
}, [isAuthenticated]); // Only depend on isAuthenticated to prevent excessive calls
71+
6272
// First useEffect - Transaction size calculation
6373
useEffect(() => {
6474
const debounceTimeout = setTimeout(() => {
6575
const fetchTransactionSize = async () => {
6676
if (isValidAddress(formData.address) && isDetailsOpen) {
77+
// Prevent multiple simultaneous transaction size calculations
78+
if (txSizeCalculating) {
79+
console.log('Transaction size calculation already in progress, skipping');
80+
return;
81+
}
82+
6783
try {
84+
setTxSizeCalculating(true);
85+
6886
if (!isAuthenticated) {
6987
console.log('Not Authenticated.');
7088
await login();
89+
return;
90+
}
91+
92+
// Check wallet health before making transaction calculations
93+
const health = await checkWalletHealth();
94+
if (!health || health.status !== 'healthy' || !health.chain_synced) {
95+
console.log('Wallet not ready (unhealthy or not synced), skipping transaction calculation');
96+
return;
7197
}
7298

73-
const response = await fetch(`${config.walletBaseURL}/calculate-tx-size`, {
99+
let response = await fetch(`${config.walletBaseURL}/calculate-tx-size`, {
74100
method: 'POST',
75101
headers: {
76102
'Content-Type': 'application/json',
@@ -83,21 +109,43 @@ const SendForm: React.FC<SendFormProps> = ({ onSend }) => {
83109
}),
84110
});
85111

112+
// Handle 401 by re-authenticating and retrying
86113
if (response.status === 401) {
87114
const errorText = await response.text();
88115
if (errorText.includes('Token expired') || errorText.includes('Unauthorized: Invalid token')) {
89-
console.log('Session expired. Please log in again.');
116+
console.log('Session expired. Re-authenticating and retrying...');
90117
deleteWalletToken();
91118
await login();
119+
120+
// Retry the request with the new token
121+
response = await fetch(`${config.walletBaseURL}/calculate-tx-size`, {
122+
method: 'POST',
123+
headers: {
124+
'Content-Type': 'application/json',
125+
Authorization: `Bearer ${token}`,
126+
},
127+
body: JSON.stringify({
128+
recipient_address: formData.address,
129+
spend_amount: parseInt(formData.amount),
130+
priority_rate: feeRate,
131+
}),
132+
});
133+
134+
if (!response.ok) {
135+
throw new Error(`HTTP error! status: ${response.status}`);
136+
}
137+
} else {
138+
throw new Error(errorText);
92139
}
93-
throw new Error(errorText);
94140
}
95141

96142
const result = await response.json();
97143
setTxSize(result.txSize);
98144
} catch (error) {
99145
console.error('Error fetching transaction size:', error);
100146
setTxSize(null);
147+
} finally {
148+
setTxSizeCalculating(false);
101149
}
102150
}
103151
};
@@ -106,7 +154,8 @@ const SendForm: React.FC<SendFormProps> = ({ onSend }) => {
106154
}, 500);
107155

108156
return () => clearTimeout(debounceTimeout);
109-
}, [formData.address, formData.amount, feeRate, isAuthenticated, login, token, isDetailsOpen, isValidAddress]);
157+
// eslint-disable-next-line react-hooks/exhaustive-deps
158+
}, [formData.address, formData.amount, feeRate, isDetailsOpen]); // Only depend on actual form changes
110159

111160
// Second useEffect - Fee calculation
112161
useEffect(() => {
@@ -172,6 +221,12 @@ const SendForm: React.FC<SendFormProps> = ({ onSend }) => {
172221
const handleSend = async () => {
173222
if (loading || inValidAmount) return;
174223

224+
// Check wallet health before allowing transaction
225+
if (!isAuthenticated || !walletHealth || walletHealth.status !== 'healthy' ||
226+
!walletHealth.chain_synced) {
227+
return; // Don't proceed if wallet is not ready
228+
}
229+
175230
setLoading(true);
176231

177232
const selectedFee = feeRate; // The user-selected fee rate
@@ -308,7 +363,7 @@ const SendForm: React.FC<SendFormProps> = ({ onSend }) => {
308363
</S.TiersContainer>
309364
<BaseRow justify={'center'}>
310365
<S.SendFormButton
311-
disabled={loading || isLoading || inValidAmount || authLoading || addressError}
366+
disabled={loading || isLoading || inValidAmount || authLoading || addressError || !isWalletReady}
312367
onClick={handleSend}
313368
size="large"
314369
type="primary"
@@ -319,11 +374,119 @@ const SendForm: React.FC<SendFormProps> = ({ onSend }) => {
319374
</S.FormSpacer>
320375
);
321376

377+
// Check if wallet is ready for transactions (healthy + synced = ready)
378+
const isWalletReady = isAuthenticated && walletHealth && walletHealth.status === 'healthy' &&
379+
walletHealth.chain_synced;
380+
381+
// Render wallet status indicator
382+
const renderWalletStatus = () => {
383+
if (!isAuthenticated) {
384+
return (
385+
<Alert
386+
message="Wallet Not Connected"
387+
description="Please authenticate with your wallet to access transaction features."
388+
type="warning"
389+
showIcon
390+
style={{
391+
marginBottom: '1rem',
392+
backgroundColor: 'var(--background-color-secondary)',
393+
border: '1px solid var(--border-color-base)',
394+
color: 'var(--text-main-color)'
395+
}}
396+
/>
397+
);
398+
}
399+
400+
if (healthLoading) {
401+
return (
402+
<Alert
403+
message="Checking Wallet Status..."
404+
type="info"
405+
showIcon
406+
style={{
407+
marginBottom: '1rem',
408+
backgroundColor: 'var(--background-color-secondary)',
409+
border: '1px solid var(--border-color-base)',
410+
color: 'var(--text-main-color)'
411+
}}
412+
/>
413+
);
414+
}
415+
416+
if (!walletHealth) {
417+
return (
418+
<Alert
419+
message="Wallet Status Unknown"
420+
description="Unable to connect to wallet service. Please check if the wallet is running."
421+
type="error"
422+
showIcon
423+
style={{
424+
marginBottom: '1rem',
425+
backgroundColor: 'var(--background-color-secondary)',
426+
border: '1px solid var(--border-color-base)',
427+
color: 'var(--text-main-color)'
428+
}}
429+
/>
430+
);
431+
}
432+
433+
if (walletHealth.status !== 'healthy') {
434+
return (
435+
<Alert
436+
message="Wallet Unhealthy"
437+
description="The wallet service is not functioning properly."
438+
type="error"
439+
showIcon
440+
style={{
441+
marginBottom: '1rem',
442+
backgroundColor: 'var(--background-color-secondary)',
443+
border: '1px solid var(--border-color-base)',
444+
color: 'var(--text-main-color)'
445+
}}
446+
/>
447+
);
448+
}
449+
450+
if (!walletHealth.chain_synced) {
451+
return (
452+
<Alert
453+
message="Blockchain Not Synced"
454+
description={`The wallet is still syncing with the blockchain. Connected to ${walletHealth.peer_count} peers. Please wait for sync to complete before sending transactions.`}
455+
type="warning"
456+
showIcon
457+
style={{
458+
marginBottom: '1rem',
459+
backgroundColor: 'var(--background-color-secondary)',
460+
border: '1px solid var(--border-color-base)',
461+
color: 'var(--text-main-color)'
462+
}}
463+
/>
464+
);
465+
}
466+
467+
// Wallet is healthy and synced
468+
return (
469+
<Alert
470+
message="Wallet Ready"
471+
description={`Wallet is online and synced. Connected to ${walletHealth.peer_count} peers.`}
472+
type="success"
473+
showIcon
474+
style={{
475+
marginBottom: '1rem',
476+
backgroundColor: 'var(--background-color-secondary)',
477+
border: '1px solid var(--border-color-base)',
478+
color: 'var(--text-main-color)'
479+
}}
480+
/>
481+
);
482+
};
483+
322484
return (
323485
<BaseSpin spinning={isLoading || loading || authLoading}>
324486
<S.SendBody justify={'center'}>
325487
<S.FormSpacer>
326488
<S.FormHeader>Send</S.FormHeader>
489+
{renderWalletStatus()}
327490
{isDetailsOpen ? (
328491
<>
329492
<S.Recipient>

src/hooks/useWalletAuth.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
11
import { useEffect, useState } from 'react';
22
import { persistWalletToken, readWalletToken, deleteWalletToken } from '@app/services/localStorage.service'; // Import the wallet-specific functions
33
import { notificationController } from '@app/controllers/notificationController';
4+
import config from '@app/config/config';
5+
6+
interface WalletHealth {
7+
status: 'healthy' | 'unhealthy';
8+
timestamp: string;
9+
wallet_locked: boolean;
10+
chain_synced: boolean;
11+
peer_count: number;
12+
}
413

514
const useWalletAuth = () => {
615
const [token, setToken] = useState<string | null>(null);
716
const [isAuthenticated, setIsAuthenticated] = useState(false);
817
const [loading, setLoading] = useState(false);
18+
const [walletHealth, setWalletHealth] = useState<WalletHealth | null>(null);
19+
const [healthLoading, setHealthLoading] = useState(false);
20+
const [healthCheckInProgress, setHealthCheckInProgress] = useState(false);
921

1022
// Fetch the wallet token from localStorage on mount
1123
useEffect(() => {
@@ -79,11 +91,71 @@ const useWalletAuth = () => {
7991
}
8092
};
8193

94+
// Check wallet health
95+
const checkWalletHealth = async () => {
96+
if (!isAuthenticated || !token) {
97+
console.log('Not authenticated, skipping health check');
98+
return null;
99+
}
100+
101+
// Prevent multiple simultaneous health checks
102+
if (healthCheckInProgress) {
103+
console.log('Health check already in progress, skipping');
104+
return walletHealth;
105+
}
106+
107+
setHealthCheckInProgress(true);
108+
setHealthLoading(true);
109+
try {
110+
let response = await fetch(`${config.walletBaseURL}/panel-health`, {
111+
method: 'GET',
112+
headers: {
113+
'Authorization': `Bearer ${token}`,
114+
},
115+
});
116+
117+
// Handle 401 by re-authenticating and retrying (same as calculate-tx-size)
118+
if (response.status === 401) {
119+
console.log('Health check failed: token expired. Re-authenticating and retrying...');
120+
deleteWalletToken();
121+
await login();
122+
123+
// Retry the request with the new token
124+
response = await fetch(`${config.walletBaseURL}/panel-health`, {
125+
method: 'GET',
126+
headers: {
127+
'Authorization': `Bearer ${token}`,
128+
},
129+
});
130+
131+
if (!response.ok) {
132+
throw new Error(`HTTP error! status: ${response.status}`);
133+
}
134+
}
135+
136+
if (!response.ok) {
137+
throw new Error(`HTTP error! status: ${response.status}`);
138+
}
139+
140+
const healthData: WalletHealth = await response.json();
141+
setWalletHealth(healthData);
142+
return healthData;
143+
} catch (error) {
144+
console.error('Error checking wallet health:', error);
145+
setWalletHealth(null);
146+
return null;
147+
} finally {
148+
setHealthLoading(false);
149+
setHealthCheckInProgress(false);
150+
}
151+
};
152+
82153
// Logout and clear wallet token
83154
const logout = () => {
84155
deleteWalletToken(); // Use the wallet-specific token deletion
85156
setToken(null);
86157
setIsAuthenticated(false);
158+
setWalletHealth(null);
87159
};
88160

89161
return {
@@ -92,6 +164,9 @@ const useWalletAuth = () => {
92164
login,
93165
logout,
94166
loading,
167+
checkWalletHealth,
168+
walletHealth,
169+
healthLoading,
95170
};
96171
};
97172

0 commit comments

Comments
 (0)