From 88431ef177b9750c6cb1fafd42553b1682f4761f Mon Sep 17 00:00:00 2001 From: antoncoding Date: Mon, 26 Jan 2026 10:29:42 +0800 Subject: [PATCH 1/6] refactor: initializtion --- .../modals/vault-initialization-modal.tsx | 7 +- src/features/autovault/vault-view.tsx | 25 ++- src/hooks/useVaultIndexing.tsx | 154 +++++++----------- src/stores/vault-indexing-store.ts | 46 ++++++ src/utils/vault-indexing.ts | 75 --------- 5 files changed, 122 insertions(+), 185 deletions(-) create mode 100644 src/stores/vault-indexing-store.ts delete mode 100644 src/utils/vault-indexing.ts diff --git a/src/features/autovault/components/vault-detail/modals/vault-initialization-modal.tsx b/src/features/autovault/components/vault-detail/modals/vault-initialization-modal.tsx index 84d8c7a6..a233aa1d 100644 --- a/src/features/autovault/components/vault-detail/modals/vault-initialization-modal.tsx +++ b/src/features/autovault/components/vault-detail/modals/vault-initialization-modal.tsx @@ -18,7 +18,7 @@ import { useMorphoMarketV1Adapters } from '@/hooks/useMorphoMarketV1Adapters'; import { v2AgentsBase } from '@/utils/monarch-agent'; import { getMorphoAddress } from '@/utils/morpho'; import { ALL_SUPPORTED_NETWORKS, SupportedNetworks, getNetworkConfig } from '@/utils/networks'; -import { startVaultIndexing } from '@/utils/vault-indexing'; +import { useVaultIndexingStore } from '@/stores/vault-indexing-store'; import { useVaultInitializationModalStore } from '@/stores/vault-initialization-modal-store'; const ZERO_ADDRESS = zeroAddress; @@ -194,6 +194,7 @@ const MAX_SYMBOL_LENGTH = 16; export function VaultInitializationModal() { // Modal state from Zustand (UI state) const { isOpen, close } = useVaultInitializationModalStore(); + const { startIndexing } = useVaultIndexingStore(); // Get vault address and chain ID from URL params const { chainId: chainIdParam, vaultAddress } = useParams<{ @@ -331,7 +332,7 @@ export function VaultInitializationModal() { } // Start indexing mode - vault page will handle retry logic - startVaultIndexing(vaultAddress, chainId); + startIndexing(vaultAddressValue, chainId); // Trigger initial refetch void vaultDataQuery.refetch(); @@ -348,12 +349,14 @@ export function VaultInitializationModal() { vaultContract, refetchAdapter, close, + startIndexing, registryAddress, selectedAgent, adapterAddress, vaultName, vaultSymbol, vaultAddress, + vaultAddressValue, chainId, ]); diff --git a/src/features/autovault/vault-view.tsx b/src/features/autovault/vault-view.tsx index 10b7c5b8..ee3d19fd 100644 --- a/src/features/autovault/vault-view.tsx +++ b/src/features/autovault/vault-view.tsx @@ -78,12 +78,16 @@ export default function VaultContent() { connectedAddress, }); - // Aggregated refetch function + // Stabilize refetch references to prevent effect churn in useVaultIndexing + const refetchVaultData = vaultDataQuery.refetch; + const refetchVaultContract = vaultContract.refetch; + const refetchAdapters = adapterQuery.refetch; + const handleRefreshVault = useCallback(() => { - void vaultDataQuery.refetch(); - void vaultContract.refetch(); - void adapterQuery.refetch(); - }, [vaultDataQuery, vaultContract, adapterQuery]); + void refetchVaultData(); + void refetchVaultContract(); + void refetchAdapters(); + }, [refetchVaultData, refetchVaultContract, refetchAdapters]); const isRefetching = vaultDataQuery.isRefetching || vaultContract.isRefetching || adapterQuery.isRefetching; @@ -94,16 +98,19 @@ export default function VaultContent() { const title = vaultData?.displayName ?? `Vault ${getSlicedAddress(vaultAddressValue)}`; const symbolToDisplay = vaultData?.displaySymbol; - // Determine if vault data has loaded successfully - const isDataLoaded = useMemo(() => { - return !vaultDataLoading && !hasError && vaultData !== null; + // Determine if vault data reflects post-initialization state. + // After initialization, adapters[] must be non-empty in the API response. + // This prevents the indexing system from exiting early with stale pre-init data. + const hasPostInitData = useMemo(() => { + if (vaultDataLoading || hasError || !vaultData) return false; + return vaultData.adapters.length > 0; }, [vaultDataLoading, hasError, vaultData]); // Use indexing hook to manage retry logic and toast const { isIndexing } = useVaultIndexing({ vaultAddress: vaultAddressValue, chainId, - isDataLoaded, + hasPostInitData, refetch: handleRefreshVault, }); diff --git a/src/hooks/useVaultIndexing.tsx b/src/hooks/useVaultIndexing.tsx index 6b68585d..d0d471df 100644 --- a/src/hooks/useVaultIndexing.tsx +++ b/src/hooks/useVaultIndexing.tsx @@ -1,63 +1,43 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import { toast } from 'react-toastify'; import type { Address } from 'viem'; import { StyledToast } from '@/components/ui/styled-toast'; -import { getIndexingVault, stopVaultIndexing } from '@/utils/vault-indexing'; +import { INDEXING_TIMEOUT_MS, useVaultIndexingStore } from '@/stores/vault-indexing-store'; import { useStyledToast } from './useStyledToast'; type UseVaultIndexingArgs = { vaultAddress: Address; chainId: number; - isDataLoaded: boolean; + hasPostInitData: boolean; refetch: () => void; }; +const REFETCH_INTERVAL_MS = 5_000; + /** * Hook to manage vault indexing state after initialization. * Shows a persistent toast and retries fetching data every 5 seconds - * until the vault is indexed or timeout is reached (2 minutes). + * until post-initialization data arrives or timeout is reached (2 minutes). + * + * Uses Zustand store for instant reactivity (no localStorage polling). */ -export function useVaultIndexing({ vaultAddress, chainId, isDataLoaded, refetch }: UseVaultIndexingArgs) { - const [isIndexing, setIsIndexing] = useState(false); - const refetchIntervalRef = useRef>(); +export function useVaultIndexing({ vaultAddress, chainId, hasPostInitData, refetch }: UseVaultIndexingArgs) { + // Use selectors for individual pieces to avoid subscribing to the entire store + const indexingVault = useVaultIndexingStore((s) => s.indexingVault); + const stopIndexing = useVaultIndexingStore((s) => s.stopIndexing); const toastIdRef = useRef(); - const hasDetectedIndexing = useRef(false); const { info: styledInfo, success: styledSuccess } = useStyledToast(); - // Poll localStorage to detect when indexing state is set - // This ensures we pick up indexing state even if it's set after component mount - useEffect(() => { - // Reset detection flag when vault or chain changes - hasDetectedIndexing.current = false; - - // Immediate check on mount - const indexingData = getIndexingVault(vaultAddress, chainId); - if (indexingData && !hasDetectedIndexing.current) { - hasDetectedIndexing.current = true; - setIsIndexing(true); - } - - // Continue polling for state changes - const pollingInterval = setInterval(() => { - const data = getIndexingVault(vaultAddress, chainId); - if (data && !hasDetectedIndexing.current) { - hasDetectedIndexing.current = true; - setIsIndexing(true); - } - }, 1000); - - // Always return cleanup function - return () => { - clearInterval(pollingInterval); - }; - }, [vaultAddress, chainId]); + // Derive isIndexing from the store state + // Time-based expiry is handled by the interval in Effect 2 (not reactive to time passing) + const isIndexing = useMemo(() => { + if (!indexingVault) return false; + return indexingVault.address.toLowerCase() === vaultAddress.toLowerCase() && indexingVault.chainId === chainId; + }, [indexingVault, vaultAddress, chainId]); - // Handle indexing toast and retry logic + // Effect 1: Show/dismiss the persistent "indexing" toast useEffect(() => { - if (!isIndexing) return; - - // Show persistent toast when indexing starts (only once) - if (!toastIdRef.current) { + if (isIndexing && !toastIdRef.current) { toastIdRef.current = toast.info( { - // Check if timeout reached (auto-cleanup happens in getIndexingVault) - const stillIndexing = getIndexingVault(vaultAddress, chainId); - - if (!stillIndexing) { - // Timeout reached - setIsIndexing(false); - hasDetectedIndexing.current = false; + // Effect 2: Refetch interval + timeout while indexing. + // Only re-runs when isIndexing changes (true→false or false→true). + // refetch is stable (vault-view depends on .refetch refs from React Query). + // stopIndexing is stable (Zustand action created once). + // styledInfo is stable (useCallback with empty deps). + useEffect(() => { + if (!isIndexing) return; - if (refetchIntervalRef.current) { - clearInterval(refetchIntervalRef.current); - refetchIntervalRef.current = undefined; - } + const startTime = indexingVault?.startTime ?? Date.now(); - if (toastIdRef.current) { - toast.dismiss(toastIdRef.current); - toastIdRef.current = undefined; - } + // Fire an immediate refetch + refetch(); - styledInfo('Indexing delayed', 'Data is taking longer than expected. Please try refreshing manually using the refresh button.'); - return; - } + const intervalId = setInterval(() => { + if (Date.now() - startTime > INDEXING_TIMEOUT_MS) { + clearInterval(intervalId); + stopIndexing(); + styledInfo('Indexing delayed', 'Data is taking longer than expected. Please try refreshing manually using the refresh button.'); + return; + } - // Trigger refetch - refetch(); - }, 5000); - } + refetch(); + }, REFETCH_INTERVAL_MS); return () => { - if (refetchIntervalRef.current) { - clearInterval(refetchIntervalRef.current); - refetchIntervalRef.current = undefined; - } - // Dismiss toast when vault or chain changes - if (toastIdRef.current) { - toast.dismiss(toastIdRef.current); - toastIdRef.current = undefined; - } + clearInterval(intervalId); }; - }, [isIndexing, isDataLoaded, vaultAddress, chainId, refetch, styledSuccess, styledInfo]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isIndexing]); + + // Effect 3: Detect completion — fresh post-init data arrived while indexing + useEffect(() => { + if (isIndexing && hasPostInitData) { + stopIndexing(); + styledSuccess('Vault data loaded', 'Your vault is ready to use.'); + } + }, [isIndexing, hasPostInitData, stopIndexing, styledSuccess]); - // Cleanup on unmount + // Cleanup toast on unmount useEffect(() => { return () => { - if (refetchIntervalRef.current) { - clearInterval(refetchIntervalRef.current); - } if (toastIdRef.current) { toast.dismiss(toastIdRef.current); } - hasDetectedIndexing.current = false; }; }, []); diff --git a/src/stores/vault-indexing-store.ts b/src/stores/vault-indexing-store.ts new file mode 100644 index 00000000..a90d3060 --- /dev/null +++ b/src/stores/vault-indexing-store.ts @@ -0,0 +1,46 @@ +import { create } from 'zustand'; +import type { Address } from 'viem'; + +export const INDEXING_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes + +type IndexingVault = { + address: Address; + chainId: number; + startTime: number; +}; + +type VaultIndexingState = { + indexingVault: IndexingVault | null; +}; + +type VaultIndexingActions = { + startIndexing: (address: Address, chainId: number) => void; + stopIndexing: () => void; +}; + +type VaultIndexingStore = VaultIndexingState & VaultIndexingActions; + +/** + * Zustand store for vault indexing state after initialization. + * Replaces the old localStorage-based system with instant reactivity. + * + * After completeInitialization() succeeds, call startIndexing() to signal + * that the vault page should poll the API until post-initialization data arrives. + */ +export const useVaultIndexingStore = create((set) => ({ + indexingVault: null, + + startIndexing: (address, chainId) => { + set({ + indexingVault: { + address, + chainId, + startTime: Date.now(), + }, + }); + }, + + stopIndexing: () => { + set({ indexingVault: null }); + }, +})); diff --git a/src/utils/vault-indexing.ts b/src/utils/vault-indexing.ts deleted file mode 100644 index c1817f30..00000000 --- a/src/utils/vault-indexing.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Utility for managing vault indexing state in localStorage. - * Tracks when a vault is waiting for API indexing after initialization, - * with automatic cleanup after 2 minutes. - */ - -type IndexingVault = { - address: string; - chainId: number; - startTime: number; -}; - -const INDEXING_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes (reduced since API responds quickly) - -function getStorageKey(address: string, chainId: number): string { - return `vault-indexing-${chainId}-${address.toLowerCase()}`; -} - -/** - * Mark a vault as actively indexing - */ -export function startVaultIndexing(address: string, chainId: number): void { - try { - const key = getStorageKey(address, chainId); - const data: IndexingVault = { - address, - chainId, - startTime: Date.now(), - }; - localStorage.setItem(key, JSON.stringify(data)); - } catch (error) { - console.warn('Failed to start vault indexing:', error); - } -} - -/** - * Check if a vault is currently indexing. - * Returns null if not indexing or if timeout has been reached. - * Automatically cleans up expired entries. - */ -export function getIndexingVault(address: string, chainId: number): IndexingVault | null { - try { - const key = getStorageKey(address, chainId); - const stored = localStorage.getItem(key); - - if (!stored) return null; - - const data = JSON.parse(stored) as IndexingVault; - const elapsed = Date.now() - data.startTime; - - // Check if timeout reached - if (elapsed > INDEXING_TIMEOUT_MS) { - // Auto-cleanup expired entry - localStorage.removeItem(key); - return null; - } - - return data; - } catch (error) { - console.warn('Failed to get vault indexing state:', error); - return null; - } -} - -/** - * Stop indexing for a vault (called when data successfully loads) - */ -export function stopVaultIndexing(address: string, chainId: number): void { - try { - const key = getStorageKey(address, chainId); - localStorage.removeItem(key); - } catch (error) { - console.warn('Failed to stop vault indexing:', error); - } -} From fb021fcefb1b7c50519125620c0f8628292f7d97 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Mon, 26 Jan 2026 11:04:21 +0800 Subject: [PATCH 2/6] chore: wording --- src/components/shared/allocator-card.tsx | 39 +++++++++++-------- .../modals/vault-initialization-modal.tsx | 19 ++++----- src/features/autovault/vault-view.tsx | 2 +- src/hooks/useVaultIndexing.tsx | 4 +- src/stores/vault-indexing-store.ts | 2 +- 5 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/components/shared/allocator-card.tsx b/src/components/shared/allocator-card.tsx index 8cbc91f2..bce2ec71 100644 --- a/src/components/shared/allocator-card.tsx +++ b/src/components/shared/allocator-card.tsx @@ -1,12 +1,12 @@ +import Image from 'next/image'; import type { Address } from 'viem'; -import { AccountIdentity } from './account-identity'; -import { useConnection } from 'wagmi'; -import { SupportedNetworks } from '@/utils/networks'; +import { getSlicedAddress } from '@/utils/address'; type AllocatorCardProps = { name: string; address: Address; description: string; + image?: string; isSelected?: boolean; onSelect?: () => void; disabled?: boolean; @@ -16,29 +16,41 @@ export function AllocatorCard({ name, address, description, + image, isSelected = false, onSelect, disabled = false, }: AllocatorCardProps): JSX.Element { - const { chainId } = useConnection(); return ( ); diff --git a/src/features/autovault/components/vault-detail/modals/vault-initialization-modal.tsx b/src/features/autovault/components/vault-detail/modals/vault-initialization-modal.tsx index a233aa1d..902822d4 100644 --- a/src/features/autovault/components/vault-detail/modals/vault-initialization-modal.tsx +++ b/src/features/autovault/components/vault-detail/modals/vault-initialization-modal.tsx @@ -63,13 +63,7 @@ function DeployAdapterStep({
{isDeploying && } - - {adapterDetected - ? `Adapter detected: ${shortenAddress(adapterAddress)}` - : isDeploying - ? 'Deploying adapter...' - : 'Adapter not detected yet. Click deploy to create one.'} - + {adapterDetected ? `Adapter detected: ${shortenAddress(adapterAddress)}` : isDeploying ? 'Deploying adapter...' : ''}
@@ -165,7 +159,7 @@ function AgentSelectionStep({ }) { return (
-

Choose an agent to automate your vault's allocations. You can change this later in settings.

+

Choose an allocator to automate your vault's allocations. You can change this later in settings.

{v2AgentsBase.map((agent) => ( onSelectAgent(selectedAgent === (agent.address as Address) ? null : (agent.address as Address))} /> @@ -239,7 +234,7 @@ export function VaultInitializationModal() { }); const [stepIndex, setStepIndex] = useState(0); - const [selectedAgent, setSelectedAgent] = useState
((v2AgentsBase.at(0)?.address as Address) || null); + const [selectedAgent, setSelectedAgent] = useState
((v2AgentsBase.at(0)?.address as Address) ?? null); const [vaultName, setVaultName] = useState(''); const [vaultSymbol, setVaultSymbol] = useState(''); const [deployedAdapter, setDeployedAdapter] = useState
(ZERO_ADDRESS); @@ -364,7 +359,7 @@ export function VaultInitializationModal() { useEffect(() => { if (!isOpen) { setStepIndex(0); - setSelectedAgent(null); + setSelectedAgent((v2AgentsBase.at(0)?.address as Address) ?? null); setVaultName(''); setVaultSymbol(''); setDeployedAdapter(ZERO_ADDRESS); @@ -385,7 +380,7 @@ export function VaultInitializationModal() { case 'metadata': return 'Set vault name & symbol'; case 'agents': - return 'Choose an agent'; + return 'Choose an Allocator'; case 'finalize': return 'Review & finalize'; default: @@ -483,7 +478,7 @@ export function VaultInitializationModal() { mainIcon={} onClose={close} /> - + {currentStep === 'deploy' && (

Complete vault setup

- Initialize your vault by deploying an adapter, setting caps, and configuring the registry to start using your vault. + Initialize your vault by deploying an adapter, and setting caps to start auto earning.

-
-
- { - const val = e.target.value; - if (val === '' || /^\d*\.?\d*$/.test(val)) { - handleUpdateCollateralCap(info.collateralAddress, 'absoluteCap', val); - } - }} - placeholder="No limit" - disabled={!isOwner} - className="w-24 rounded bg-hovered px-2 py-1 text-right text-xs shadow-sm focus:outline-none focus:ring-1 focus:ring-primary" - /> - -
- - ); - })} - - - )} - {/* Market Caps Section */} {marketCaps.size > 0 && (
-

Market Caps ({marketCaps.size})

-

- Individual limits per market. Effective allocation is capped by the smaller of this and the collateral cap above. -

+

Market Caps ({marketCaps.size - removedMarketIds.size})

+

Individual limits per market for how agents can allocate vault funds.

{/* Column Headers */} @@ -538,17 +454,26 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin
Market
Relative %
Absolute ({vaultAssetToken?.symbol ?? 'units'})
+ {isOwner &&
}
({ - market: info.market, - relativeCap: info.relativeCap, - absoluteCap: info.absoluteCap, - isEditable: true, - isNew: !info.existingCapId, - onUpdateCap: (field, value) => handleUpdateMarketCap(info.market.uniqueKey, field, value), - }))} + markets={Array.from(marketCaps.values()).map((info) => { + const collateralAddr = info.market.collateralAsset.address.toLowerCase(); + const collateralInfo = collateralCapMap.get(collateralAddr); + return { + market: info.market, + relativeCap: info.relativeCap, + absoluteCap: info.absoluteCap, + isEditable: true, + isNew: !info.existingCapId, + isRemoved: removedMarketIds.has(info.market.uniqueKey.toLowerCase()), + collateralCapPercent: collateralInfo?.relativeCap, + onUpdateCap: (field, value) => handleUpdateMarketCap(info.market.uniqueKey, field, value), + onRemove: () => handleRemoveMarketCap(info.market.uniqueKey), + onUndoRemove: () => handleUndoRemoveMarketCap(info.market.uniqueKey), + }; + })} showHeaders={false} vaultAssetSymbol={vaultAssetToken?.symbol} vaultAssetAddress={vaultAsset} @@ -588,15 +513,17 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin disabled={!hasChanges || isUpdating} onClick={() => void handleSave()} > - {isUpdating ? ( - - Saving... - - ) : needSwitchChain ? ( - 'Switch Network' - ) : ( - 'Save Changes' - )} + {(() => { + if (isUpdating) { + return ( + + Saving... + + ); + } + if (needSwitchChain) return 'Switch Network'; + return 'Save Changes'; + })()}
diff --git a/src/features/autovault/components/vault-detail/settings/MarketCapsTable.tsx b/src/features/autovault/components/vault-detail/settings/MarketCapsTable.tsx index c2cef715..f23a8257 100644 --- a/src/features/autovault/components/vault-detail/settings/MarketCapsTable.tsx +++ b/src/features/autovault/components/vault-detail/settings/MarketCapsTable.tsx @@ -1,5 +1,5 @@ -import { maxUint128 } from 'viem'; -import type { Address } from 'viem'; +import { Cross2Icon, InfoCircledIcon } from '@radix-ui/react-icons'; +import { type Address, maxUint128 } from 'viem'; import { Badge } from '@/components/ui/badge'; import { MarketIdentity, MarketIdentityFocus } from '@/features/markets/components/market-identity'; import { findToken } from '@/utils/tokens'; @@ -11,7 +11,11 @@ type MarketCapRow = { absoluteCap: string; isEditable?: boolean; isNew?: boolean; + isRemoved?: boolean; + collateralCapPercent?: number; onUpdateCap?: (field: 'relativeCap' | 'absoluteCap', value: string) => void; + onRemove?: () => void; + onUndoRemove?: () => void; }; type MarketCapsTableProps = { @@ -31,23 +35,17 @@ export function MarketCapsTable({ chainId, isOwner = true, }: MarketCapsTableProps) { - // Get decimals for proper formatting const vaultAssetDecimals = vaultAssetAddress && chainId ? (findToken(vaultAssetAddress, chainId)?.decimals ?? 18) : 18; const formatAbsoluteCap = (cap: string): string => { - if (!cap || cap === '') { - return 'No limit'; - } + if (!cap) return 'No limit'; try { const capBigInt = BigInt(cap); - if (capBigInt >= maxUint128) { - return 'No limit'; - } + if (capBigInt >= maxUint128) return 'No limit'; const value = Number(capBigInt) / 10 ** vaultAssetDecimals; return value.toLocaleString(undefined, { maximumFractionDigits: 2 }); - } catch (_e) { - // If we can't parse it as BigInt, return as is + } catch { return cap; } }; @@ -66,84 +64,134 @@ export function MarketCapsTable({ )}
- {markets.map((row) => ( -
-
- - {row.isNew && New} -
- {row.isEditable && row.onUpdateCap ? ( - <> -
- { - const val = e.target.value; - if (val === '' || /^\d*\.?\d*$/.test(val)) { - const num = Number.parseFloat(val); - if (val === '' || (num >= 0 && num <= 100)) { - row.onUpdateCap!('relativeCap', val); - } - } - }} - placeholder="100" - disabled={!isOwner} - className="w-16 rounded bg-hovered px-2 py-1 text-right text-xs shadow-sm focus:outline-none focus:ring-1 focus:ring-primary disabled:opacity-50" + {markets.map((row) => { + const marketCapNum = Number.parseFloat(row.relativeCap) || 0; + const isCollateralConstraining = + row.collateralCapPercent !== undefined && row.collateralCapPercent < marketCapNum && !row.isRemoved; + + return ( +
+
+
+ - % - + {row.isNew && !row.isRemoved && New}
-
- { - const val = e.target.value; - if (val === '' || /^\d*\.?\d*$/.test(val)) { - row.onUpdateCap!('absoluteCap', val); - } - }} - placeholder="No limit" - disabled={!isOwner} - className="w-24 rounded bg-hovered px-2 py-1 text-right text-xs shadow-sm focus:outline-none focus:ring-1 focus:ring-primary disabled:opacity-50" - /> - + {(() => { + if (row.isRemoved) { + return ( +
+ Removed + {row.onUndoRemove && ( + + )} +
+ ); + } + + if (row.isEditable && row.onUpdateCap) { + return ( + <> +
+ { + const val = e.target.value; + if (val === '' || /^\d*\.?\d*$/.test(val)) { + const num = Number.parseFloat(val); + if (val === '' || (num >= 0 && num <= 100)) { + row.onUpdateCap!('relativeCap', val); + } + } + }} + placeholder="100" + disabled={!isOwner} + className="w-16 rounded bg-hovered px-2 py-1 text-right text-xs shadow-sm focus:outline-none focus:ring-1 focus:ring-primary disabled:opacity-50" + /> + % + +
+
+ { + const val = e.target.value; + if (val === '' || /^\d*\.?\d*$/.test(val)) { + row.onUpdateCap!('absoluteCap', val); + } + }} + placeholder="No limit" + disabled={!isOwner} + className="w-24 rounded bg-hovered px-2 py-1 text-right text-xs shadow-sm focus:outline-none focus:ring-1 focus:ring-primary disabled:opacity-50" + /> + +
+ {row.onRemove && isOwner && ( + + )} + + ); + } + + return ( + <> +
{row.relativeCap}%
+
{formatAbsoluteCap(row.absoluteCap)}
+ + ); + })()} +
+ {isCollateralConstraining && ( +
+ + + Effective cap limited to {row.collateralCapPercent?.toFixed(2)}% by existing + collateral cap +
- - ) : ( - <> -
{row.relativeCap}%
-
{formatAbsoluteCap(row.absoluteCap)}
- - )} -
- ))} + )} +
+ ); + })}
); diff --git a/src/modals/vault/vault-deposit-modal.tsx b/src/modals/vault/vault-deposit-modal.tsx index cdae4b3d..c37d7a51 100644 --- a/src/modals/vault/vault-deposit-modal.tsx +++ b/src/modals/vault/vault-deposit-modal.tsx @@ -119,7 +119,7 @@ export function VaultDepositModal({ variant="primary" className="ml-2 min-w-32" > - {!permit2Authorized || (!usePermit2Setting && !isApproved) ? 'Approve' : 'Deposit'} + Deposit
From 54e4cdc810145b5406f77f9dc2105ddfed94ff6c Mon Sep 17 00:00:00 2001 From: antoncoding Date: Mon, 26 Jan 2026 11:57:12 +0800 Subject: [PATCH 4/6] refactor: simplify flow --- .../modals/vault-initialization-modal.tsx | 4 +++- .../vault-detail/settings/EditCaps.tsx | 18 ++++++------------ 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/features/autovault/components/vault-detail/modals/vault-initialization-modal.tsx b/src/features/autovault/components/vault-detail/modals/vault-initialization-modal.tsx index 902822d4..b94b7356 100644 --- a/src/features/autovault/components/vault-detail/modals/vault-initialization-modal.tsx +++ b/src/features/autovault/components/vault-detail/modals/vault-initialization-modal.tsx @@ -159,7 +159,9 @@ function AgentSelectionStep({ }) { return (
-

Choose an allocator to automate your vault's allocations. You can change this later in settings.

+

+ Choose an allocator to automate your vault's allocations. You can change this later in settings. +

{v2AgentsBase.map((agent) => ( { hasUserEditsRef.current = true; const key = marketId.toLowerCase(); - let wasNewCap = false; setMarketCaps((prev) => { const info = prev.get(key); - if (info && !info.existingCapId) { + if (!info) return prev; + + if (!info.existingCapId) { // Newly added cap — remove from state entirely - wasNewCap = true; const next = new Map(prev); next.delete(key); return next; } + + // Existing on-chain cap — mark as removed (will be zeroed on save) + setRemovedMarketIds((prevRemoved) => new Set(prevRemoved).add(key)); return prev; }); - - // Existing on-chain cap — mark as removed (will be zeroed on save) - if (!wasNewCap) { - setRemovedMarketIds((prev) => { - const next = new Set(prev); - next.add(key); - return next; - }); - } }, []); const handleUndoRemoveMarketCap = useCallback((marketId: string) => { From 8b71040b390865d21d6e37c11a8ef7317d518941 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Mon, 26 Jan 2026 12:01:16 +0800 Subject: [PATCH 5/6] chore: interval and docs --- src/hooks/useVaultIndexing.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/hooks/useVaultIndexing.tsx b/src/hooks/useVaultIndexing.tsx index 209920be..b9c4ee62 100644 --- a/src/hooks/useVaultIndexing.tsx +++ b/src/hooks/useVaultIndexing.tsx @@ -12,12 +12,12 @@ type UseVaultIndexingArgs = { refetch: () => void; }; -const REFETCH_INTERVAL_MS = 5_000; +const REFETCH_INTERVAL_MS = 10_000; /** * Hook to manage vault indexing state after initialization. - * Shows a persistent toast and retries fetching data every 5 seconds - * until post-initialization data arrives or timeout is reached (2 minutes). + * Shows a persistent toast and retries fetching data every 10 seconds + * until post-initialization data arrives or timeout is reached (10 minutes). * * Uses Zustand store for instant reactivity (no localStorage polling). */ From 2a95ae110bb7b165ba55fd9c3ed5fc1886791efe Mon Sep 17 00:00:00 2001 From: antoncoding Date: Mon, 26 Jan 2026 12:10:10 +0800 Subject: [PATCH 6/6] feat: hide removed caps --- .../components/vault-detail/settings/CurrentCaps.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/autovault/components/vault-detail/settings/CurrentCaps.tsx b/src/features/autovault/components/vault-detail/settings/CurrentCaps.tsx index 8f9a45f4..b6a9bd88 100644 --- a/src/features/autovault/components/vault-detail/settings/CurrentCaps.tsx +++ b/src/features/autovault/components/vault-detail/settings/CurrentCaps.tsx @@ -31,7 +31,7 @@ export function CurrentCaps({ existingCaps, isOwner, onStartEdit, chainId, vault try { const capBigInt = BigInt(cap); - if (capBigInt >= maxUint128) { + if (capBigInt === 0n || capBigInt >= maxUint128) { return 'No limit'; } const value = Number(capBigInt) / 10 ** vaultAssetDecimals; @@ -109,7 +109,7 @@ export function CurrentCaps({ existingCaps, isOwner, onStartEdit, chainId, vault sharedMarketCount, }; }) - .filter((item) => item.market !== undefined) ?? [] + .filter((item) => item.market !== undefined && item.effectiveCap > 0) ?? [] ); }, [existingCaps, markets, collateralCapMap, marketCountByCollateral]);