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 54840322..84d8c7a6 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 @@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { FiZap } from 'react-icons/fi'; import { type Address, zeroAddress, decodeEventLog } from 'viem'; +import { useParams } from 'next/navigation'; import { usePublicClient } from 'wagmi'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -11,10 +12,14 @@ import { Modal, ModalHeader, ModalBody, ModalFooter } from '@/components/common/ import { Spinner } from '@/components/ui/spinner'; import { adapterFactoryAbi } from '@/abis/morpho-market-v1-adapter-factory'; import { useDeployMorphoMarketV1Adapter } from '@/hooks/useDeployMorphoMarketV1Adapter'; +import { useVaultV2Data } from '@/hooks/useVaultV2Data'; +import { useVaultV2 } from '@/hooks/useVaultV2'; +import { useMorphoMarketV1Adapters } from '@/hooks/useMorphoMarketV1Adapters'; import { v2AgentsBase } from '@/utils/monarch-agent'; import { getMorphoAddress } from '@/utils/morpho'; -import { type SupportedNetworks, getNetworkConfig } from '@/utils/networks'; +import { ALL_SUPPORTED_NETWORKS, SupportedNetworks, getNetworkConfig } from '@/utils/networks'; import { startVaultIndexing } from '@/utils/vault-indexing'; +import { useVaultInitializationModalStore } from '@/stores/vault-initialization-modal-store'; const ZERO_ADDRESS = zeroAddress; const shortenAddress = (value: Address | string) => (value === ZERO_ADDRESS ? '0x0000…0000' : `${value.slice(0, 6)}…${value.slice(-4)}`); @@ -180,35 +185,58 @@ function AgentSelectionStep({ const MAX_NAME_LENGTH = 64; const MAX_SYMBOL_LENGTH = 16; -export function VaultInitializationModal({ - isOpen, - onOpenChange, - vaultAddress, - marketAdapter, - marketAdapterLoading, - refetchMarketAdapter, - chainId, - onAdapterConfigured, - completeInitialization, - isInitializing, -}: { - isOpen: boolean; - marketAdapter: Address; - marketAdapterLoading: boolean; - refetchMarketAdapter: () => void; - onOpenChange: (open: boolean) => void; - vaultAddress: Address; - chainId: SupportedNetworks; - onAdapterConfigured: () => void; - completeInitialization: ( - morphoRegistry: Address, - marketV1Adapter: Address, - allocator?: Address, - name?: string, - symbol?: string, - ) => Promise; - isInitializing: boolean; -}) { +/** + * VaultInitializationModal - Completely self-contained modal component. + * Reads all data directly from Zustand stores and hooks - no props needed! + * + * Open this modal using: useVaultInitializationModalStore().open() + */ +export function VaultInitializationModal() { + // Modal state from Zustand (UI state) + const { isOpen, close } = useVaultInitializationModalStore(); + + // Get vault address and chain ID from URL params + const { chainId: chainIdParam, vaultAddress } = useParams<{ + chainId: string; + vaultAddress: string; + }>(); + + const vaultAddressValue = vaultAddress as Address; + + const chainId = useMemo(() => { + const parsed = Number(chainIdParam); + if (Number.isFinite(parsed) && ALL_SUPPORTED_NETWORKS.includes(parsed as SupportedNetworks)) { + return parsed as SupportedNetworks; + } + return SupportedNetworks.Base; + }, [chainIdParam]); + + // Fetch vault data + const vaultDataQuery = useVaultV2Data({ + vaultAddress: vaultAddressValue, + chainId, + }); + + // Transaction success handler + const handleTransactionSuccess = useCallback(() => { + void vaultDataQuery.refetch(); + }, [vaultDataQuery]); + + // Fetch vault contract state and actions + const vaultContract = useVaultV2({ + vaultAddress: vaultAddressValue, + chainId, + onTransactionSuccess: handleTransactionSuccess, + }); + + const { completeInitialization, isInitializing } = vaultContract; + + // Fetch adapter + const { morphoMarketV1Adapter: marketAdapter, refetch: refetchAdapter } = useMorphoMarketV1Adapters({ + vaultAddress: vaultAddressValue, + chainId, + }); + const [stepIndex, setStepIndex] = useState(0); const [selectedAgent, setSelectedAgent] = useState
((v2AgentsBase.at(0)?.address as Address) || null); const [vaultName, setVaultName] = useState(''); @@ -218,18 +246,19 @@ export function VaultInitializationModal({ const publicClient = usePublicClient({ chainId }); - const morphoAddress = useMemo(() => getMorphoAddress(chainId), [chainId]); + const morphoAddress = useMemo(() => (chainId ? getMorphoAddress(chainId) : ZERO_ADDRESS), [chainId]); const registryAddress = useMemo(() => { + if (!chainId) return ZERO_ADDRESS; const configured = getNetworkConfig(chainId).vaultConfig?.morphoRegistry; return (configured as Address | undefined) ?? ZERO_ADDRESS; }, [chainId]); // Adapter is detected if it exists in the subgraph OR we just deployed it - const adapterAddress = deployedAdapter !== ZERO_ADDRESS ? deployedAdapter : marketAdapter; + const adapterAddress = deployedAdapter !== ZERO_ADDRESS ? deployedAdapter : (marketAdapter ?? ZERO_ADDRESS); const adapterDetected = adapterAddress !== ZERO_ADDRESS; const { deploy, isDeploying, canDeploy } = useDeployMorphoMarketV1Adapter({ - vaultAddress, + vaultAddress: vaultAddressValue, chainId, morphoAddress, }); @@ -274,7 +303,7 @@ export function VaultInitializationModal({ setDeployedAdapter(adapter); // Trigger refetch for subgraph sync - void refetchMarketAdapter(); + void refetchAdapter(); // Auto-advance to next step setStepIndex(1); @@ -282,10 +311,10 @@ export function VaultInitializationModal({ } catch (_error) { // Error is handled by useDeployMorphoMarketV1Adapter hook } - }, [deploy, publicClient, refetchMarketAdapter]); + }, [deploy, publicClient, refetchAdapter]); const handleCompleteInitialization = useCallback(async () => { - if (adapterAddress === ZERO_ADDRESS || registryAddress === ZERO_ADDRESS) return; + if (adapterAddress === ZERO_ADDRESS || registryAddress === ZERO_ADDRESS || !vaultAddress || !chainId) return; try { // Note: Adapter cap will be set when user configures market caps @@ -305,16 +334,20 @@ export function VaultInitializationModal({ startVaultIndexing(vaultAddress, chainId); // Trigger initial refetch - void onAdapterConfigured(); + void vaultDataQuery.refetch(); + void vaultContract.refetch(); + void refetchAdapter(); - onOpenChange(false); + close(); } catch (_error) { // Error is handled by useVaultV2 hook (toast shown to user) } }, [ completeInitialization, - onAdapterConfigured, - onOpenChange, + vaultDataQuery, + vaultContract, + refetchAdapter, + close, registryAddress, selectedAgent, adapterAddress, @@ -426,10 +459,17 @@ export function VaultInitializationModal({ ); }; + // Don't render if required data is missing + if (!isOpen || !vaultAddress || !chainId) { + return null; + } + return ( { + if (!open) close(); + }} size="lg" scrollBehavior="inside" className="bg-background dark:border border-gray-700" @@ -438,7 +478,7 @@ export function VaultInitializationModal({ title={stepTitle} description="Complete vault initialization to start using your vault" mainIcon={} - onClose={() => onOpenChange(false)} + onClose={close} /> {currentStep === 'deploy' && ( diff --git a/src/features/autovault/components/vault-detail/modals/vault-settings-modal.tsx b/src/features/autovault/components/vault-detail/modals/vault-settings-modal.tsx index 694069bf..589e7f64 100644 --- a/src/features/autovault/components/vault-detail/modals/vault-settings-modal.tsx +++ b/src/features/autovault/components/vault-detail/modals/vault-settings-modal.tsx @@ -1,14 +1,13 @@ 'use client'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback } from 'react'; import { ReloadIcon } from '@radix-ui/react-icons'; import { FiSettings } from 'react-icons/fi'; import type { Address } from 'viem'; import { Modal, ModalBody, ModalHeader } from '@/components/common/Modal'; -import type { VaultV2Cap } from '@/data-sources/morpho-api/v2-vaults'; -import type { CapData } from '@/hooks/useVaultV2Data'; -import type { SupportedNetworks } from '@/utils/networks'; import { GeneralTab, AgentsTab, CapsTab, type SettingsTab } from '../settings'; +import { useVaultSettingsModalStore } from '@/stores/vault-settings-modal-store'; +import { useVaultV2Data } from '@/hooks/useVaultV2Data'; const TABS: { id: SettingsTab; label: string }[] = [ { id: 'general', label: 'General' }, @@ -17,108 +16,51 @@ const TABS: { id: SettingsTab; label: string }[] = [ ]; type VaultSettingsModalProps = { - isOpen: boolean; - onOpenChange: (open: boolean) => void; - initialTab?: SettingsTab; - isOwner: boolean; - onUpdateMetadata: (values: { name?: string; symbol?: string }) => Promise; - updatingMetadata: boolean; - defaultName: string; - defaultSymbol: string; - currentName: string; - currentSymbol: string; - owner?: string; - curator?: string; - allocators: string[]; - sentinels?: string[]; - chainId: SupportedNetworks; - vaultAsset?: Address; - marketAdapter: Address; // the deploy morpho market v1 adapter - capData?: CapData; - onSetAllocator: (allocator: Address, isAllocator: boolean) => Promise; - updateCaps: (caps: VaultV2Cap[]) => Promise; - isUpdatingAllocator: boolean; - isUpdatingCaps: boolean; - onRefresh?: () => void; - isRefreshing?: boolean; + vaultAddress: Address; + chainId: number; }; -export function VaultSettingsModal({ - isOpen, - onOpenChange, - initialTab = 'general', - isOwner, - onUpdateMetadata, - updatingMetadata, - defaultName, - defaultSymbol, - currentName, - currentSymbol, - owner, - curator, - allocators, - sentinels = [], - chainId, - vaultAsset, - marketAdapter, - capData = undefined, - onSetAllocator, - updateCaps, - isUpdatingAllocator, - isUpdatingCaps, - onRefresh, - isRefreshing = false, -}: VaultSettingsModalProps) { - const [activeTab, setActiveTab] = useState(initialTab); +/** + * VaultSettingsModal - Self-contained modal using Pull pattern. + * Tabs pull their own data using hooks internally. + * + * Open: useVaultSettingsModalStore().open('tabName') + */ +export function VaultSettingsModal({ vaultAddress, chainId }: VaultSettingsModalProps) { + // UI state from Zustand + const { isOpen, activeTab, close, setTab } = useVaultSettingsModalStore(); - useEffect(() => { - if (isOpen) { - setActiveTab(initialTab); - } - }, [initialTab, isOpen]); + // Only pull data needed for the modal header refresh button + const vaultDataQuery = useVaultV2Data({ vaultAddress, chainId }); - const handleTabChange = useCallback((tab: SettingsTab) => { - setActiveTab(tab); - }, []); + const handleTabChange = useCallback( + (tab: SettingsTab) => { + setTab(tab); + }, + [setTab], + ); const renderActiveTab = () => { switch (activeTab) { case 'general': return ( ); case 'agents': return ( ); case 'caps': return ( ); default: @@ -133,7 +75,9 @@ export function VaultSettingsModal({ return ( { + if (!open) close(); + }} size="5xl" scrollBehavior="inside" className="w-full max-w-6xl" @@ -143,20 +87,16 @@ export function VaultSettingsModal({ title="Vault Settings" description="Manage metadata, automation agents, and vault caps" mainIcon={} - onClose={() => onOpenChange(false)} - auxiliaryAction={ - onRefresh - ? { - icon: , - onClick: () => { - if (!isRefreshing) { - onRefresh(); - } - }, - ariaLabel: 'Refresh vault data', - } - : undefined - } + onClose={close} + auxiliaryAction={{ + icon: , + onClick: () => { + if (!vaultDataQuery.isLoading) { + void vaultDataQuery.refetch(); + } + }, + ariaLabel: 'Refresh vault data', + }} />
diff --git a/src/features/autovault/components/vault-detail/settings/AgentsTab.tsx b/src/features/autovault/components/vault-detail/settings/AgentsTab.tsx index a23ed95e..b600350c 100644 --- a/src/features/autovault/components/vault-detail/settings/AgentsTab.tsx +++ b/src/features/autovault/components/vault-detail/settings/AgentsTab.tsx @@ -1,23 +1,30 @@ import { useCallback, useState } from 'react'; import type { Address } from 'viem'; +import { useConnection } from 'wagmi'; import { AccountIdentity } from '@/components/shared/account-identity'; import { Button } from '@/components/ui/button'; import { Spinner } from '@/components/ui/spinner'; import { useMarketNetwork } from '@/hooks/useMarketNetwork'; +import { useVaultV2Data } from '@/hooks/useVaultV2Data'; +import { useVaultV2 } from '@/hooks/useVaultV2'; import { v2AgentsBase } from '@/utils/monarch-agent'; import { AgentListItem } from './AgentListItem'; import type { AgentsTabProps } from './types'; -export function AgentsTab({ - isOwner, - owner, - curator, - allocators, - // sentinels = [], - onSetAllocator, - isUpdatingAllocator, - chainId, -}: AgentsTabProps) { +export function AgentsTab({ vaultAddress, chainId }: AgentsTabProps) { + const { address: connectedAddress } = useConnection(); + + // Pull data directly - TanStack Query deduplicates + const { data: vaultData } = useVaultV2Data({ vaultAddress, chainId }); + const { isOwner, setAllocator, isUpdatingAllocator } = useVaultV2({ + vaultAddress, + chainId, + connectedAddress, + }); + + const owner = vaultData?.owner; + const curator = vaultData?.curator; + const allocators = vaultData?.allocators ?? []; const [allocatorToAdd, setAllocatorToAdd] = useState
(null); const [allocatorToRemove, setAllocatorToRemove] = useState
(null); const [isEditingAllocators, setIsEditingAllocators] = useState(false); @@ -36,12 +43,12 @@ export function AgentsTab({ setAllocatorToAdd(allocator); try { - await onSetAllocator(allocator, true); + await setAllocator(allocator, true); } finally { setAllocatorToAdd(null); } }, - [onSetAllocator, needSwitchChain, switchToNetwork], + [setAllocator, needSwitchChain, switchToNetwork], ); const handleRemoveAllocator = useCallback( @@ -53,12 +60,12 @@ export function AgentsTab({ } setAllocatorToRemove(allocator); - const success = await onSetAllocator(allocator, false); + const success = await setAllocator(allocator, false); if (success) { setAllocatorToRemove(null); } }, - [onSetAllocator, needSwitchChain, switchToNetwork], + [setAllocator, needSwitchChain, switchToNetwork], ); const renderSingleRole = (label: string, description: string, addressValue?: string) => { diff --git a/src/features/autovault/components/vault-detail/settings/CapsTab.tsx b/src/features/autovault/components/vault-detail/settings/CapsTab.tsx index e76a83d3..ea88922f 100644 --- a/src/features/autovault/components/vault-detail/settings/CapsTab.tsx +++ b/src/features/autovault/components/vault-detail/settings/CapsTab.tsx @@ -1,9 +1,31 @@ import { useState } from 'react'; +import type { Address } from 'viem'; +import { useConnection } from 'wagmi'; +import { useVaultV2Data } from '@/hooks/useVaultV2Data'; +import { useVaultV2 } from '@/hooks/useVaultV2'; +import { useMorphoMarketV1Adapters } from '@/hooks/useMorphoMarketV1Adapters'; import { CurrentCaps } from './CurrentCaps'; import { EditCaps } from './EditCaps'; import type { CapsTabProps } from './types'; -export function CapsTab({ isOwner, chainId, vaultAsset, adapterAddress, existingCaps, updateCaps, isUpdatingCaps }: CapsTabProps) { +export function CapsTab({ vaultAddress, chainId }: CapsTabProps) { + const { address: connectedAddress } = useConnection(); + + // Pull data directly - TanStack Query deduplicates + const { data: vaultData } = useVaultV2Data({ vaultAddress, chainId }); + const { isOwner, updateCaps, isUpdatingCaps } = useVaultV2({ + vaultAddress, + chainId, + connectedAddress, + }); + const { morphoMarketV1Adapter: adapterAddress } = useMorphoMarketV1Adapters({ + vaultAddress, + chainId, + }); + + const vaultAsset = vaultData?.assetAddress as Address | undefined; + const existingCaps = vaultData?.capsData; + const [isEditing, setIsEditing] = useState(false); return isEditing ? ( diff --git a/src/features/autovault/components/vault-detail/settings/GeneralTab.tsx b/src/features/autovault/components/vault-detail/settings/GeneralTab.tsx index 7416a471..c820ab3f 100644 --- a/src/features/autovault/components/vault-detail/settings/GeneralTab.tsx +++ b/src/features/autovault/components/vault-detail/settings/GeneralTab.tsx @@ -1,20 +1,28 @@ import { useCallback, useEffect, useId, useMemo, useState } from 'react'; +import { useConnection } from 'wagmi'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { Spinner } from '@/components/ui/spinner'; import { useMarketNetwork } from '@/hooks/useMarketNetwork'; +import { useVaultV2Data } from '@/hooks/useVaultV2Data'; +import { useVaultV2 } from '@/hooks/useVaultV2'; import type { GeneralTabProps } from './types'; -export function GeneralTab({ - isOwner, - defaultName, - defaultSymbol, - currentName, - currentSymbol, - onUpdateMetadata, - updatingMetadata, - chainId, -}: GeneralTabProps) { +export function GeneralTab({ vaultAddress, chainId }: GeneralTabProps) { + const { address: connectedAddress } = useConnection(); + + // Pull data directly - TanStack Query deduplicates + const { data: vaultData } = useVaultV2Data({ vaultAddress, chainId }); + const { isOwner, name, symbol, updateNameAndSymbol, isUpdatingMetadata } = useVaultV2({ + vaultAddress, + chainId, + connectedAddress, + }); + + const defaultName = vaultData?.displayName ?? ''; + const defaultSymbol = vaultData?.displaySymbol ?? ''; + const currentName = name; + const currentSymbol = symbol; const nameInputId = useId(); const symbolInputId = useId(); @@ -65,7 +73,7 @@ export function GeneralTab({ return; } - const success = await onUpdateMetadata({ + const success = await updateNameAndSymbol({ name: trimmedName !== previousName ? trimmedName || undefined : undefined, symbol: trimmedSymbol !== previousSymbol ? trimmedSymbol || undefined : undefined, }); @@ -73,7 +81,7 @@ export function GeneralTab({ if (success) { setMetadataError(null); } - }, [metadataChanged, onUpdateMetadata, previousName, previousSymbol, trimmedName, trimmedSymbol, needSwitchChain, switchToNetwork]); + }, [metadataChanged, updateNameAndSymbol, previousName, previousSymbol, trimmedName, trimmedSymbol, needSwitchChain, switchToNetwork]); return (
@@ -127,10 +135,10 @@ export function GeneralTab({ className="ml-auto" variant="surface" size="sm" - disabled={!metadataChanged || updatingMetadata || !isOwner} + disabled={!metadataChanged || isUpdatingMetadata || !isOwner} onClick={() => void handleMetadataSubmit()} > - {updatingMetadata ? ( + {isUpdatingMetadata ? ( Saving... diff --git a/src/features/autovault/components/vault-detail/settings/types.ts b/src/features/autovault/components/vault-detail/settings/types.ts index eef686b4..1eb1e160 100644 --- a/src/features/autovault/components/vault-detail/settings/types.ts +++ b/src/features/autovault/components/vault-detail/settings/types.ts @@ -1,6 +1,4 @@ import type { Address } from 'viem'; -import type { VaultV2Cap } from '@/data-sources/morpho-api/v2-vaults'; -import type { CapData } from '@/hooks/useVaultV2Data'; import type { SupportedNetworks } from '@/utils/networks'; import type { Market } from '@/utils/types'; @@ -13,33 +11,16 @@ export type MarketCapState = { }; export type GeneralTabProps = { - isOwner: boolean; - defaultName: string; - defaultSymbol: string; - currentName: string; - currentSymbol: string; - onUpdateMetadata: (values: { name?: string; symbol?: string }) => Promise; - updatingMetadata: boolean; + vaultAddress: Address; chainId: SupportedNetworks; }; export type AgentsTabProps = { - isOwner: boolean; - owner?: string; - curator?: string; - allocators: string[]; - sentinels?: string[]; - onSetAllocator: (allocator: Address, isAllocator: boolean) => Promise; - isUpdatingAllocator: boolean; + vaultAddress: Address; chainId: SupportedNetworks; }; export type CapsTabProps = { - isOwner: boolean; + vaultAddress: Address; chainId: SupportedNetworks; - vaultAsset?: Address; - adapterAddress?: Address; - existingCaps?: CapData; - updateCaps: (caps: VaultV2Cap[]) => Promise; - isUpdatingCaps: boolean; }; diff --git a/src/features/autovault/components/vault-detail/total-supply-card.tsx b/src/features/autovault/components/vault-detail/total-supply-card.tsx index 15c357dd..8eadfcbe 100644 --- a/src/features/autovault/components/vault-detail/total-supply-card.tsx +++ b/src/features/autovault/components/vault-detail/total-supply-card.tsx @@ -3,35 +3,41 @@ import { Card, CardBody, CardHeader } from '@/components/ui/card'; import { Tooltip } from '@/components/ui/tooltip'; import { GoPlusCircle } from 'react-icons/go'; import type { Address } from 'viem'; +import { useConnection } from 'wagmi'; import { TokenIcon } from '@/components/shared/token-icon'; import { formatBalance } from '@/utils/balance'; +import { useVaultV2Data } from '@/hooks/useVaultV2Data'; +import { useVaultV2 } from '@/hooks/useVaultV2'; +import { useVaultPage } from '@/hooks/useVaultPage'; +import type { SupportedNetworks } from '@/utils/networks'; import { DepositToVaultModal } from './modals/deposit-to-vault-modal'; type VaultTotalAssetsCardProps = { - totalAssets?: bigint; - vault24hEarnings?: bigint | null; - tokenDecimals?: number; - tokenSymbol?: string; - assetAddress?: Address; - chainId: number; vaultAddress: Address; - vaultName: string; - onRefresh?: () => void; - isLoading?: boolean; + chainId: SupportedNetworks; }; -export function TotalSupplyCard({ - tokenDecimals, - tokenSymbol, - assetAddress, - chainId, - vaultAddress, - vaultName, - totalAssets, - vault24hEarnings, - onRefresh, - isLoading = false, -}: VaultTotalAssetsCardProps): JSX.Element { +export function TotalSupplyCard({ vaultAddress, chainId }: VaultTotalAssetsCardProps): JSX.Element { + const { address: connectedAddress } = useConnection(); + + // Pull data directly - TanStack Query deduplicates + const { data: vaultData, isLoading: vaultDataLoading } = useVaultV2Data({ vaultAddress, chainId }); + const { + totalAssets, + isLoading: contractLoading, + refetch, + } = useVaultV2({ + vaultAddress, + chainId, + connectedAddress, + }); + const { vault24hEarnings } = useVaultPage({ vaultAddress, chainId, connectedAddress }); + + const tokenDecimals = vaultData?.tokenDecimals; + const tokenSymbol = vaultData?.tokenSymbol; + const assetAddress = vaultData?.assetAddress as Address | undefined; + const vaultName = vaultData?.displayName ?? ''; + const isLoading = vaultDataLoading || contractLoading; const [showDepositModal, setShowDepositModal] = useState(false); const totalAssetsLabel = useMemo(() => { @@ -70,7 +76,7 @@ export function TotalSupplyCard({ const handleDepositSuccess = () => { setShowDepositModal(false); - onRefresh?.(); + void refetch(); }; const cardStyle = 'bg-surface rounded shadow-sm'; diff --git a/src/features/autovault/components/vault-detail/vault-allocator-card.tsx b/src/features/autovault/components/vault-detail/vault-allocator-card.tsx index 7800c0c1..ab750049 100644 --- a/src/features/autovault/components/vault-detail/vault-allocator-card.tsx +++ b/src/features/autovault/components/vault-detail/vault-allocator-card.tsx @@ -5,25 +5,34 @@ import { GearIcon } from '@radix-ui/react-icons'; import { BsQuestionCircle } from 'react-icons/bs'; import { GrStatusGood } from 'react-icons/gr'; import type { Address } from 'viem'; +import { useConnection } from 'wagmi'; import { AgentIcon } from '@/components/shared/agent-icon'; import { TooltipContent } from '@/components/shared/tooltip-content'; import { findAgent } from '@/utils/monarch-agent'; +import { useVaultV2Data } from '@/hooks/useVaultV2Data'; +import { useVaultV2 } from '@/hooks/useVaultV2'; +import { useVaultSettingsModalStore } from '@/stores/vault-settings-modal-store'; +import type { SupportedNetworks } from '@/utils/networks'; type VaultAllocatorCardProps = { - allocators: string[]; - onManageAgents: () => void; - needsSetup?: boolean; - isOwner?: boolean; - isLoading?: boolean; + vaultAddress: Address; + chainId: SupportedNetworks; + needsInitialization: boolean; }; -export function VaultAllocatorCard({ - allocators, - onManageAgents, - needsSetup = false, - isOwner = false, - isLoading = false, -}: VaultAllocatorCardProps) { +export function VaultAllocatorCard({ vaultAddress, chainId, needsInitialization }: VaultAllocatorCardProps) { + const { address: connectedAddress } = useConnection(); + + // Pull data directly - TanStack Query deduplicates + const { data: vaultData, isLoading: vaultDataLoading } = useVaultV2Data({ vaultAddress, chainId }); + const { isOwner } = useVaultV2({ vaultAddress, chainId, connectedAddress }); + + // UI state from Zustand + const { open: openSettings } = useVaultSettingsModalStore(); + + const allocators = vaultData?.allocators ?? []; + const isLoading = vaultDataLoading; + const cardStyle = 'bg-surface rounded shadow-sm'; const maxDisplay = 5; const iconSize = 20; @@ -45,17 +54,17 @@ export function VaultAllocatorCard({ Allocators - {isOwner && !needsSetup && ( + {isOwner && !needsInitialization && ( openSettings('agents')} /> )} {isLoading ? (
- ) : needsSetup ? ( + ) : needsInitialization ? (
diff --git a/src/features/autovault/components/vault-detail/vault-collaterals-card.tsx b/src/features/autovault/components/vault-detail/vault-collaterals-card.tsx index 459b5880..372251a9 100644 --- a/src/features/autovault/components/vault-detail/vault-collaterals-card.tsx +++ b/src/features/autovault/components/vault-detail/vault-collaterals-card.tsx @@ -1,30 +1,35 @@ import { Card, CardBody, CardHeader } from '@/components/ui/card'; import { GearIcon } from '@radix-ui/react-icons'; import type { Address } from 'viem'; +import { useConnection } from 'wagmi'; import { TokenIcon } from '@/components/shared/token-icon'; import { Tooltip } from '@/components/ui/tooltip'; import { TooltipContent } from '@/components/shared/tooltip-content'; -import type { VaultV2Cap } from '@/data-sources/morpho-api/v2-vaults'; import { parseCapIdParams } from '@/utils/morpho'; import type { SupportedNetworks } from '@/utils/networks'; +import { useVaultV2Data } from '@/hooks/useVaultV2Data'; +import { useVaultV2 } from '@/hooks/useVaultV2'; +import { useVaultSettingsModalStore } from '@/stores/vault-settings-modal-store'; type VaultCollateralsCardProps = { - collateralCaps: VaultV2Cap[]; + vaultAddress: Address; chainId: SupportedNetworks; - onManageCaps: () => void; - needsSetup?: boolean; - isOwner?: boolean; - isLoading?: boolean; + needsInitialization: boolean; }; -export function VaultCollateralsCard({ - collateralCaps, - chainId, - onManageCaps, - needsSetup = false, - isOwner = false, - isLoading = false, -}: VaultCollateralsCardProps) { +export function VaultCollateralsCard({ vaultAddress, chainId, needsInitialization }: VaultCollateralsCardProps) { + const { address: connectedAddress } = useConnection(); + + // Pull data directly - TanStack Query deduplicates + const { data: vaultData, isLoading: vaultDataLoading } = useVaultV2Data({ vaultAddress, chainId }); + const { isOwner } = useVaultV2({ vaultAddress, chainId, connectedAddress }); + + // UI state from Zustand + const { open: openSettings } = useVaultSettingsModalStore(); + + const collateralCaps = vaultData?.capsData?.collateralCaps ?? []; + const isLoading = vaultDataLoading; + const cardStyle = 'bg-surface rounded shadow-sm'; const maxDisplay = 5; const iconSize = 20; @@ -44,17 +49,17 @@ export function VaultCollateralsCard({ Collaterals - {isOwner && !needsSetup && ( + {isOwner && !needsInitialization && ( openSettings('caps')} /> )} {isLoading ? (
- ) : needsSetup ? ( + ) : needsInitialization ? (
diff --git a/src/features/autovault/components/vault-detail/vault-market-allocations.tsx b/src/features/autovault/components/vault-detail/vault-market-allocations.tsx index a06df4d1..14745a0d 100644 --- a/src/features/autovault/components/vault-detail/vault-market-allocations.tsx +++ b/src/features/autovault/components/vault-detail/vault-market-allocations.tsx @@ -2,22 +2,21 @@ import { useMemo, useState } from 'react'; import { IconSwitch } from '@/components/ui/icon-switch'; import { HiOutlineCube } from 'react-icons/hi'; import { MdOutlineAccountBalance } from 'react-icons/md'; -import type { CollateralAllocation, MarketAllocation } from '@/types/vaultAllocations'; +import type { Address } from 'viem'; +import { useConnection } from 'wagmi'; import type { SupportedNetworks } from '@/utils/networks'; import { useMarkets } from '@/hooks/useMarkets'; +import { useVaultV2Data } from '@/hooks/useVaultV2Data'; +import { useVaultV2 } from '@/hooks/useVaultV2'; +import { useVaultAllocations } from '@/hooks/useVaultAllocations'; import { TableContainerWithDescription } from '@/components/common/table-container-with-header'; import { CollateralView } from './allocations/allocations/collateral-view'; import { MarketView } from './allocations/allocations/market-view'; type VaultMarketAllocationsProps = { - totalAssets?: bigint; - collateralAllocations: CollateralAllocation[]; - marketAllocations: MarketAllocation[]; - vaultAssetSymbol: string; - vaultAssetDecimals: number; + vaultAddress: Address; chainId: SupportedNetworks; - isLoading: boolean; - needsInitialization?: boolean; + needsInitialization: boolean; }; type ViewMode = 'collateral' | 'market'; @@ -26,16 +25,22 @@ function ViewIcon({ isSelected, className }: { isSelected?: boolean; className?: return isSelected ? : ; } -export function VaultMarketAllocations({ - totalAssets, - collateralAllocations, - marketAllocations, - vaultAssetSymbol, - vaultAssetDecimals, - chainId, - isLoading, - needsInitialization = false, -}: VaultMarketAllocationsProps) { +export function VaultMarketAllocations({ vaultAddress, chainId, needsInitialization }: VaultMarketAllocationsProps) { + const { address: connectedAddress } = useConnection(); + + // Pull data directly - TanStack Query deduplicates + const { data: vaultData, isLoading: vaultDataLoading } = useVaultV2Data({ vaultAddress, chainId }); + const { totalAssets } = useVaultV2({ vaultAddress, chainId, connectedAddress }); + const { + collateralAllocations, + marketAllocations, + loading: allocationsLoading, + } = useVaultAllocations({ + vaultAddress, + chainId, + }); + + const isLoading = vaultDataLoading || allocationsLoading; const [viewMode, setViewMode] = useState('market'); const { loading: marketsLoading } = useMarkets(); @@ -52,11 +57,12 @@ export function VaultMarketAllocations({ const hasAnyAllocations = useMemo(() => totalAllocation > 0n, [totalAllocation]); const viewDescription = useMemo(() => { + if (!vaultData) return ''; if (viewMode === 'collateral') { - return `See how your ${vaultAssetSymbol} supply is collateralized across assets shared by multiple markets.`; + return `See how your ${vaultData.tokenSymbol} supply is collateralized across assets shared by multiple markets.`; } - return `See where your ${vaultAssetSymbol} supply is deployed across markets.`; - }, [viewMode, vaultAssetSymbol]); + return `See where your ${vaultData.tokenSymbol} supply is deployed across markets.`; + }, [viewMode, vaultData]); // Show loading state when either allocations or markets context is still loading if (isLoading || marketsLoading) { @@ -78,6 +84,8 @@ export function VaultMarketAllocations({ ); } + if (!vaultData) return null; + const hasNoAllocations = collateralAllocations.length === 0 && marketAllocations.length === 0; const headerActions = ( @@ -115,16 +123,16 @@ export function VaultMarketAllocations({ ) : ( )} diff --git a/src/features/autovault/vault-view.tsx b/src/features/autovault/vault-view.tsx index 34706065..44baa61c 100644 --- a/src/features/autovault/vault-view.tsx +++ b/src/features/autovault/vault-view.tsx @@ -12,6 +12,9 @@ import { Tooltip } from '@/components/ui/tooltip'; import { TooltipContent } from '@/components/shared/tooltip-content'; import Header from '@/components/layout/header/Header'; import { useVaultPage } from '@/hooks/useVaultPage'; +import { useVaultV2Data } from '@/hooks/useVaultV2Data'; +import { useVaultV2 } from '@/hooks/useVaultV2'; +import { useMorphoMarketV1Adapters } from '@/hooks/useMorphoMarketV1Adapters'; import { useVaultIndexing } from '@/hooks/useVaultIndexing'; import { getSlicedAddress } from '@/utils/address'; import { ALL_SUPPORTED_NETWORKS, SupportedNetworks, getNetworkConfig } from '@/utils/networks'; @@ -23,6 +26,8 @@ import { VaultMarketAllocations } from '@/features/autovault/components/vault-de import { VaultSettingsModal } from '@/features/autovault/components/vault-detail/modals/vault-settings-modal'; import { VaultSummaryMetrics } from '@/features/autovault/components/vault-detail/vault-summary-metrics'; import { TransactionHistoryPreview } from '@/features/history/components/transaction-history-preview'; +import { useVaultSettingsModalStore } from '@/stores/vault-settings-modal-store'; +import { useVaultInitializationModalStore } from '@/stores/vault-initialization-modal-store'; export default function VaultContent() { const { chainId: chainIdParam, vaultAddress } = useParams<{ @@ -55,35 +60,41 @@ export default function VaultContent() { } }, [chainId]); - // Unified data hook - all vault data and actions in one place - const vault = useVaultPage({ + // Pull minimal data for vault-view itself + const vaultDataQuery = useVaultV2Data({ vaultAddress: vaultAddressValue, chainId }); + const vaultContract = useVaultV2({ vaultAddress: vaultAddressValue, chainId, connectedAddress, + onTransactionSuccess: vaultDataQuery.refetch, }); + const adapterQuery = useMorphoMarketV1Adapters({ vaultAddress: vaultAddressValue, chainId }); - const { - refetchAll, - completeInitialization, - isInitializing, - updateNameAndSymbol, - setAllocator, - refetchAdapter, - collateralAllocations, - marketAllocations, - vaultAPY, - vault24hEarnings, - isAPYLoading, - } = vault; + // Only use useVaultPage for complex computed state + const { vaultAPY, isAPYLoading, isVaultInitialized, needsInitialization } = useVaultPage({ + vaultAddress: vaultAddressValue, + chainId, + connectedAddress, + }); + // Aggregated refetch function const handleRefreshVault = useCallback(() => { - void refetchAll(); - }, [refetchAll]); + void vaultDataQuery.refetch(); + void vaultContract.refetch(); + void adapterQuery.refetch(); + }, [vaultDataQuery, vaultContract, adapterQuery]); + + // Extract minimal data for vault-view rendering + const vaultData = vaultDataQuery.data; + const hasError = vaultDataQuery.isError; + const vaultDataLoading = vaultDataQuery.isLoading; + const title = vaultData?.displayName ?? `Vault ${getSlicedAddress(vaultAddressValue)}`; + const symbolToDisplay = vaultData?.displaySymbol; // Determine if vault data has loaded successfully const isDataLoaded = useMemo(() => { - return !vault.vaultDataLoading && !vault.hasError && vault.vaultData !== null; - }, [vault.vaultDataLoading, vault.hasError, vault.vaultData]); + return !vaultDataLoading && !hasError && vaultData !== null; + }, [vaultDataLoading, hasError, vaultData]); // Use indexing hook to manage retry logic and toast const { isIndexing } = useVaultIndexing({ @@ -93,40 +104,16 @@ export default function VaultContent() { refetch: handleRefreshVault, }); - const handleUpdateMetadata = useCallback( - async (values: { name?: string; symbol?: string }) => updateNameAndSymbol(values), - [updateNameAndSymbol], - ); - - const handleSetAllocator = useCallback( - async (allocator: Address, isAllocator: boolean) => setAllocator(allocator, isAllocator), - [setAllocator], - ); - - const handleRefetchAdapter = useCallback(() => { - void refetchAdapter(); - }, [refetchAdapter]); + // UI state from Zustand stores (for vault-view banners only) + const { open: openSettings } = useVaultSettingsModalStore(); + const { open: openInitialization } = useVaultInitializationModalStore(); - const handleAdapterConfigured = useCallback(() => { - void refetchAll(); - }, [refetchAll]); + // Computed state flags for vault-view banners + const hasNoAllocators = (vaultData?.allocators ?? []).length === 0; + const capsUninitialized = + !vaultData?.capsData || (vaultData.capsData.collateralCaps.length === 0 && vaultData.capsData.marketCaps.length === 0); - // UI state - const [settingsTab, setSettingsTab] = useState<'general' | 'agents' | 'caps'>('general'); - const [showSettings, setShowSettings] = useState(false); - const [showInitializationModal, setShowInitializationModal] = useState(false); - - // Derived display data - const fallbackTitle = `Vault ${getSlicedAddress(vaultAddressValue)}`; - const title = vault.vaultData?.displayName ?? fallbackTitle; - const symbolToDisplay = vault.vaultData?.displaySymbol; - const allocators = vault.vaultData?.allocators ?? []; - const sentinels = vault.vaultData?.sentinels ?? []; - const capData = vault.vaultData?.capsData; - const collateralCaps = capData?.collateralCaps ?? []; - const assetAddress = vault.vaultData?.assetAddress; - - // Format APY + // Format APY for APY card in vault-view const apyLabel = useMemo(() => { if (vaultAPY === null || vaultAPY === undefined) return '0%'; return `${(vaultAPY * 100).toFixed(2)}%`; @@ -168,7 +155,7 @@ export default function VaultContent() { } // Show error state if data failed to load (but not while indexing) - if (vault.hasError) { + if (hasError) { return (
@@ -210,14 +197,14 @@ export default function VaultContent() { variant="ghost" size="sm" onClick={handleRefreshVault} - disabled={vault.vaultDataLoading} + disabled={vaultDataLoading} className="text-secondary min-w-0 px-2" > - + - {vault.isOwner && ( + {vaultContract.isOwner && ( { - setSettingsTab('general'); - setShowSettings(true); - }} + onClick={() => openSettings('general')} > @@ -243,7 +227,7 @@ export default function VaultContent() {
{/* Setup Banner - Show if vault needs initialization */} - {vault.needsInitialization && vault.isOwner && networkConfig?.vaultConfig?.marketV1AdapterFactory && ( + {needsInitialization && vaultContract.isOwner && networkConfig?.vaultConfig?.marketV1AdapterFactory && (

Complete vault setup

@@ -255,7 +239,7 @@ export default function VaultContent() { variant="primary" size="sm" className="mt-3 sm:mt-0" - onClick={() => setShowInitializationModal(true)} + onClick={openInitialization} > Start Setup @@ -263,7 +247,7 @@ export default function VaultContent() { )} {/* Only show allocator/caps banners if vault IS initialized */} - {vault.isVaultInitialized && vault.hasNoAllocators && vault.isOwner && ( + {isVaultInitialized && hasNoAllocators && vaultContract.isOwner && (

Choose an agent

@@ -273,17 +257,14 @@ export default function VaultContent() { variant="primary" size="sm" className="mt-3 sm:mt-0" - onClick={() => { - setSettingsTab('agents'); - setShowSettings(true); - }} + onClick={() => openSettings('agents')} > Configure agents
)} - {vault.isVaultInitialized && vault.capsUninitialized && vault.isOwner && ( + {isVaultInitialized && capsUninitialized && vaultContract.isOwner && (

Configure allocation caps

@@ -295,10 +276,7 @@ export default function VaultContent() { variant="primary" size="sm" className="mt-3 sm:mt-0" - onClick={() => { - setSettingsTab('caps'); - setShowSettings(true); - }} + onClick={() => openSettings('caps')} > Configure caps @@ -308,23 +286,15 @@ export default function VaultContent() { {/* Summary Metrics */} Current APY - {vault.isLoading || isAPYLoading ? ( + {vaultContract.isLoading || isAPYLoading ? (
) : (
{apyLabel}
@@ -332,52 +302,28 @@ export default function VaultContent() { { - if (vault.needsInitialization && networkConfig?.vaultConfig?.marketV1AdapterFactory) { - setShowInitializationModal(true); - return; - } - setSettingsTab('agents'); - setShowSettings(true); - }} - needsSetup={vault.needsInitialization} - isOwner={vault.isOwner} - isLoading={vault.vaultDataLoading} + vaultAddress={vaultAddressValue} + chainId={chainId} + needsInitialization={needsInitialization} /> { - if (vault.needsInitialization && networkConfig?.vaultConfig?.marketV1AdapterFactory) { - setShowInitializationModal(true); - return; - } - setSettingsTab('caps'); - setShowSettings(true); - }} - needsSetup={vault.needsInitialization} - isOwner={vault.isOwner} - isLoading={vault.vaultDataLoading} + needsInitialization={needsInitialization} /> {/* Market Allocations */} {/* Transaction History Preview - only show when vault is fully set up */} - {vault.adapter && vault.isVaultInitialized && !vault.capsUninitialized && ( + {adapterQuery.morphoMarketV1Adapter && isVaultInitialized && !capsUninitialized && ( )} - {/* Settings Modal */} + {/* Settings Modal - Pulls own data */}
- {/* Initialization Modal */} - {networkConfig?.vaultConfig?.marketV1AdapterFactory && ( - - )} + {/* Initialization Modal - Pulls own data from URL params */} + {networkConfig?.vaultConfig?.marketV1AdapterFactory && }
); } diff --git a/src/features/positions/components/vault-allocation-detail.tsx b/src/features/positions/components/vault-allocation-detail.tsx index 05a344ed..e78a10ea 100644 --- a/src/features/positions/components/vault-allocation-detail.tsx +++ b/src/features/positions/components/vault-allocation-detail.tsx @@ -11,7 +11,6 @@ import type { UserVaultV2 } from '@/data-sources/subgraph/v2-vaults'; import { useRateLabel } from '@/hooks/useRateLabel'; import { useVaultAllocations } from '@/hooks/useVaultAllocations'; import { formatBalance } from '@/utils/balance'; -import { parseCapIdParams } from '@/utils/morpho'; import { AllocationCell } from './allocation-cell'; type VaultAllocationDetailProps = { @@ -21,27 +20,8 @@ type VaultAllocationDetailProps = { export function VaultAllocationDetail({ vault }: VaultAllocationDetailProps) { const { short: rateLabel } = useRateLabel(); - // Separate collateral and market caps - const { collateralCaps, marketCaps } = useMemo(() => { - const collat: typeof vault.caps = []; - const market: typeof vault.caps = []; - - vault.caps.forEach((cap) => { - const params = parseCapIdParams(cap.idParams); - if (params.type === 'collateral') { - collat.push(cap); - } else if (params.type === 'market') { - market.push(cap); - } - }); - - return { collateralCaps: collat, marketCaps: market }; - }, [vault.caps]); - - // Fetch actual allocations + // Fetch actual allocations - useVaultAllocations pulls caps internally const { marketAllocations, loading } = useVaultAllocations({ - collateralCaps, - marketCaps, vaultAddress: vault.address as Address, chainId: vault.networkId, enabled: true, diff --git a/src/hooks/useAllocations.ts b/src/hooks/useAllocations.ts index ecb77699..5a3cd7d7 100644 --- a/src/hooks/useAllocations.ts +++ b/src/hooks/useAllocations.ts @@ -1,5 +1,6 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useMemo } from 'react'; import type { Address } from 'viem'; +import { useQuery } from '@tanstack/react-query'; import type { VaultV2Cap } from '@/data-sources/morpho-api/v2-vaults'; import type { SupportedNetworks } from '@/utils/networks'; import { readAllocation } from '@/utils/vaultAllocation'; @@ -17,18 +18,7 @@ type UseAllocationsArgs = { enabled?: boolean; }; -type UseAllocationsReturn = { - allocations: AllocationData[]; - loading: boolean; - error: Error | null; - refetch: () => Promise; -}; - -export function useAllocations({ vaultAddress, chainId, caps = [], enabled = true }: UseAllocationsArgs): UseAllocationsReturn { - const [allocations, setAllocations] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - +export function useAllocations({ vaultAddress, chainId, caps = [], enabled = true }: UseAllocationsArgs) { // Create a stable key from capIds to detect actual changes const capsKey = useMemo(() => { return caps @@ -37,17 +27,13 @@ export function useAllocations({ vaultAddress, chainId, caps = [], enabled = tru .join(','); }, [caps]); - const load = useCallback(async () => { - if (!vaultAddress || !enabled || caps.length === 0) { - setAllocations([]); - setLoading(false); - return; - } + const query = useQuery({ + queryKey: ['vault-allocations', vaultAddress, chainId, capsKey], + queryFn: async () => { + if (!vaultAddress || caps.length === 0) { + return []; + } - setLoading(true); - setError(null); - - try { // Read all allocations in parallel const allocationPromises = caps.map(async (cap) => { const allocation = await readAllocation(vaultAddress, cap.capId as `0x${string}`, chainId); @@ -60,28 +46,16 @@ export function useAllocations({ vaultAddress, chainId, caps = [], enabled = tru }); const results = await Promise.all(allocationPromises); - setAllocations(results); - } catch (err) { - const errorObj = err instanceof Error ? err : new Error('Failed to fetch allocations'); - setError(errorObj); - console.error('Error fetching allocations:', err); - } finally { - setLoading(false); - } - }, [vaultAddress, chainId, capsKey, enabled]); // Use capsKey instead of caps - - useEffect(() => { - void load(); - }, [load]); - - const refetch = useCallback(async () => { - await load(); - }, [load]); + return results; + }, + enabled: enabled && Boolean(vaultAddress) && caps.length > 0, + staleTime: 30_000, // 30 seconds - allocation data is cacheable + }); return { - allocations, - loading, - error, - refetch, + allocations: query.data ?? [], + isLoading: query.isLoading, + error: query.error, + refetch: query.refetch, }; } diff --git a/src/hooks/useMorphoMarketV1Adapters.ts b/src/hooks/useMorphoMarketV1Adapters.ts index 20a4a119..4af68874 100644 --- a/src/hooks/useMorphoMarketV1Adapters.ts +++ b/src/hooks/useMorphoMarketV1Adapters.ts @@ -1,14 +1,11 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useMemo } from 'react'; import { type Address, zeroAddress } from 'viem'; -import { fetchMorphoMarketV1Adapters, type MorphoMarketV1AdapterRecord } from '@/data-sources/subgraph/morpho-market-v1-adapters'; +import { useQuery } from '@tanstack/react-query'; +import { fetchMorphoMarketV1Adapters } from '@/data-sources/subgraph/morpho-market-v1-adapters'; import { getMorphoAddress } from '@/utils/morpho'; import { getNetworkConfig, type SupportedNetworks } from '@/utils/networks'; export function useMorphoMarketV1Adapters({ vaultAddress, chainId }: { vaultAddress?: Address; chainId: SupportedNetworks }) { - const [adapters, setAdapters] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const vaultConfig = useMemo(() => { try { return getNetworkConfig(chainId).vaultConfig; @@ -20,44 +17,33 @@ export function useMorphoMarketV1Adapters({ vaultAddress, chainId }: { vaultAddr const subgraphUrl = vaultConfig?.adapterSubgraphEndpoint ?? null; const morpho = useMemo(() => getMorphoAddress(chainId), [chainId]); - const fetchAdapters = useCallback(async () => { - if (!vaultAddress || !subgraphUrl) { - setAdapters([]); - setError(null); - setLoading(false); - return; - } - - setLoading(true); - setError(null); + const query = useQuery({ + queryKey: ['morpho-market-v1-adapters', vaultAddress, chainId], + queryFn: async () => { + if (!vaultAddress || !subgraphUrl) { + return []; + } - try { const result = await fetchMorphoMarketV1Adapters({ subgraphUrl, parentVault: vaultAddress, morpho, }); - setAdapters(result); - } catch (err) { - setError(err instanceof Error ? err : new Error('Failed to fetch adapters')); - setAdapters([]); - } finally { - setLoading(false); - } - }, [vaultAddress, subgraphUrl, morpho]); - useEffect(() => { - void fetchAdapters(); - }, [fetchAdapters]); + return result; + }, + enabled: Boolean(vaultAddress && subgraphUrl), + staleTime: 30_000, // 30 seconds - adapter data is cacheable + }); - const morphoMarketV1Adapter = useMemo(() => (adapters.length == 0 ? zeroAddress : adapters[0].adapter), [adapters]); + const morphoMarketV1Adapter = useMemo(() => (query.data && query.data.length > 0 ? query.data[0].adapter : zeroAddress), [query.data]); return { morphoMarketV1Adapter, - adapters, // all market adapters (should only be just one) - loading, - error, - refetch: fetchAdapters, - hasAdapters: adapters.length > 0, + adapters: query.data ?? [], // all market adapters (should only be just one) + isLoading: query.isLoading, + error: query.error, + refetch: query.refetch, + hasAdapters: (query.data ?? []).length > 0, }; } diff --git a/src/hooks/useVaultAllocations.ts b/src/hooks/useVaultAllocations.ts index 2b90aeb0..ceaa74d1 100644 --- a/src/hooks/useVaultAllocations.ts +++ b/src/hooks/useVaultAllocations.ts @@ -1,16 +1,15 @@ import { useMemo } from 'react'; import type { Address } from 'viem'; -import type { VaultV2Cap } from '@/data-sources/morpho-api/v2-vaults'; import type { CollateralAllocation, MarketAllocation } from '@/types/vaultAllocations'; +import type { VaultV2Cap } from '@/data-sources/morpho-api/v2-vaults'; import { parseCapIdParams } from '@/utils/morpho'; import type { SupportedNetworks } from '@/utils/networks'; import { findToken } from '@/utils/tokens'; import { useAllocations } from './useAllocations'; import { useMarkets } from './useMarkets'; +import { useVaultV2Data } from './useVaultV2Data'; type UseVaultAllocationsArgs = { - collateralCaps: VaultV2Cap[]; - marketCaps: VaultV2Cap[]; vaultAddress: Address; chainId: SupportedNetworks; enabled?: boolean; @@ -21,7 +20,7 @@ type UseVaultAllocationsReturn = { marketAllocations: MarketAllocation[]; loading: boolean; error: Error | null; - refetch: () => Promise; + refetch: () => void; }; /** @@ -35,15 +34,15 @@ type UseVaultAllocationsReturn = { * 4. Fetches on-chain allocations only for valid caps * 5. Returns typed, ready-to-use allocation structures */ -export function useVaultAllocations({ - collateralCaps, - marketCaps, - vaultAddress, - chainId, - enabled = true, -}: UseVaultAllocationsArgs): UseVaultAllocationsReturn { +export function useVaultAllocations({ vaultAddress, chainId, enabled = true }: UseVaultAllocationsArgs): UseVaultAllocationsReturn { const { allMarkets } = useMarkets(); + // Pull vault data directly - TanStack Query handles deduplication + const { data: vaultData } = useVaultV2Data({ vaultAddress, chainId }); + + const collateralCaps = vaultData?.capsData?.collateralCaps ?? []; + const marketCaps = vaultData?.capsData?.marketCaps ?? []; + // Parse and filter collateral caps const { validCollateralCaps, parsedCollateralCaps } = useMemo(() => { const valid: VaultV2Cap[] = []; @@ -109,13 +108,15 @@ export function useVaultAllocations({ const allValidCaps = useMemo(() => [...validCollateralCaps, ...validMarketCaps], [validCollateralCaps, validMarketCaps]); // Fetch allocations only for valid, recognized caps - const { allocations, loading, error, refetch } = useAllocations({ + const { allocations, isLoading, error, refetch } = useAllocations({ vaultAddress, chainId, caps: allValidCaps, enabled: enabled && allValidCaps.length > 0, }); + const loading = isLoading; + // Create allocation map for efficient lookup const allocationMap = useMemo(() => { const map = new Map(); @@ -148,6 +149,8 @@ export function useVaultAllocations({ marketAllocations, loading, error, - refetch, + refetch: () => { + void refetch(); + }, }; } diff --git a/src/hooks/useVaultPage.ts b/src/hooks/useVaultPage.ts index 2a59aef0..2ec37532 100644 --- a/src/hooks/useVaultPage.ts +++ b/src/hooks/useVaultPage.ts @@ -14,92 +14,45 @@ type UseVaultPageArgs = { }; /** - * Unified hook for vault page data and actions. - * Combines all vault-related data fetching and provides computed state. + * Simplified vault page hook - ONLY computes complex derived state. + * Components should pull raw data themselves using individual hooks. + * + * Use this for: + * - Complex computations requiring multiple data sources + * - Expensive calculations (APY) + * - Aggregated refetch functions + * + * DON'T use this for: + * - Raw data (use useVaultV2Data, etc. directly) + * - Simple 1-liner computations (do in component) */ export function useVaultPage({ vaultAddress, chainId, connectedAddress }: UseVaultPageArgs) { - // Fetch vault data from API/subgraph - const { - data: vaultData, - loading: vaultDataLoading, - error: vaultDataError, - refetch: refetchVaultData, - } = useVaultV2Data({ - vaultAddress, - chainId, - }); - - // Memoize transaction success handler to prevent infinite refetch loops - const handleTransactionSuccess = useCallback(() => { - void refetchVaultData(); - }, [refetchVaultData]); - - // Fetch vault contract state and actions - const { - isLoading: contractLoading, - refetch: refetchContract, - completeInitialization, - isInitializing, - updateNameAndSymbol, - isUpdatingMetadata, - name: onChainName, - symbol: onChainSymbol, - owner: contractOwner, - setAllocator, - isUpdatingAllocator, - updateCaps, - isUpdatingCaps, - totalAssets, - } = useVaultV2({ - vaultAddress, - chainId, - onTransactionSuccess: handleTransactionSuccess, - }); - - // Fetch market adapter - const { morphoMarketV1Adapter, loading: adapterLoading, refetch: refetchAdapter } = useMorphoMarketV1Adapters({ vaultAddress, chainId }); - - // Compute initialization state - // A vault goes through these states: - // 1. Vault deployed (has address) - // 2. Adapter deployed (morphoMarketV1Adapter !== zeroAddress) - // 3. Vault initialized (adapter registered + registry set) - API returns data - // 4. Fully configured (adapter cap + collateral caps + market caps set) - // - // The Morpho API returns data once the vault is initialized (state 3+). - // Caps can be configured separately after initialization. - const isVaultInitialized = useMemo(() => { - // Still loading - can't determine state yet - if (adapterLoading || vaultDataLoading) { - return false; - } - - // If adapter not deployed, definitely not initialized - if (morphoMarketV1Adapter === zeroAddress) { - return false; - } + // Pull only what we need for computations + const vaultDataQuery = useVaultV2Data({ vaultAddress, chainId }); + const contract = useVaultV2({ vaultAddress, chainId, connectedAddress, onTransactionSuccess: vaultDataQuery.refetch }); + const adapterQuery = useMorphoMarketV1Adapters({ vaultAddress, chainId }); + const allocationsQuery = useVaultAllocations({ vaultAddress, chainId }); - // If adapter exists and we have vault data from API, the vault is initialized - // (Morpho API only returns data for initialized vaults that have been registered) - // Note: Caps may or may not be set at this point - that's a separate configuration step - return vaultData !== null && vaultData !== undefined; - }, [adapterLoading, vaultDataLoading, morphoMarketV1Adapter, vaultData]); + // Complex derived state: isVaultInitialized (needs multiple sources) + const isVaultInitialized = useMemo(() => { + if (adapterQuery.isLoading || vaultDataQuery.isLoading) return false; + if (adapterQuery.morphoMarketV1Adapter === zeroAddress) return false; + return vaultDataQuery.data !== null && vaultDataQuery.data !== undefined; + }, [adapterQuery.isLoading, adapterQuery.morphoMarketV1Adapter, vaultDataQuery.isLoading, vaultDataQuery.data]); - // Helper flag: adapter not deployed at all (need to deploy it first) const needsAdapterDeployment = useMemo( - () => !adapterLoading && morphoMarketV1Adapter === zeroAddress, - [adapterLoading, morphoMarketV1Adapter], + () => !adapterQuery.isLoading && adapterQuery.morphoMarketV1Adapter === zeroAddress, + [adapterQuery.isLoading, adapterQuery.morphoMarketV1Adapter], ); - // Fetch adapter positions for APY calculation (only last 24h, only current chain) + // Fetch adapter positions for APY calculation const { positions: adapterPositions, isEarningsLoading: isAPYLoading } = useUserPositionsSummaryData( - !needsAdapterDeployment && morphoMarketV1Adapter !== zeroAddress ? morphoMarketV1Adapter : undefined, + !needsAdapterDeployment && adapterQuery.morphoMarketV1Adapter !== zeroAddress ? adapterQuery.morphoMarketV1Adapter : undefined, 'day', [chainId], ); - // Calculate vault APY from adapter positions (weighted average) - // Uses normalized decimals to avoid precision loss from BigInt->Number conversion + // Expensive computation: vaultAPY (weighted average across positions) const vaultAPY = useMemo(() => { if (!adapterPositions || adapterPositions.length === 0) return null; @@ -107,7 +60,6 @@ export function useVaultPage({ vaultAddress, chainId, connectedAddress }: UseVau let weightedAPY = 0; for (const position of adapterPositions) { - // Normalize to human-readable decimals to avoid overflow/precision loss const suppliedNorm = Number(formatUnits(BigInt(position.state.supplyAssets), position.market.loanAsset.decimals)); if (suppliedNorm <= 0) continue; @@ -120,15 +72,13 @@ export function useVaultPage({ vaultAddress, chainId, connectedAddress }: UseVau return weightedAPY / totalSuppliedNorm; }, [adapterPositions]); - // Calculate total 24h earnings from adapter positions + // Calculate total 24h earnings const vault24hEarnings = useMemo(() => { if (!adapterPositions || adapterPositions.length === 0) return null; let total = 0n; - adapterPositions.forEach((position) => { if (position.earned) { - // Sum up all earnings (assumes they're in raw bigint string format) total += BigInt(position.earned); } }); @@ -136,111 +86,33 @@ export function useVaultPage({ vaultAddress, chainId, connectedAddress }: UseVau return total; }, [adapterPositions]); - // Determine ownership from contract owner (not API data, since API returns null for uninitialized vaults) - const isOwner = useMemo( - () => Boolean(contractOwner && connectedAddress && contractOwner.toLowerCase() === connectedAddress.toLowerCase()), - [contractOwner, connectedAddress], - ); - - const hasNoAllocators = useMemo( - () => !needsAdapterDeployment && (vaultData?.allocators ?? []).length === 0, - [needsAdapterDeployment, vaultData?.allocators], - ); - - const capsUninitialized = useMemo(() => vaultData?.capsData?.needSetupCaps ?? true, [vaultData?.capsData?.needSetupCaps]); - - // Fetch and parse allocations with typed structures - const { - collateralAllocations, - marketAllocations, - loading: allocationsLoading, - error: allocationsError, - refetch: refetchAllocations, - } = useVaultAllocations({ - collateralCaps: vaultData?.capsData?.collateralCaps ?? [], - marketCaps: vaultData?.capsData?.marketCaps ?? [], - vaultAddress, - chainId, - enabled: !needsAdapterDeployment && !!vaultData?.capsData, - }); - - // Unified refetch function - const refetchAll = useCallback(() => { - void refetchVaultData(); - void refetchContract(); - void refetchAdapter(); - void refetchAllocations(); - }, [refetchVaultData, refetchContract, refetchAdapter, refetchAllocations]); - - // Loading states - wait for ALL queries before showing content - const isLoading = vaultDataLoading || contractLoading || adapterLoading || allocationsLoading; - const hasError = !!vaultDataError || !!allocationsError; - - // Comprehensive check: needs initialization if vault is deployed but not fully initialized - // This captures the state where: - // - Vault contract is deployed - // - Adapter may or may not be deployed - // - But the vault hasn't been initialized (registry not set, adapter not registered, caps not set) + // Complex derived state: needsInitialization const needsInitialization = useMemo(() => { - // Don't show initialization prompt while still loading - if (isLoading) { - return false; - } - - // If vault is already initialized, no need for initialization - if (isVaultInitialized) { - return false; - } - - // At this point: vault exists but is not initialized - // This covers both cases: - // 1. Adapter not deployed yet (need to deploy + initialize) - // 2. Adapter deployed but not connected to vault (need to initialize) + const isLoading = vaultDataQuery.isLoading || contract.isLoading || adapterQuery.isLoading; + if (isLoading) return false; + if (isVaultInitialized) return false; return true; - }, [isLoading, isVaultInitialized]); + }, [vaultDataQuery.isLoading, contract.isLoading, adapterQuery.isLoading, isVaultInitialized]); + // Aggregated refetch function (convenience) + const refetchAll = useCallback(() => { + void vaultDataQuery.refetch(); + void contract.refetch(); + void adapterQuery.refetch(); + void allocationsQuery.refetch(); + }, [vaultDataQuery, contract, adapterQuery, allocationsQuery]); + + // Return ONLY computed/derived state - no raw data! return { - // Data - vaultData, - totalAssets, - collateralAllocations, - marketAllocations, - adapter: morphoMarketV1Adapter, - onChainName, - onChainSymbol, - - // APY & Earnings + // Complex computed state + isVaultInitialized, + needsAdapterDeployment, + needsInitialization, vaultAPY, vault24hEarnings, isAPYLoading, - // Computed state - isOwner, - needsAdapterDeployment, - needsInitialization, - isVaultInitialized, - hasNoAllocators, - capsUninitialized, - - // Loading/Error states - isLoading, - vaultDataLoading, - allocationsLoading, - adapterLoading, - hasError, - - // Actions - completeInitialization, - updateNameAndSymbol, - setAllocator, - updateCaps, + // Aggregated utilities refetchAll, - refetchAdapter, - - // Action loading states - isInitializing, - isUpdatingMetadata, - isUpdatingAllocator, - isUpdatingCaps, }; } diff --git a/src/hooks/useVaultV2.ts b/src/hooks/useVaultV2.ts index c67c9b12..8d211c88 100644 --- a/src/hooks/useVaultV2.ts +++ b/src/hooks/useVaultV2.ts @@ -1,88 +1,86 @@ import { useCallback, useMemo } from 'react'; import { type Address, encodeFunctionData, zeroAddress, toFunctionSelector } from 'viem'; -import { useConnection, useChainId, useReadContract } from 'wagmi'; +import { useConnection, useChainId, useReadContracts } from 'wagmi'; import { vaultv2Abi } from '@/abis/vaultv2'; import type { VaultV2Cap } from '@/data-sources/morpho-api/v2-vaults'; import type { SupportedNetworks } from '@/utils/networks'; import { useTransactionWithToast } from './useTransactionWithToast'; +/** + * @notice Reading and Writing hook (via wagmi) for Morpho V2 Vaults + */ export function useVaultV2({ vaultAddress, chainId, + connectedAddress, onTransactionSuccess, }: { vaultAddress?: Address; chainId?: SupportedNetworks | number; + connectedAddress?: Address; onTransactionSuccess?: () => void; }) { const connectedChainId = useChainId(); const chainIdToUse = (chainId ?? connectedChainId) as SupportedNetworks; const { address: account } = useConnection(); - const { data: owner } = useReadContract({ - address: vaultAddress, + const vaultContract = { + address: vaultAddress ?? zeroAddress, abi: vaultv2Abi, - functionName: 'owner', - args: [], chainId: chainIdToUse, - query: { - enabled: Boolean(vaultAddress), - }, - }); - - const { data: curator } = useReadContract({ - address: vaultAddress, - abi: vaultv2Abi, - functionName: 'curator', - args: [], - chainId: chainIdToUse, - query: { - enabled: Boolean(vaultAddress), - }, - }); - - const { data: rawName } = useReadContract({ - address: vaultAddress, - abi: vaultv2Abi, - functionName: 'name', - args: [], - chainId: chainIdToUse, - query: { - enabled: Boolean(vaultAddress), - }, - }); - - const { data: rawSymbol } = useReadContract({ - address: vaultAddress, - abi: vaultv2Abi, - functionName: 'symbol', - args: [], - chainId: chainIdToUse, - query: { - enabled: Boolean(vaultAddress), - }, - }); + }; - // Read totalAssets directly from the vault contract const { - data: totalAssets, - refetch: refetchBalance, - isLoading: loadingBalance, - } = useReadContract({ - address: vaultAddress, - abi: vaultv2Abi, - functionName: 'totalAssets', - chainId: chainIdToUse, + data: batchData, + refetch: refetchAll, + isLoading, + } = useReadContracts({ + contracts: [ + { + // owner + ...vaultContract, + functionName: 'owner', + args: [], + }, + { + // curator + ...vaultContract, + functionName: 'curator', + args: [], + }, + { + // name + ...vaultContract, + functionName: 'name', + args: [], + }, + { + // symbol + ...vaultContract, + functionName: 'symbol', + args: [], + }, + { + // totalAssets + ...vaultContract, + functionName: 'totalAssets', + args: [], + }, + ], query: { - enabled: Boolean(vaultAddress), + enabled: vaultContract.address !== zeroAddress, }, }); - const currentCurator = useMemo(() => (curator as Address | undefined) ?? zeroAddress, [curator]); - - const refetchAll = useCallback(() => { - void refetchBalance(); - }, [refetchBalance]); + const [owner, curator, name, symbol, totalAssets] = useMemo(() => { + return [ + batchData?.[0].result ?? zeroAddress, + batchData?.[1].result ?? zeroAddress, + batchData?.[2].result ?? '', + batchData?.[3].result ?? '', + batchData?.[4].result ?? 0n, + ]; + }, [batchData]); const { isConfirming: isInitializing, sendTransactionAsync: sendInitializationTx } = useTransactionWithToast({ toastId: `init-${vaultAddress ?? 'unknown'}`, @@ -157,7 +155,7 @@ export function useVaultV2({ } // Step 1. Assign curator if unset. - if (currentCurator === zeroAddress) { + if (curator === zeroAddress) { const setCuratorTx = encodeFunctionData({ abi: vaultv2Abi, functionName: 'setCurator', @@ -258,7 +256,7 @@ export function useVaultV2({ throw initError; } }, - [account, chainIdToUse, currentCurator, sendInitializationTx, vaultAddress], + [account, chainIdToUse, curator, sendInitializationTx, vaultAddress], ); const updateNameAndSymbol = useCallback( @@ -550,29 +548,20 @@ export function useVaultV2({ [account, chainIdToUse, sendWithdrawTx, vaultAddress], ); - const name = useMemo(() => { - if (!rawName) return ''; - return String(rawName); - }, [rawName]); - - const symbol = useMemo(() => { - if (!rawSymbol) return ''; - return String(rawSymbol); - }, [rawSymbol]); - - const vaultOwner = useMemo(() => { - if (!owner) return zeroAddress; - return owner as Address; - }, [owner]); + const isOwner = useMemo( + () => Boolean(owner && connectedAddress && owner.toLowerCase() === connectedAddress.toLowerCase()), + [owner, connectedAddress], + ); return { - isLoading: loadingBalance, + isLoading: isLoading, refetch: refetchAll, completeInitialization, isInitializing, name, symbol, - owner: vaultOwner, + owner, + isOwner, updateNameAndSymbol, isUpdatingMetadata, setAllocator, diff --git a/src/hooks/useVaultV2Data.ts b/src/hooks/useVaultV2Data.ts index 6988636d..c05d9ea3 100644 --- a/src/hooks/useVaultV2Data.ts +++ b/src/hooks/useVaultV2Data.ts @@ -1,5 +1,5 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; import type { Address } from 'viem'; +import { useQuery } from '@tanstack/react-query'; import { useTokens } from '@/components/providers/TokenProvider'; import { fetchVaultV2Details, type VaultV2Cap } from '@/data-sources/morpho-api/v2-vaults'; import { getSlicedAddress } from '@/utils/address'; @@ -24,8 +24,8 @@ export type VaultV2Data = { displayName: string; displaySymbol: string; assetAddress: string; - tokenSymbol?: string; - tokenDecimals?: number; + tokenSymbol: string; // Always has default: '--' + tokenDecimals: number; // Always has default: 18 allocators: string[]; sentinels: string[]; owner: string; @@ -35,46 +35,29 @@ export type VaultV2Data = { curatorDisplay: string; }; -type UseVaultV2DataReturn = { - data: VaultV2Data | null; - loading: boolean; - error: Error | null; - refetch: () => Promise; -}; - -export function useVaultV2Data({ - vaultAddress, - chainId, - fallbackName = '', - fallbackSymbol = '', -}: UseVaultV2DataArgs): UseVaultV2DataReturn { +export function useVaultV2Data({ vaultAddress, chainId, fallbackName = '', fallbackSymbol = '' }: UseVaultV2DataArgs) { const { findToken } = useTokens(); - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - const load = useCallback(async () => { - if (!vaultAddress) { - setData(null); - setLoading(false); - return; - } - - setLoading(true); - setError(null); + const query = useQuery({ + queryKey: ['vault-v2-data', vaultAddress, chainId], + queryFn: async () => { + if (!vaultAddress) { + return null; + } - try { const result = await fetchVaultV2Details(vaultAddress, chainId); if (!result) { - setData(null); - return; + return null; } const token = result.asset ? findToken(result.asset, chainId) : undefined; const curatorDisplay = result.curator ? getSlicedAddress(result.curator as Address) : '--'; + // Apply defaults for token info + const tokenSymbol = token?.symbol ?? '--'; + const tokenDecimals = token?.decimals ?? 18; + // Parse caps by level using parseCapIdParams let adapterCap: VaultV2Cap | null = null; const collateralCaps: VaultV2Cap[] = []; @@ -95,12 +78,12 @@ export function useVaultV2Data({ // if any one of the caps is not set, it means it still need setup! const needSetupCaps = !adapterCap || collateralCaps.length === 0 || marketCaps.length === 0; - setData({ + const vaultData: VaultV2Data = { displayName: result.name || fallbackName, displaySymbol: result.symbol || fallbackSymbol, assetAddress: result.asset, - tokenSymbol: token?.symbol, - tokenDecimals: token?.decimals, + tokenSymbol, // Defaults applied above + tokenDecimals, // Defaults applied above allocators: result.allocators, sentinels: result.sentinels, owner: result.owner, @@ -113,31 +96,13 @@ export function useVaultV2Data({ }, adapters: result.adapters, curatorDisplay, - }); - } catch (err) { - setError(err instanceof Error ? err : new Error('Failed to fetch vault data')); - setData(null); - } finally { - setLoading(false); - } - }, [vaultAddress, chainId]); - - useEffect(() => { - void load(); - }, [load]); - - // Memoize the refetch function to prevent unnecessary re-renders in parent components - const refetch = useCallback(async () => { - await load(); - }, [load]); - - return useMemo( - () => ({ - data, - loading, - error, - refetch, - }), - [data, error, loading, refetch], - ); + }; + + return vaultData; + }, + enabled: Boolean(vaultAddress), + staleTime: 30_000, // 30 seconds - data is cacheable across components + }); + + return query; } diff --git a/src/stores/vault-initialization-modal-store.ts b/src/stores/vault-initialization-modal-store.ts new file mode 100644 index 00000000..3b079af3 --- /dev/null +++ b/src/stores/vault-initialization-modal-store.ts @@ -0,0 +1,45 @@ +import { create } from 'zustand'; + +type VaultInitializationModalState = { + isOpen: boolean; +}; + +type VaultInitializationModalActions = { + /** + * Open the initialization modal + */ + open: () => void; + + /** + * Close the initialization modal + */ + close: () => void; +}; + +type VaultInitializationModalStore = VaultInitializationModalState & VaultInitializationModalActions; + +/** + * Zustand store for vault initialization modal state. + * Manages modal visibility for the vault setup flow. + * + * @example + * ```tsx + * // Open initialization modal + * const { open } = useVaultInitializationModalStore(); + * open(); + * + * // In modal component + * const { isOpen, close } = useVaultInitializationModalStore(); + * ``` + */ +export const useVaultInitializationModalStore = create((set) => ({ + isOpen: false, + + open: () => { + set({ isOpen: true }); + }, + + close: () => { + set({ isOpen: false }); + }, +})); diff --git a/src/stores/vault-settings-modal-store.ts b/src/stores/vault-settings-modal-store.ts new file mode 100644 index 00000000..5527d235 --- /dev/null +++ b/src/stores/vault-settings-modal-store.ts @@ -0,0 +1,58 @@ +import { create } from 'zustand'; + +export type SettingsTab = 'general' | 'agents' | 'caps'; + +type VaultSettingsModalState = { + isOpen: boolean; + activeTab: SettingsTab; +}; + +type VaultSettingsModalActions = { + /** + * Open the settings modal, optionally specifying which tab to show + */ + open: (tab?: SettingsTab) => void; + + /** + * Close the settings modal + */ + close: () => void; + + /** + * Switch to a different tab + */ + setTab: (tab: SettingsTab) => void; +}; + +type VaultSettingsModalStore = VaultSettingsModalState & VaultSettingsModalActions; + +/** + * Zustand store for vault settings modal state. + * Manages modal visibility and active tab selection. + * + * @example + * ```tsx + * // Open settings modal on agents tab + * const { open } = useVaultSettingsModalStore(); + * open('agents'); + * + * // In modal component + * const { isOpen, activeTab, close } = useVaultSettingsModalStore(); + * ``` + */ +export const useVaultSettingsModalStore = create((set) => ({ + isOpen: false, + activeTab: 'general', + + open: (tab = 'general') => { + set({ isOpen: true, activeTab: tab }); + }, + + close: () => { + set({ isOpen: false }); + }, + + setTab: (tab) => { + set({ activeTab: tab }); + }, +}));