diff --git a/src/components/shared/address-identity.tsx b/src/components/shared/address-identity.tsx index 2b5b263d..12075905 100644 --- a/src/components/shared/address-identity.tsx +++ b/src/components/shared/address-identity.tsx @@ -1,3 +1,4 @@ +import type { ReactNode } from 'react'; import Link from 'next/link'; import { ExternalLinkIcon } from '@radix-ui/react-icons'; import { Avatar } from '@/components/Avatar/Avatar'; @@ -8,24 +9,24 @@ type AddressIdentityProps = { address: string; chainId: number; label?: string; + icon?: ReactNode; isToken?: boolean; tokenSymbol?: string; }; -/** - * Use to display address, not Account. Better used for contracts - * @param param0 - * @returns - */ -export function AddressIdentity({ address, chainId, label, isToken, tokenSymbol }: AddressIdentityProps) { +const truncateAddress = (address: string) => `${address.slice(0, 6)}...${address.slice(-4)}`; + +export function AddressIdentity({ address, chainId, label, icon, isToken, tokenSymbol }: AddressIdentityProps) { + const displayText = label ? `${label} ${truncateAddress(address)}` : truncateAddress(address); + return ( - {isToken ? ( + {icon ?? (isToken ? ( - )} - {label && {label}} - - {address.slice(0, 6)}...{address.slice(-4)} - + ))} + {displayText} ); diff --git a/src/features/autovault/components/vault-detail/modals/vault-settings/VaultSettingsContent.tsx b/src/features/autovault/components/vault-detail/modals/vault-settings/VaultSettingsContent.tsx index 8efd43e2..b6e8e6af 100644 --- a/src/features/autovault/components/vault-detail/modals/vault-settings/VaultSettingsContent.tsx +++ b/src/features/autovault/components/vault-detail/modals/vault-settings/VaultSettingsContent.tsx @@ -6,7 +6,7 @@ import { slideVariants, slideTransition, type SlideDirection } from '@/component import type { SupportedNetworks } from '@/utils/networks'; import { VaultSettingsHeader } from './VaultSettingsHeader'; import { GeneralPanel, RolesPanel, CapsPanel } from './panels'; -import { EditCapsDetail } from './details'; +import { EditCapsDetail, EditAllocatorsDetail, EditMetadataDetail } from './details'; import type { VaultSettingsCategory, VaultDetailView } from '@/stores/vault-settings-modal-store'; type PanelProps = { @@ -29,6 +29,8 @@ type DetailProps = { const DETAIL_COMPONENTS: Record, React.ComponentType> = { 'edit-caps': EditCapsDetail, + 'edit-allocators': EditAllocatorsDetail, + 'edit-metadata': EditMetadataDetail, }; type VaultSettingsContentProps = { diff --git a/src/features/autovault/components/vault-detail/modals/vault-settings/VaultSettingsModal.tsx b/src/features/autovault/components/vault-detail/modals/vault-settings/VaultSettingsModal.tsx index a326391a..4321d2f0 100644 --- a/src/features/autovault/components/vault-detail/modals/vault-settings/VaultSettingsModal.tsx +++ b/src/features/autovault/components/vault-detail/modals/vault-settings/VaultSettingsModal.tsx @@ -1,6 +1,5 @@ 'use client'; -import { useCallback } from 'react'; import type { Address } from 'viem'; import { Modal } from '@/components/common/Modal'; import { useVaultSettingsModalStore } from '@/stores/vault-settings-modal-store'; @@ -33,13 +32,6 @@ export function VaultSettingsModal({ vaultAddress, chainId }: VaultSettingsModal toggleSidebar, } = useVaultSettingsModalStore(); - const handleCategoryChange = useCallback( - (category: typeof activeCategory) => { - setCategory(category); - }, - [setCategory], - ); - if (!isOpen) { return null; } @@ -61,7 +53,7 @@ export function VaultSettingsModal({ vaultAddress, chainId }: VaultSettingsModal collapsed={sidebarCollapsed} onToggle={toggleSidebar} selectedCategory={activeCategory} - onSelectCategory={handleCategoryChange} + onSelectCategory={setCategory} disabled={activeDetailView !== null} /> , string> = { 'edit-caps': 'Edit Caps', + 'edit-allocators': 'Edit Allocators', + 'edit-metadata': 'Edit Metadata', }; diff --git a/src/features/autovault/components/vault-detail/modals/vault-settings/details/EditAllocatorsDetail.tsx b/src/features/autovault/components/vault-detail/modals/vault-settings/details/EditAllocatorsDetail.tsx new file mode 100644 index 00000000..6f1b3b22 --- /dev/null +++ b/src/features/autovault/components/vault-detail/modals/vault-settings/details/EditAllocatorsDetail.tsx @@ -0,0 +1,41 @@ +'use client'; + +import type { Address } from 'viem'; +import { useConnection } from 'wagmi'; +import { useVaultV2Data } from '@/hooks/useVaultV2Data'; +import { useVaultV2 } from '@/hooks/useVaultV2'; +import type { SupportedNetworks } from '@/utils/networks'; +import { EditAllocators } from '../../../settings/EditAllocators'; + +type EditAllocatorsDetailProps = { + vaultAddress: Address; + chainId: SupportedNetworks; + onBack: () => void; +}; + +export function EditAllocatorsDetail({ vaultAddress, chainId, onBack }: EditAllocatorsDetailProps) { + const { address: connectedAddress } = useConnection(); + + const { data: vaultData } = useVaultV2Data({ vaultAddress, chainId }); + const { isOwner, setAllocator, isUpdatingAllocator } = useVaultV2({ + vaultAddress, + chainId, + connectedAddress, + // Note: onTransactionSuccess is not used here because we update inplace and don't navigate back automatically + // The user clicks "Done" when finished + }); + + const allocators = (vaultData?.allocators ?? []) as Address[]; + + return ( + setAllocator(allocator, true)} + onRemoveAllocator={(allocator) => setAllocator(allocator, false)} + onBack={onBack} + /> + ); +} diff --git a/src/features/autovault/components/vault-detail/modals/vault-settings/details/EditCapsDetail.tsx b/src/features/autovault/components/vault-detail/modals/vault-settings/details/EditCapsDetail.tsx index 37dea685..6e5f97b2 100644 --- a/src/features/autovault/components/vault-detail/modals/vault-settings/details/EditCapsDetail.tsx +++ b/src/features/autovault/components/vault-detail/modals/vault-settings/details/EditCapsDetail.tsx @@ -41,7 +41,7 @@ export function EditCapsDetail({ vaultAddress, chainId, onBack }: EditCapsDetail isOwner={isOwner} isUpdating={isUpdatingCaps} adapterAddress={adapterAddress} - onCancel={onBack} + onBack={onBack} onSave={async (caps) => { const success = await updateCaps(caps); // Don't call onBack() here - onTransactionSuccess handles navigation after tx confirms diff --git a/src/features/autovault/components/vault-detail/modals/vault-settings/details/EditMetadataDetail.tsx b/src/features/autovault/components/vault-detail/modals/vault-settings/details/EditMetadataDetail.tsx new file mode 100644 index 00000000..da2fb549 --- /dev/null +++ b/src/features/autovault/components/vault-detail/modals/vault-settings/details/EditMetadataDetail.tsx @@ -0,0 +1,43 @@ +'use client'; + +import type { Address } from 'viem'; +import { useConnection } from 'wagmi'; +import { useVaultV2Data } from '@/hooks/useVaultV2Data'; +import { useVaultV2 } from '@/hooks/useVaultV2'; +import type { SupportedNetworks } from '@/utils/networks'; +import { EditMetadata } from '../../../settings/EditMetadata'; + +type EditMetadataDetailProps = { + vaultAddress: Address; + chainId: SupportedNetworks; + onBack: () => void; +}; + +export function EditMetadataDetail({ vaultAddress, chainId, onBack }: EditMetadataDetailProps) { + const { address: connectedAddress } = useConnection(); + + const { data: vaultData } = useVaultV2Data({ vaultAddress, chainId }); + const { isOwner, name, symbol, updateNameAndSymbol, isUpdatingMetadata } = useVaultV2({ + vaultAddress, + chainId, + connectedAddress, + onTransactionSuccess: onBack, + }); + + const defaultName = vaultData?.displayName ?? ''; + const defaultSymbol = vaultData?.displaySymbol ?? ''; + + return ( + updateNameAndSymbol({ name: newName, symbol: newSymbol })} + onBack={onBack} + /> + ); +} diff --git a/src/features/autovault/components/vault-detail/modals/vault-settings/details/index.ts b/src/features/autovault/components/vault-detail/modals/vault-settings/details/index.ts index 5bc1d22a..0e76d4a0 100644 --- a/src/features/autovault/components/vault-detail/modals/vault-settings/details/index.ts +++ b/src/features/autovault/components/vault-detail/modals/vault-settings/details/index.ts @@ -1 +1,3 @@ export { EditCapsDetail } from './EditCapsDetail'; +export { EditAllocatorsDetail } from './EditAllocatorsDetail'; +export { EditMetadataDetail } from './EditMetadataDetail'; diff --git a/src/features/autovault/components/vault-detail/modals/vault-settings/panels/GeneralPanel.tsx b/src/features/autovault/components/vault-detail/modals/vault-settings/panels/GeneralPanel.tsx index 8c0f90d7..f77de024 100644 --- a/src/features/autovault/components/vault-detail/modals/vault-settings/panels/GeneralPanel.tsx +++ b/src/features/autovault/components/vault-detail/modals/vault-settings/panels/GeneralPanel.tsx @@ -1,27 +1,25 @@ 'use client'; -import { useCallback, useEffect, useId, useMemo, useState } from 'react'; import type { Address } from 'viem'; 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 { SupportedNetworks } from '@/utils/networks'; +import type { VaultDetailView } from '@/stores/vault-settings-modal-store'; type GeneralPanelProps = { vaultAddress: Address; chainId: SupportedNetworks; + onNavigateToDetail?: (view: Exclude) => void; }; -export function GeneralPanel({ vaultAddress, chainId }: GeneralPanelProps) { +export function GeneralPanel({ vaultAddress, chainId, onNavigateToDetail }: GeneralPanelProps) { const { address: connectedAddress } = useConnection(); // Pull data directly - TanStack Query deduplicates const { data: vaultData } = useVaultV2Data({ vaultAddress, chainId }); - const { isOwner, name, symbol, updateNameAndSymbol, isUpdatingMetadata } = useVaultV2({ + const { isOwner, name, symbol } = useVaultV2({ vaultAddress, chainId, connectedAddress, @@ -29,133 +27,39 @@ export function GeneralPanel({ vaultAddress, chainId }: GeneralPanelProps) { const defaultName = vaultData?.displayName ?? ''; const defaultSymbol = vaultData?.displaySymbol ?? ''; - const currentName = name; - const currentSymbol = symbol; - const nameInputId = useId(); - const symbolInputId = useId(); - - const previousName = useMemo(() => currentName.trim(), [currentName]); - const previousSymbol = useMemo(() => currentSymbol.trim(), [currentSymbol]); - - const [nameInput, setNameInput] = useState(previousName ?? defaultName); - const [symbolInput, setSymbolInput] = useState(previousSymbol ?? defaultSymbol); - const [metadataError, setMetadataError] = useState(null); - - const { needSwitchChain, switchToNetwork } = useMarketNetwork({ - targetChainId: chainId, - }); - - // Reset inputs when current values change - useEffect(() => { - setNameInput(previousName ?? defaultName); - setSymbolInput(previousSymbol ?? defaultSymbol); - }, [previousName, previousSymbol, defaultName, defaultSymbol]); - - const trimmedName = nameInput.trim(); - const trimmedSymbol = symbolInput.trim(); - - const metadataChanged = useMemo(() => { - const hasNewName = trimmedName !== previousName; - const hasNewSymbol = trimmedSymbol !== previousSymbol; - return hasNewName || hasNewSymbol; - }, [previousName, previousSymbol, trimmedName, trimmedSymbol]); - - // Clear error when inputs change - useEffect(() => { - if (metadataError && metadataChanged) { - setMetadataError(null); - } - }, [metadataChanged, metadataError]); - - const handleMetadataSubmit = useCallback(async () => { - if (!metadataChanged) { - setMetadataError('No changes detected.'); - return; - } - - setMetadataError(null); - - // Switch network if needed - if (needSwitchChain) { - switchToNetwork(); - return; - } - - const success = await updateNameAndSymbol({ - name: trimmedName !== previousName ? (trimmedName ?? undefined) : undefined, - symbol: trimmedSymbol !== previousSymbol ? (trimmedSymbol ?? undefined) : undefined, - }); - - if (success) { - setMetadataError(null); - } - }, [metadataChanged, updateNameAndSymbol, previousName, previousSymbol, trimmedName, trimmedSymbol, needSwitchChain, switchToNetwork]); + const currentName = name !== '' ? name : defaultName; + const currentSymbol = symbol !== '' ? symbol : defaultSymbol; return (
-
- - setNameInput(event.target.value)} - placeholder={defaultName} - disabled={!isOwner} - id={nameInputId} - classNames={{ - input: 'text-sm', - inputWrapper: 'bg-hovered/60 border-transparent shadow-none focus-within:border-transparent focus-within:bg-hovered/80', - }} - /> -
- -
- - setSymbolInput(event.target.value)} - placeholder={defaultSymbol} - maxLength={16} - disabled={!isOwner} - id={symbolInputId} - classNames={{ - input: 'text-sm', - inputWrapper: 'bg-hovered/60 border-transparent shadow-none focus-within:border-transparent focus-within:bg-hovered/80', - }} - /> +
+
+

Vault Metadata

+

Basic information about the vault.

+
+ {isOwner && ( + + )}
- {metadataError &&

{metadataError}

} +
+
+

Vault Name

+

{currentName}

+
- +
+

Vault Symbol

+

{currentSymbol}

+
+
); diff --git a/src/features/autovault/components/vault-detail/modals/vault-settings/panels/RolesPanel.tsx b/src/features/autovault/components/vault-detail/modals/vault-settings/panels/RolesPanel.tsx index 279c8663..9e6d50df 100644 --- a/src/features/autovault/components/vault-detail/modals/vault-settings/panels/RolesPanel.tsx +++ b/src/features/autovault/components/vault-detail/modals/vault-settings/panels/RolesPanel.tsx @@ -1,30 +1,28 @@ 'use client'; -import { useCallback, useState } from 'react'; -import Image from 'next/image'; import type { Address } from 'viem'; import { zeroAddress } from 'viem'; import { useConnection } from 'wagmi'; import { Button } from '@/components/ui/button'; -import { Spinner } from '@/components/ui/spinner'; -import { useMarketNetwork } from '@/hooks/useMarketNetwork'; import { useMorphoMarketV1Adapters } from '@/hooks/useMorphoMarketV1Adapters'; import { useVaultV2Data } from '@/hooks/useVaultV2Data'; import { useVaultV2 } from '@/hooks/useVaultV2'; import type { SupportedNetworks } from '@/utils/networks'; -import { v2AgentsBase, findAgent } from '@/utils/monarch-agent'; -import { RoleAddressItem } from '../../../settings/RoleAddressItem'; +import { getAgentLabel, getAgentIcon } from '../../../settings/agent-display'; +import { AddressIdentity } from '@/components/shared/address-identity'; +import type { VaultDetailView } from '@/stores/vault-settings-modal-store'; type RolesPanelProps = { vaultAddress: Address; chainId: SupportedNetworks; + onNavigateToDetail?: (view: Exclude) => void; }; -export function RolesPanel({ vaultAddress, chainId }: RolesPanelProps) { +export function RolesPanel({ vaultAddress, chainId, onNavigateToDetail }: RolesPanelProps) { const { address: connectedAddress } = useConnection(); const { data: vaultData } = useVaultV2Data({ vaultAddress, chainId }); - const { isOwner, setAllocator, isUpdatingAllocator } = useVaultV2({ + const { isOwner } = useVaultV2({ vaultAddress, chainId, connectedAddress, @@ -36,70 +34,9 @@ export function RolesPanel({ vaultAddress, chainId }: RolesPanelProps) { const allocators = vaultData?.allocators ?? []; const adapters = vaultData?.adapters ?? []; - const [allocatorToAdd, setAllocatorToAdd] = useState
(null); - const [allocatorToRemove, setAllocatorToRemove] = useState
(null); - const [isEditingAllocators, setIsEditingAllocators] = useState(false); - - const { needSwitchChain, switchToNetwork } = useMarketNetwork({ - targetChainId: chainId, - }); - - const handleAddAllocator = useCallback( - async (allocator: Address) => { - if (needSwitchChain) { - switchToNetwork(); - return; - } - setAllocatorToAdd(allocator); - try { - await setAllocator(allocator, true); - } finally { - setAllocatorToAdd(null); - } - }, - [setAllocator, needSwitchChain, switchToNetwork], - ); - - const handleRemoveAllocator = useCallback( - async (allocator: Address) => { - if (needSwitchChain) { - switchToNetwork(); - return; - } - setAllocatorToRemove(allocator); - const success = await setAllocator(allocator, false); - if (success) { - setAllocatorToRemove(null); - } - }, - [setAllocator, needSwitchChain, switchToNetwork], - ); - const isMarketV1Adapter = (addr: string) => morphoMarketV1Adapter !== zeroAddress && addr.toLowerCase() === morphoMarketV1Adapter.toLowerCase(); - const currentAllocatorAddresses = allocators.map((a) => a.toLowerCase()); - const availableAllocators = v2AgentsBase.filter((agent) => !currentAllocatorAddresses.includes(agent.address.toLowerCase())); - - const getAgentLabel = (address: string) => { - const agent = findAgent(address); - return agent?.name; - }; - - const getAgentIcon = (address: string) => { - const agent = findAgent(address); - if (!agent?.image) return undefined; - return ( - {agent.name} - ); - }; - const renderRoleSection = ( label: string, description: string, @@ -119,7 +56,7 @@ export function RolesPanel({ vaultAddress, chainId }: RolesPanelProps) { ) : (
{addresses.map((addr) => ( - Allocators

Automation agents executing the configured strategy.

- {!isEditingAllocators && ( + {isOwner && ( )}
- {isEditingAllocators ? ( -
- {allocators.length > 0 && ( -
-

Current Allocators

- {allocators.map((address) => ( -
- - -
- ))} -
- )} - - {availableAllocators.length > 0 && ( -
-

{allocators.length > 0 ? 'Available to Add' : 'Select Allocator'}

- {availableAllocators.map((agent) => ( -
-
- - } - /> -

{agent.strategyDescription}

-
- -
- ))} -
- )} - -
- -
-
- ) : allocators.length === 0 ? ( + {allocators.length === 0 ? ( Not assigned ) : (
{allocators.map((address) => ( - {/* Header */} -
-
+
+

Allocation Caps

Caps limit how much of the vault can be allocated to each market. The effective cap shown is the smaller of the market cap and its collateral cap.

-
+
{isOwner && ( )}
@@ -158,6 +158,7 @@ export function CurrentCaps({ existingCaps, isOwner, onStartEdit, chainId, vault >
Promise; + onRemoveAllocator: (allocator: Address) => Promise; + onBack: () => void; +}; + +export function EditAllocators({ + allocators, + chainId, + isOwner, + isUpdating, + onAddAllocator, + onRemoveAllocator, + onBack, +}: EditAllocatorsProps) { + const [targetAllocator, setTargetAllocator] = useState
(null); + + const { needSwitchChain, switchToNetwork } = useMarketNetwork({ + targetChainId: chainId, + }); + + const handleAllocatorAction = useCallback( + async (allocator: Address, action: (a: Address) => Promise) => { + if (needSwitchChain) { + switchToNetwork(); + return; + } + setTargetAllocator(allocator); + try { + await action(allocator); + } finally { + setTargetAllocator(null); + } + }, + [needSwitchChain, switchToNetwork], + ); + + const availableAllocators = useMemo(() => { + const currentAddresses = new Set(allocators.map((a) => a.toLowerCase())); + return v2AgentsBase.filter((agent) => !currentAddresses.has(agent.address.toLowerCase())); + }, [allocators]); + + const isTargetLoading = (address: string) => isUpdating && targetAllocator === address; + + return ( +
+
+
+

Edit Allocators

+

Manage automation agents executing the configured strategy.

+
+
+ +
+ {allocators.length > 0 && ( +
+

Current Allocators

+ {allocators.map((address) => ( +
+ + +
+ ))} +
+ )} + + {availableAllocators.length > 0 && ( +
+

{allocators.length > 0 ? 'Available to Add' : 'Select Allocator'}

+ {availableAllocators.map((agent) => ( +
+
+ +

{agent.strategyDescription}

+
+ +
+ ))} +
+ )} + +
+ +
+
+
+ ); +} diff --git a/src/features/autovault/components/vault-detail/settings/EditCaps.tsx b/src/features/autovault/components/vault-detail/settings/EditCaps.tsx index ab4676ce..55bb79dc 100644 --- a/src/features/autovault/components/vault-detail/settings/EditCaps.tsx +++ b/src/features/autovault/components/vault-detail/settings/EditCaps.tsx @@ -22,7 +22,7 @@ type EditCapsProps = { isOwner: boolean; isUpdating: boolean; adapterAddress?: Address; - onCancel: () => void; + onBack: () => void; onSave: (caps: VaultV2Cap[]) => Promise; }; @@ -33,7 +33,7 @@ type MarketCapInfo = { existingCapId?: string; }; -export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdating, adapterAddress, onCancel, onSave }: EditCapsProps) { +export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdating, adapterAddress, onBack, onSave }: EditCapsProps) { const [marketCaps, setMarketCaps] = useState>(new Map()); const [removedMarketIds, setRemovedMarketIds] = useState>(new Set()); const [showAddMarketModal, setShowAddMarketModal] = useState(false); @@ -166,8 +166,8 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin const handleCancel = useCallback(() => { hasUserEditsRef.current = false; - onCancel(); - }, [onCancel]); + onBack(); + }, [onBack]); const hasChanges = useMemo(() => { const hasNewMarkets = Array.from(marketCaps.values()).some((m) => !m.existingCapId); diff --git a/src/features/autovault/components/vault-detail/settings/EditMetadata.tsx b/src/features/autovault/components/vault-detail/settings/EditMetadata.tsx new file mode 100644 index 00000000..a3cb197f --- /dev/null +++ b/src/features/autovault/components/vault-detail/settings/EditMetadata.tsx @@ -0,0 +1,163 @@ +'use client'; + +import { useCallback, useEffect, useId, useState } from 'react'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { Spinner } from '@/components/ui/spinner'; +import { useMarketNetwork } from '@/hooks/useMarketNetwork'; +import type { SupportedNetworks } from '@/utils/networks'; + +type EditMetadataProps = { + chainId: SupportedNetworks; + isOwner: boolean; + isUpdating: boolean; + defaultName: string; + defaultSymbol: string; + currentName: string; + currentSymbol: string; + onUpdate: (name?: string, symbol?: string) => Promise; + onBack: () => void; +}; + +export function EditMetadata({ + chainId, + isOwner, + isUpdating, + defaultName, + defaultSymbol, + currentName, + currentSymbol, + onUpdate, + onBack, +}: EditMetadataProps) { + const nameInputId = useId(); + const symbolInputId = useId(); + + const previousName = currentName.trim(); + const previousSymbol = currentSymbol.trim(); + + const [nameInput, setNameInput] = useState(previousName !== '' ? previousName : defaultName); + const [symbolInput, setSymbolInput] = useState(previousSymbol !== '' ? previousSymbol : defaultSymbol); + const [metadataError, setMetadataError] = useState(null); + + const { needSwitchChain, switchToNetwork } = useMarketNetwork({ + targetChainId: chainId, + }); + + // Reset inputs when current values change + useEffect(() => { + setNameInput(previousName !== '' ? previousName : defaultName); + setSymbolInput(previousSymbol !== '' ? previousSymbol : defaultSymbol); + }, [previousName, previousSymbol, defaultName, defaultSymbol]); + + const trimmedName = nameInput.trim(); + const trimmedSymbol = symbolInput.trim(); + const metadataChanged = trimmedName !== previousName || trimmedSymbol !== previousSymbol; + + // Clear error when inputs change + useEffect(() => { + if (metadataError && metadataChanged) { + setMetadataError(null); + } + }, [metadataChanged, metadataError]); + + const handleMetadataSubmit = useCallback(async () => { + if (!metadataChanged) { + setMetadataError('No changes detected.'); + return; + } + + setMetadataError(null); + + if (needSwitchChain) { + switchToNetwork(); + return; + } + + await onUpdate(trimmedName !== previousName ? trimmedName : undefined, trimmedSymbol !== previousSymbol ? trimmedSymbol : undefined); + }, [metadataChanged, onUpdate, previousName, previousSymbol, trimmedName, trimmedSymbol, needSwitchChain, switchToNetwork]); + + return ( +
+
+
+

Edit Metadata

+

Update the name and symbol of the vault.

+
+
+ +
+
+ + setNameInput(event.target.value)} + placeholder={defaultName} + disabled={!isOwner} + id={nameInputId} + classNames={{ + input: 'text-sm', + inputWrapper: 'bg-hovered/60 border-transparent shadow-none focus-within:border-transparent focus-within:bg-hovered/80', + }} + /> +
+ +
+ + setSymbolInput(event.target.value)} + placeholder={defaultSymbol} + maxLength={16} + disabled={!isOwner} + id={symbolInputId} + classNames={{ + input: 'text-sm', + inputWrapper: 'bg-hovered/60 border-transparent shadow-none focus-within:border-transparent focus-within:bg-hovered/80', + }} + /> +
+ + {metadataError &&

{metadataError}

} + +
+ + +
+
+
+ ); +} diff --git a/src/features/autovault/components/vault-detail/settings/RoleAddressItem.tsx b/src/features/autovault/components/vault-detail/settings/RoleAddressItem.tsx deleted file mode 100644 index 6db2b35a..00000000 --- a/src/features/autovault/components/vault-detail/settings/RoleAddressItem.tsx +++ /dev/null @@ -1,41 +0,0 @@ -'use client'; - -import type { ReactNode } from 'react'; -import Link from 'next/link'; -import { ExternalLinkIcon } from '@radix-ui/react-icons'; -import { Avatar } from '@/components/Avatar/Avatar'; -import { getExplorerURL } from '@/utils/external'; - -type RoleAddressItemProps = { - address: string; - chainId: number; - label?: string; - icon?: ReactNode; -}; - -/** - * Displays an address with optional custom label and icon. - * Follows AddressIdentity style: icon + label + shortened address + external link - */ -export function RoleAddressItem({ address, chainId, label, icon }: RoleAddressItemProps) { - return ( - - {icon ?? ( - - )} - {label && {label}} - - {address.slice(0, 6)}...{address.slice(-4)} - - - - ); -} diff --git a/src/features/autovault/components/vault-detail/settings/agent-display.tsx b/src/features/autovault/components/vault-detail/settings/agent-display.tsx new file mode 100644 index 00000000..d3d16b69 --- /dev/null +++ b/src/features/autovault/components/vault-detail/settings/agent-display.tsx @@ -0,0 +1,21 @@ +import type { ReactNode } from 'react'; +import Image from 'next/image'; +import { findAgent } from '@/utils/monarch-agent'; + +export function getAgentLabel(address: string): string | undefined { + return findAgent(address)?.name; +} + +export function getAgentIcon(address: string): ReactNode | undefined { + const agent = findAgent(address); + if (!agent?.image) return undefined; + return ( + {agent.name} + ); +} diff --git a/src/features/autovault/components/vault-detail/settings/index.ts b/src/features/autovault/components/vault-detail/settings/index.ts index 1ff395c7..8509a936 100644 --- a/src/features/autovault/components/vault-detail/settings/index.ts +++ b/src/features/autovault/components/vault-detail/settings/index.ts @@ -1,4 +1,3 @@ export { CurrentCaps } from './CurrentCaps'; export { EditCaps } from './EditCaps'; -export { RoleAddressItem } from './RoleAddressItem'; export * from './types'; diff --git a/src/features/autovault/vault-list-view.tsx b/src/features/autovault/vault-list-view.tsx index 83f4aae2..253c2959 100644 --- a/src/features/autovault/vault-list-view.tsx +++ b/src/features/autovault/vault-list-view.tsx @@ -228,7 +228,12 @@ export default function AutovaultListContent() { key={`${vault.networkId}-${vault.address}`} onClick={() => handleManageVault(vault.address, vault.networkId)} className="cursor-pointer" - startContent={} + startContent={ + + } > {vault.address.slice(0, 6)} diff --git a/src/hooks/useVaultV2.ts b/src/hooks/useVaultV2.ts index 1645dfe9..e14b89d3 100644 --- a/src/hooks/useVaultV2.ts +++ b/src/hooks/useVaultV2.ts @@ -1,5 +1,5 @@ import { useCallback, useMemo } from 'react'; -import { type Address, encodeFunctionData, zeroAddress, toFunctionSelector } from 'viem'; +import { type Address, encodeFunctionData, zeroAddress } from 'viem'; import { useQueryClient } from '@tanstack/react-query'; import { useConnection, useChainId, useReadContracts } from 'wagmi'; import { vaultv2Abi } from '@/abis/vaultv2'; @@ -112,6 +112,7 @@ export function useVaultV2({ pendingDescription: 'Applying new name and symbol', successDescription: 'Vault metadata saved', chainId: chainIdToUse, + onSuccess: onTransactionSuccess, }); const { isConfirming: isUpdatingAllocator, sendTransactionAsync: sendAllocatorTx } = useTransactionWithToast({ @@ -207,21 +208,21 @@ export function useVaultV2({ // Note: do not do this for maximized flexibility for now: open in the future! // Step 5. Abdicate registry control. - const setAdapterRegistrySelector = toFunctionSelector('setAdapterRegistry(address)'); + // const setAdapterRegistrySelector = toFunctionSelector('setAdapterRegistry(address)'); - const abdicateSetAdapterRegistryTx = encodeFunctionData({ - abi: vaultv2Abi, - functionName: 'abdicate', - args: [setAdapterRegistrySelector], - }); + // const abdicateSetAdapterRegistryTx = encodeFunctionData({ + // abi: vaultv2Abi, + // functionName: 'abdicate', + // args: [setAdapterRegistrySelector], + // }); - const submitAbdicateSetAdapterRegistryTx = encodeFunctionData({ - abi: vaultv2Abi, - functionName: 'submit', - args: [abdicateSetAdapterRegistryTx], - }); + // const submitAbdicateSetAdapterRegistryTx = encodeFunctionData({ + // abi: vaultv2Abi, + // functionName: 'submit', + // args: [abdicateSetAdapterRegistryTx], + // }); - txs.push(submitAbdicateSetAdapterRegistryTx, abdicateSetAdapterRegistryTx); + // txs.push(submitAbdicateSetAdapterRegistryTx, abdicateSetAdapterRegistryTx); // Step 6.1 Set user as allocator (for withdrawal / setting Withdrawal Data) const setSelfAllocatorTx = encodeFunctionData({ diff --git a/src/hooks/useVaultV2Data.ts b/src/hooks/useVaultV2Data.ts index 01cc54b8..9fc545f6 100644 --- a/src/hooks/useVaultV2Data.ts +++ b/src/hooks/useVaultV2Data.ts @@ -110,12 +110,7 @@ export function useVaultV2Data({ vaultAddress, chainId, fallbackName = '', fallb args: [addr as Address], })); - const contracts = [ - ...basicContracts, - ...allocatorContracts, - ...capContracts, - ...adapterContracts, - ]; + const contracts = [...basicContracts, ...allocatorContracts, ...capContracts, ...adapterContracts]; const results = await client.multicall({ contracts, diff --git a/src/stores/useVaultKeysCache.ts b/src/stores/useVaultKeysCache.ts index 776f3341..2ff44147 100644 --- a/src/stores/useVaultKeysCache.ts +++ b/src/stores/useVaultKeysCache.ts @@ -136,23 +136,31 @@ export function useVaultKeysCache(vaultAddress: string | undefined, chainId: num return { addAllocators: useCallback( - (addresses: string[]) => { if (vaultKey) store.addAllocators(vaultKey, addresses); }, + (addresses: string[]) => { + if (vaultKey) store.addAllocators(vaultKey, addresses); + }, [store.addAllocators, vaultKey], ), addCaps: useCallback( - (caps: CachedCap[]) => { if (vaultKey) store.addCaps(vaultKey, caps); }, + (caps: CachedCap[]) => { + if (vaultKey) store.addCaps(vaultKey, caps); + }, [store.addCaps, vaultKey], ), addAdapters: useCallback( - (addresses: string[]) => { if (vaultKey) store.addAdapters(vaultKey, addresses); }, + (addresses: string[]) => { + if (vaultKey) store.addAdapters(vaultKey, addresses); + }, [store.addAdapters, vaultKey], ), getVaultKeys: useCallback( - (): VaultKeysEntry => vaultKey ? store.getVaultKeys(vaultKey) : emptyEntry(), + (): VaultKeysEntry => (vaultKey ? store.getVaultKeys(vaultKey) : emptyEntry()), [store.getVaultKeys, vaultKey], ), seedFromApi: useCallback( - (entry: Partial) => { if (vaultKey) store.seedFromApi(vaultKey, entry); }, + (entry: Partial) => { + if (vaultKey) store.seedFromApi(vaultKey, entry); + }, [store.seedFromApi, vaultKey], ), vaultKey, diff --git a/src/stores/vault-settings-modal-store.ts b/src/stores/vault-settings-modal-store.ts index ccd981ec..263ae56f 100644 --- a/src/stores/vault-settings-modal-store.ts +++ b/src/stores/vault-settings-modal-store.ts @@ -1,7 +1,7 @@ import { create } from 'zustand'; export type VaultSettingsCategory = 'general' | 'roles' | 'caps'; -export type VaultDetailView = 'edit-caps' | null; +export type VaultDetailView = 'edit-caps' | 'edit-allocators' | 'edit-metadata' | null; type VaultSettingsModalState = { isOpen: boolean;