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 84d8c7a6..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 @@ -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; @@ -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,9 @@ 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))} /> @@ -194,6 +191,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<{ @@ -238,7 +236,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); @@ -331,7 +329,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 +346,14 @@ export function VaultInitializationModal() { vaultContract, refetchAdapter, close, + startIndexing, registryAddress, selectedAgent, adapterAddress, vaultName, vaultSymbol, vaultAddress, + vaultAddressValue, chainId, ]); @@ -361,7 +361,7 @@ export function VaultInitializationModal() { useEffect(() => { if (!isOpen) { setStepIndex(0); - setSelectedAgent(null); + setSelectedAgent((v2AgentsBase.at(0)?.address as Address) ?? null); setVaultName(''); setVaultSymbol(''); setDeployedAdapter(ZERO_ADDRESS); @@ -382,7 +382,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: @@ -480,7 +480,7 @@ export function VaultInitializationModal() { mainIcon={} onClose={close} /> - + {currentStep === 'deploy' && ( = 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]); diff --git a/src/features/autovault/components/vault-detail/settings/EditCaps.tsx b/src/features/autovault/components/vault-detail/settings/EditCaps.tsx index 0611ac51..ab4676ce 100644 --- a/src/features/autovault/components/vault-detail/settings/EditCaps.tsx +++ b/src/features/autovault/components/vault-detail/settings/EditCaps.tsx @@ -2,11 +2,9 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { PlusIcon } from '@radix-ui/react-icons'; import { toast } from 'react-toastify'; import { type Address, parseUnits, maxUint128 } from 'viem'; -import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Spinner } from '@/components/ui/spinner'; import { useTokensQuery } from '@/hooks/queries/useTokensQuery'; -import { TokenIcon } from '@/components/shared/token-icon'; import type { VaultV2Cap } from '@/data-sources/morpho-api/v2-vaults'; import { useMarketNetwork } from '@/hooks/useMarketNetwork'; import { useProcessedMarkets } from '@/hooks/useProcessedMarkets'; @@ -28,14 +26,6 @@ type EditCapsProps = { onSave: (caps: VaultV2Cap[]) => Promise; }; -type CollateralCapInfo = { - collateralAddress: Address; - collateralSymbol: string; - relativeCap: string; - absoluteCap: string; - existingCapId?: string; -}; - type MarketCapInfo = { market: Market; relativeCap: string; @@ -45,7 +35,7 @@ type MarketCapInfo = { export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdating, adapterAddress, onCancel, onSave }: EditCapsProps) { const [marketCaps, setMarketCaps] = useState>(new Map()); - const [collateralCaps, setCollateralCaps] = useState>(new Map()); + const [removedMarketIds, setRemovedMarketIds] = useState>(new Set()); const [showAddMarketModal, setShowAddMarketModal] = useState(false); // Track if user has made edits to prevent state reset from background refetches @@ -65,6 +55,22 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin const vaultAssetDecimals = vaultAssetToken?.decimals ?? 18; + // Build read-only collateral cap lookup from existing on-chain data + // Used to compute effective caps and show constraint indicators + const collateralCapMap = useMemo(() => { + const capMap = new Map(); + for (const cap of existingCaps?.collateralCaps ?? []) { + const parsed = parseCapIdParams(cap.idParams); + if (parsed.collateralToken) { + capMap.set(parsed.collateralToken.toLowerCase(), { + relativeCap: Number(cap.relativeCap) / 1e16, + absoluteCap: cap.absoluteCap, + }); + } + } + return capMap; + }, [existingCaps]); + // Filter available markets for adding const availableMarkets = useMemo(() => { if (!markets || !vaultAsset) return []; @@ -77,36 +83,8 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin if (hasUserEditsRef.current) return; if (availableMarkets.length === 0) return; - // Initialize collateral caps - const collateralCapsMap = new Map(); - existingCaps?.collateralCaps.forEach((cap) => { - const parsed = parseCapIdParams(cap.idParams); - if (parsed.collateralToken) { - const token = findToken(parsed.collateralToken, chainId); - - const relativeCapBigInt = BigInt(cap.relativeCap); - const relativeCap = (Number(relativeCapBigInt) / 1e16).toString(); - - const absoluteCapBigInt = BigInt(cap.absoluteCap); - const absoluteCap = - absoluteCapBigInt === 0n || absoluteCapBigInt >= maxUint128 - ? '' - : (Number(absoluteCapBigInt) / 10 ** vaultAssetDecimals).toString(); - - collateralCapsMap.set(parsed.collateralToken.toLowerCase(), { - collateralAddress: parsed.collateralToken, - collateralSymbol: token?.symbol ?? 'Unknown', - relativeCap, - absoluteCap, - existingCapId: cap.capId, - }); - } - }); - setCollateralCaps(collateralCapsMap); - - // Initialize market caps const marketCapsMap = new Map(); - existingCaps?.marketCaps.forEach((cap) => { + for (const cap of existingCaps?.marketCaps ?? []) { const parsed = parseCapIdParams(cap.idParams); const market = availableMarkets.find((m) => m.uniqueKey.toLowerCase() === parsed.marketId?.toLowerCase()); if (market) { @@ -126,37 +104,21 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin existingCapId: cap.capId, }); } - }); + } setMarketCaps(marketCapsMap); - }, [availableMarkets, chainId, existingCaps, findToken, vaultAssetDecimals]); + }, [availableMarkets, existingCaps, vaultAssetDecimals]); const handleAddMarkets = useCallback((newMarkets: Market[]) => { hasUserEditsRef.current = true; setMarketCaps((prev) => { const next = new Map(prev); - newMarkets.forEach((market) => { + for (const market of newMarkets) { next.set(market.uniqueKey.toLowerCase(), { market, relativeCap: '100', absoluteCap: '', }); - - // Auto-create collateral cap if needed - const collateralAddr = market.collateralAsset.address.toLowerCase(); - setCollateralCaps((prevCaps) => { - if (!prevCaps.has(collateralAddr)) { - const newCaps = new Map(prevCaps); - newCaps.set(collateralAddr, { - collateralAddress: market.collateralAsset.address as Address, - collateralSymbol: market.collateralAsset.symbol, - relativeCap: '100', - absoluteCap: '', - }); - return newCaps; - } - return prevCaps; - }); - }); + } return next; }); }, []); @@ -173,17 +135,31 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin }); }, []); - const handleUpdateCollateralCap = useCallback((collateralAddr: string, field: 'relativeCap' | 'absoluteCap', value: string) => { + const handleRemoveMarketCap = useCallback((marketId: string) => { hasUserEditsRef.current = true; - setCollateralCaps((prev) => { - const next = new Map(prev); - const existing = next.get(collateralAddr.toLowerCase()); - if (existing) { - next.set(collateralAddr.toLowerCase(), { - ...existing, - [field]: value, - }); + const key = marketId.toLowerCase(); + + setMarketCaps((prev) => { + const info = prev.get(key); + if (!info) return prev; + + if (!info.existingCapId) { + // Newly added cap — remove from state entirely + 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; + }); + }, []); + + const handleUndoRemoveMarketCap = useCallback((marketId: string) => { + setRemovedMarketIds((prev) => { + const next = new Set(prev); + next.delete(marketId.toLowerCase()); return next; }); }, []); @@ -194,11 +170,9 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin }, [onCancel]); const hasChanges = useMemo(() => { - // Check for new caps const hasNewMarkets = Array.from(marketCaps.values()).some((m) => !m.existingCapId); - const hasNewCollaterals = Array.from(collateralCaps.values()).some((c) => !c.existingCapId); + const hasRemovedMarkets = removedMarketIds.size > 0; - // Check for modified caps const hasModifiedMarkets = Array.from(marketCaps.values()).some((info) => { if (!info.existingCapId) return false; const existing = existingCaps?.marketCaps.find((cap) => { @@ -215,24 +189,8 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin return info.relativeCap !== existingRelative || info.absoluteCap !== existingAbsolute; }); - const hasModifiedCollaterals = Array.from(collateralCaps.values()).some((info) => { - if (!info.existingCapId) return false; - const existing = existingCaps?.collateralCaps.find((cap) => { - const parsed = parseCapIdParams(cap.idParams); - return parsed.collateralToken?.toLowerCase() === info.collateralAddress.toLowerCase(); - }); - if (!existing) return false; - const existingRelative = (Number(BigInt(existing.relativeCap)) / 1e16).toString(); - const existingAbsoluteBigInt = BigInt(existing.absoluteCap); - const existingAbsolute = - existingAbsoluteBigInt === 0n || existingAbsoluteBigInt >= maxUint128 - ? '' - : (Number(existingAbsoluteBigInt) / 10 ** vaultAssetDecimals).toString(); - return info.relativeCap !== existingRelative || info.absoluteCap !== existingAbsolute; - }); - - return hasNewMarkets || hasNewCollaterals || hasModifiedMarkets || hasModifiedCollaterals; - }, [marketCaps, collateralCaps, existingCaps, vaultAssetDecimals]); + return hasNewMarkets || hasModifiedMarkets || hasRemovedMarkets; + }, [marketCaps, removedMarketIds, existingCaps, vaultAssetDecimals]); const handleSave = useCallback(async () => { if (needSwitchChain) { @@ -248,70 +206,102 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin const capsToUpdate: VaultV2Cap[] = []; // Add or fix adapter cap to ensure it's always 100% relative + maxUint128 absolute - if (adapterAddress) { - const targetRelativeCap = parseUnits('100', 16); - const targetAbsoluteCap = maxUint128; - - const currentRelativeCap = existingCaps?.adapterCap ? BigInt(existingCaps.adapterCap.relativeCap) : 0n; - const currentAbsoluteCap = existingCaps?.adapterCap ? BigInt(existingCaps.adapterCap.absoluteCap) : 0n; + const targetRelativeCap = parseUnits('100', 16); + const targetAbsoluteCap = maxUint128; + + const currentRelativeCap = existingCaps?.adapterCap ? BigInt(existingCaps.adapterCap.relativeCap) : 0n; + const currentAbsoluteCap = existingCaps?.adapterCap ? BigInt(existingCaps.adapterCap.absoluteCap) : 0n; + + // Only update if not already at target values + if (currentRelativeCap !== targetRelativeCap || currentAbsoluteCap !== targetAbsoluteCap) { + const { params, id } = getAdapterCapId(adapterAddress); + capsToUpdate.push({ + capId: id, + idParams: params, + relativeCap: targetRelativeCap.toString(), + absoluteCap: targetAbsoluteCap.toString(), + oldRelativeCap: currentRelativeCap.toString(), + oldAbsoluteCap: currentAbsoluteCap.toString(), + }); + } - // Only update if not already at target values - if (currentRelativeCap !== targetRelativeCap || currentAbsoluteCap !== targetAbsoluteCap) { - const { params, id } = getAdapterCapId(adapterAddress); - capsToUpdate.push({ - capId: id, - idParams: params, - relativeCap: targetRelativeCap.toString(), - absoluteCap: targetAbsoluteCap.toString(), - oldRelativeCap: currentRelativeCap.toString(), - oldAbsoluteCap: currentAbsoluteCap.toString(), - }); + // Auto-derive collateral caps from active market caps + // Collect active collateral addresses from non-removed market caps + const activeCollaterals = new Set(); + for (const [key, info] of marketCaps.entries()) { + if (!removedMarketIds.has(key)) { + activeCollaterals.add(info.market.collateralAsset.address.toLowerCase()); } } - // Add collateral caps with delta calculation (only when changed) - for (const [, info] of collateralCaps.entries()) { - const newRelativeCapBigInt = - info.relativeCap && info.relativeCap !== '' && Number.parseFloat(info.relativeCap) > 0 ? parseUnits(info.relativeCap, 16) : 0n; - - const newAbsoluteCapBigInt = - info.absoluteCap && info.absoluteCap !== '' && Number.parseFloat(info.absoluteCap) > 0 - ? parseUnits(info.absoluteCap, vaultAssetDecimals) - : maxUint128; - - // Find existing cap to calculate delta - const existingCap = existingCaps?.collateralCaps.find((cap) => { + // Ensure each active collateral has max caps + for (const collateralAddr of activeCollaterals) { + const existing = existingCaps?.collateralCaps.find((cap) => { const parsed = parseCapIdParams(cap.idParams); - return parsed.collateralToken?.toLowerCase() === info.collateralAddress.toLowerCase(); + return parsed.collateralToken?.toLowerCase() === collateralAddr; }); - const oldRelativeCap = existingCap ? BigInt(existingCap.relativeCap) : 0n; - const oldAbsoluteCap = existingCap ? BigInt(existingCap.absoluteCap) : 0n; - - // Only include if changed - if (oldRelativeCap !== newRelativeCapBigInt || oldAbsoluteCap !== newAbsoluteCapBigInt) { - const { params, id } = getCollateralCapId(info.collateralAddress); + const oldRelativeCap = existing ? BigInt(existing.relativeCap) : 0n; + const oldAbsoluteCap = existing ? BigInt(existing.absoluteCap) : 0n; + if (oldRelativeCap !== targetRelativeCap || oldAbsoluteCap !== targetAbsoluteCap) { + const { params, id } = getCollateralCapId(collateralAddr as Address); capsToUpdate.push({ capId: id, idParams: params, - relativeCap: newRelativeCapBigInt.toString(), - absoluteCap: newAbsoluteCapBigInt.toString(), + relativeCap: targetRelativeCap.toString(), + absoluteCap: targetAbsoluteCap.toString(), oldRelativeCap: oldRelativeCap.toString(), oldAbsoluteCap: oldAbsoluteCap.toString(), }); } } - // Add market caps with delta calculation (only when changed) - for (const [, info] of marketCaps.entries()) { - const newRelativeCapBigInt = - info.relativeCap && info.relativeCap !== '' && Number.parseFloat(info.relativeCap) > 0 ? parseUnits(info.relativeCap, 16) : 0n; + // Zero out collateral caps for collaterals with no active markets + for (const cap of existingCaps?.collateralCaps ?? []) { + const parsed = parseCapIdParams(cap.idParams); + const addr = parsed.collateralToken?.toLowerCase(); + if (addr && !activeCollaterals.has(addr)) { + const oldRelativeCap = BigInt(cap.relativeCap); + const oldAbsoluteCap = BigInt(cap.absoluteCap); + + if (oldRelativeCap !== 0n || oldAbsoluteCap !== 0n) { + const { params, id } = getCollateralCapId(addr as Address); + capsToUpdate.push({ + capId: id, + idParams: params, + relativeCap: '0', + absoluteCap: '0', + oldRelativeCap: oldRelativeCap.toString(), + oldAbsoluteCap: oldAbsoluteCap.toString(), + }); + } + } + } + + // Add market caps with delta calculation (handles both active and removed) + for (const [key, info] of marketCaps.entries()) { + const isRemoved = removedMarketIds.has(key); + + // Skip newly-added-then-removed caps (nothing to zero out on-chain) + if (isRemoved && !info.existingCapId) continue; - const newAbsoluteCapBigInt = - info.absoluteCap && info.absoluteCap !== '' && Number.parseFloat(info.absoluteCap) > 0 - ? parseUnits(info.absoluteCap, vaultAssetDecimals) - : maxUint128; + let newRelativeCapBigInt = 0n; + let newAbsoluteCapBigInt = 0n; + + if (!isRemoved) { + const relativeVal = Number.parseFloat(info.relativeCap); + if (info.relativeCap !== '' && relativeVal > 0) { + newRelativeCapBigInt = parseUnits(info.relativeCap, 16); + } + + const absoluteVal = Number.parseFloat(info.absoluteCap); + if (info.absoluteCap !== '' && absoluteVal > 0) { + newAbsoluteCapBigInt = parseUnits(info.absoluteCap, vaultAssetDecimals); + } else { + newAbsoluteCapBigInt = maxUint128; + } + } // Find existing cap to calculate delta const existingCap = existingCaps?.marketCaps.find((cap) => { @@ -353,9 +343,8 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin try { const success = await onSave(capsToUpdate); if (success) { - // Reset edit tracking on successful save hasUserEditsRef.current = false; - // Parent handles switching back to read mode + setRemovedMarketIds(new Set()); } else { toast.error('Failed to save changes'); } @@ -363,7 +352,17 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin console.error('Save error:', error); toast.error('Failed to save changes'); } - }, [marketCaps, collateralCaps, needSwitchChain, switchToNetwork, onSave, adapterAddress, vaultAsset, vaultAssetDecimals, existingCaps]); + }, [ + marketCaps, + removedMarketIds, + needSwitchChain, + switchToNetwork, + onSave, + adapterAddress, + vaultAsset, + vaultAssetDecimals, + existingCaps, + ]); if (marketsLoading) { return ( @@ -373,7 +372,7 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin ); } - const existingMarketIds = new Set(Array.from(marketCaps.keys())); + const existingMarketIds = new Set(Array.from(marketCaps.keys()).filter((key) => !removedMarketIds.has(key))); return ( <> @@ -387,8 +386,12 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin {/* Adapter Cap Warning */} {(() => { - // Check if adapter cap needs attention const hasAdapterCap = !!existingCaps?.adapterCap; + const hasOtherCaps = (existingCaps?.collateralCaps?.length ?? 0) > 0 || (existingCaps?.marketCaps?.length ?? 0) > 0; + + // No warning when user has no caps at all (fresh state) + if (!hasOtherCaps && !hasAdapterCap) return null; + if (!hasAdapterCap) { return (
@@ -432,105 +435,12 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin return null; })()} - {/* Collateral Caps Section */} - {collateralCaps.size > 0 && ( -
-
-

Collateral Caps ({collateralCaps.size})

-

Shared limit across all markets using the same collateral.

-
- - {/* Column Headers */} -
-
Collateral
-
Relative %
-
Absolute ({vaultAssetToken?.symbol ?? 'units'})
-
- -
- {Array.from(collateralCaps.values()).map((info) => { - const isNew = !info.existingCapId; - - return ( -
- -
- {info.collateralSymbol} - {isNew && New} -
-
- { - const val = e.target.value; - if (val === '' || /^\d*\.?\d*$/.test(val)) { - const num = Number.parseFloat(val); - if (val === '' || (num >= 0 && num <= 100)) { - handleUpdateCollateralCap(info.collateralAddress, '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" - /> - % - -
-
- { - 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 +448,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 +507,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/features/autovault/vault-view.tsx b/src/features/autovault/vault-view.tsx index 10b7c5b8..86787130 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, }); @@ -233,7 +240,7 @@ export default function VaultContent() {

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.

diff --git a/src/stores/vault-indexing-store.ts b/src/stores/vault-indexing-store.ts new file mode 100644 index 00000000..65162689 --- /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 = 10 * 60 * 1000; // 10 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); - } -}