Skip to content

Commit d812544

Browse files
committed
feat: add maximum deposit cap warning
- show warning at >90% utilization - disable deposit at cap - fetch and refresh cap data Closes #249
1 parent 34f1781 commit d812544

5 files changed

Lines changed: 86 additions & 61 deletions

File tree

frontend/public/mock-api/vault-summary.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"tvl": 12450800,
3+
"depositCap": 13500000,
34
"apy": 8.45,
45
"participantCount": 1248,
56
"monthlyGrowthPct": 12.5,

frontend/src/components/VaultDashboard.tsx

Lines changed: 68 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import React, { useEffect, useState } from "react";
2-
import { useState, useEffect } from "react";
3-
import { Activity, ShieldCheck, TrendingUp, Wallet as WalletIcon } from "./icons";
2+
import { Activity, ShieldCheck, TrendingUp, Wallet as WalletIcon, AlertTriangle, Info } from "./icons";
43
import { hasCustomRpcConfig, networkConfig } from "../config/network";
54
import { useVault } from "../context/VaultContext";
65
import ApiStatusBanner from "./ApiStatusBanner";
@@ -11,14 +10,43 @@ import { FormField, SubmitButton } from "../forms";
1110
import CopyButton from "./CopyButton";
1211
import { useDepositMutation, useWithdrawMutation } from "../hooks/useVaultMutations";
1312
import TransactionStatus, { type ActionStatus } from "./TransactionStatus";
14-
import { useDepositMutation, useWithdrawMutation } from "../hooks/useVaultMutations";
15-
import CopyButton from "./CopyButton";
1613

1714
interface VaultDashboardProps {
1815
walletAddress: string | null;
1916
usdcBalance?: number;
2017
}
2118

19+
const VaultCapWarning: React.FC<{ utilization: number; isReached: boolean }> = ({ utilization, isReached }) => {
20+
const percent = (utilization * 100).toFixed(1);
21+
22+
return (
23+
<div
24+
className="glass-panel"
25+
style={{
26+
padding: "16px",
27+
marginBottom: "24px",
28+
border: `1px solid ${isReached ? 'var(--text-error)' : 'var(--text-warning)'}`,
29+
background: isReached ? 'rgba(255, 69, 58, 0.1)' : 'rgba(255, 159, 10, 0.1)',
30+
display: "flex",
31+
alignItems: "flex-start",
32+
gap: "12px"
33+
}}
34+
>
35+
{isReached ? <AlertTriangle color="var(--text-error)" size={20} /> : <Info color="var(--text-warning)" size={20} />}
36+
<div>
37+
<div style={{ fontWeight: 600, color: isReached ? 'var(--text-error)' : 'var(--text-warning)', marginBottom: "4px" }}>
38+
{isReached ? 'Vault Capacity Reached' : 'Vault Near Capacity'}
39+
</div>
40+
<div style={{ fontSize: "0.85rem", color: "var(--text-secondary)", lineHeight: "1.4" }}>
41+
{isReached
42+
? `This vault has reached its maximum deposit cap of ${percent}%. Deposits are temporarily disabled.`
43+
: `This vault is at ${percent}% capacity. New deposits may be restricted soon.`}
44+
</div>
45+
</div>
46+
</div>
47+
);
48+
};
49+
2250
function buildFakeTxHash(walletAddress: string, action: "deposit" | "withdraw", amount: number): string {
2351
const seed = `${walletAddress}-${action}-${amount.toFixed(2)}-${Date.now()}`;
2452
let hash = "";
@@ -304,72 +332,58 @@ const VaultDashboard: React.FC<VaultDashboardProps> = ({
304332

305333
{(["deposit", "withdraw"] as const).map((tab) => (
306334
<TabsContent key={tab} value={tab}>
335+
{(isCapReached || isCapWarning) && tab === "deposit" && (
336+
<VaultCapWarning utilization={utilization} isReached={isCapReached} />
337+
)}
307338
<form
308339
onSubmit={(event) => {
309340
event.preventDefault();
310341
void handleTransaction(tab);
311342
}}
312343
>
313-
<div style={{ marginBottom: "24px" }}>
314-
<div className="flex justify-between items-center" style={{ marginBottom: "16px" }}>
315-
<div style={{ color: "var(--text-secondary)", fontSize: "0.9rem" }}>
316-
{tab === "deposit" ? "Amount to deposit" : "Amount to withdraw"}
317-
</div>
318-
<div style={{ color: "var(--text-secondary)", fontSize: "0.85rem" }}>
319-
Balance:{" "}
320-
<span style={{ color: "var(--text-primary)", fontWeight: 600 }}>
321-
{walletAddress ? availableBalance.toFixed(2) : "0.00"}
322-
</span>
344+
<div style={{ marginBottom: "24px" }}>
345+
<div className="flex justify-between items-center" style={{ marginBottom: "16px" }}>
346+
<div style={{ color: "var(--text-secondary)", fontSize: "0.9rem" }}>
347+
{tab === "deposit" ? "Amount to deposit" : "Amount to withdraw"}
348+
</div>
349+
<div style={{ color: "var(--text-secondary)", fontSize: "0.85rem" }}>
350+
Balance:{" "}
351+
<span style={{ color: "var(--text-primary)", fontWeight: 600 }}>
352+
{walletAddress ? availableBalance.toFixed(2) : "0.00"}
353+
</span>
354+
</div>
323355
</div>
324-
</div>
325356

326-
<FormField
327-
label={tab === "deposit" ? "Deposit amount" : "Withdrawal amount"}
328-
name={`${tab}-amount`}
329-
type="number"
330-
placeholder="0.00"
331-
value={amount}
332-
onChange={(event) => setAmount(event.target.value)}
333-
disabled={isBusy}
334-
/>
357+
<FormField
358+
label={tab === "deposit" ? "Deposit amount" : "Withdrawal amount"}
359+
name={`${tab}-amount`}
360+
type="number"
361+
placeholder="0.00"
362+
value={amount}
363+
onChange={(event) => setAmount(event.target.value)}
364+
disabled={isBusy || (tab === "deposit" && isCapReached)}
365+
/>
335366

336-
<div className="flex justify-between items-center" style={{ margin: "16px 0 24px" }}>
337-
<span style={{ color: "var(--text-secondary)", fontSize: "0.85rem" }}>Asset: USDC</span>
338-
<button
339-
type="button"
340-
className="btn btn-outline"
341-
onClick={() => setAmount(availableBalance.toFixed(2))}
342-
disabled={!walletAddress || availableBalance <= 0 || isBusy}
343-
>
344-
MAX
345-
</button>
367+
<div className="flex justify-between items-center" style={{ margin: "16px 0 24px" }}>
368+
<span style={{ color: "var(--text-secondary)", fontSize: "0.85rem" }}>Asset: USDC</span>
369+
<button
370+
type="button"
371+
className="btn btn-outline"
372+
onClick={() => setAmount(availableBalance.toFixed(2))}
373+
disabled={!walletAddress || availableBalance <= 0 || isBusy || (tab === "deposit" && isCapReached)}
374+
>
375+
MAX
376+
</button>
377+
</div>
346378
</div>
347379

348380
<SubmitButton
349381
loading={isBusy && activeTab === tab}
350-
disabled={!walletAddress || isBusy || !amount || Number(amount) <= 0}
351-
label={tab === "deposit" ? "Approve & Deposit" : "Withdraw Funds"}
382+
disabled={!walletAddress || isBusy || !amount || Number(amount) <= 0 || (tab === "deposit" && isCapReached)}
383+
label={tab === "deposit" ? (isCapReached ? "Vault is full" : "Approve & Deposit") : "Withdraw Funds"}
352384
loadingLabel="Waiting for confirmation..."
353385
/>
354386
</form>
355-
Balance: <span style={{ color: "var(--text-primary)", fontWeight: 600 }}>{availableBalance.toFixed(2)}</span>
356-
</div>
357-
</div>
358-
359-
<div className="input-group">
360-
<div className="input-wrapper">
361-
<span style={{ color: "var(--text-secondary)", paddingRight: "12px", borderRight: "1px solid var(--border-glass)", marginRight: "16px" }}>USDC</span>
362-
<input className="input-field" type="number" placeholder="0.00" value={amount} onChange={(e) => setAmount(e.target.value)} disabled={isProcessing !== null} />
363-
<button className="btn-max" onClick={() => setAmount(availableBalance.toFixed(2))} disabled={!walletAddress || availableBalance <= 0 || isProcessing !== null}>
364-
MAX
365-
</button>
366-
</div>
367-
</div>
368-
</div>
369-
370-
<button className="btn btn-primary" style={{ width: "100%", padding: "16px" }} onClick={() => handleTransaction(tab)} disabled={isProcessing !== null || !amount || Number(amount) <= 0}>
371-
{isProcessing === tab ? "Processing..." : tab === "deposit" ? "Approve & Deposit" : "Withdraw Funds"}
372-
</button>
373387
</TabsContent>
374388
))}
375389
</Tabs>

frontend/src/context/VaultContext.tsx

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,19 @@ import React, {
33
useContext,
44
useEffect,
55
} from "react";
6+
import { subscribeToApiTelemetry, normalizeApiError } from "../lib/api";
67
import type { ApiError } from "../lib/api";
7-
import { subscribeToApiTelemetry } from "../lib/api";
88
import type { VaultSummary } from "../lib/vaultApi";
99
import { networkConfig } from "../config/network";
1010
import { useVaultSummary } from "../hooks/useVaultData";
1111

1212
interface VaultContextType {
1313
summary: VaultSummary;
1414
tvl: number;
15+
depositCap: number;
16+
utilization: number;
17+
isCapWarning: boolean;
18+
isCapReached: boolean;
1519
apy: number;
1620
formattedTvl: string;
1721
formattedApy: string;
@@ -23,6 +27,7 @@ interface VaultContextType {
2327

2428
const DEFAULT_SUMMARY: VaultSummary = {
2529
tvl: 12450800,
30+
depositCap: 15000000,
2631
apy: 8.45,
2732
participantCount: 1248,
2833
monthlyGrowthPct: 12.5,
@@ -61,16 +66,15 @@ export const VaultProvider: React.FC<{ children: React.ReactNode }> = ({
6166
: DEFAULT_SUMMARY;
6267

6368
const error: ApiError | null = queryError
64-
? {
65-
code: "FETCH_ERROR",
66-
message: queryError.message,
67-
userMessage: "Failed to load vault data",
68-
statusCode: 500,
69-
}
69+
? normalizeApiError(queryError)
7070
: null;
7171

7272
const lastUpdate = new Date(summary.updatedAt);
7373

74+
const utilization = summary.depositCap > 0 ? summary.tvl / summary.depositCap : 0;
75+
const isCapWarning = utilization > 0.9 && utilization < 1.0;
76+
const isCapReached = utilization >= 1.0;
77+
7478
useEffect(() => {
7579
const unsubscribe = subscribeToApiTelemetry((event) => {
7680
if (event.type === "error") {
@@ -98,6 +102,10 @@ export const VaultProvider: React.FC<{ children: React.ReactNode }> = ({
98102
value={{
99103
summary,
100104
tvl: summary.tvl,
105+
depositCap: summary.depositCap,
106+
utilization,
107+
isCapWarning,
108+
isCapReached,
101109
apy: summary.apy,
102110
formattedTvl,
103111
formattedApy,

frontend/src/hooks/useVaultData.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export function useVaultSummary() {
1111
queryKey: queryKeys.vault.summary(),
1212
queryFn: getVaultSummary,
1313
staleTime: 30000, // 30 seconds
14+
refetchInterval: 30000, // 30 seconds
1415
});
1516
}
1617

frontend/src/lib/vaultApi.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface StrategyMetadata {
1313

1414
export interface VaultSummary {
1515
tvl: number;
16+
depositCap: number;
1617
apy: number;
1718
participantCount: number;
1819
monthlyGrowthPct: number;

0 commit comments

Comments
 (0)