From de0c9450eb824c7c48eb7fe2308d7fb006e7c2c8 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Mon, 26 Jan 2026 14:59:05 +0800 Subject: [PATCH 1/4] feat: vault edit flow --- .../vault-settings/VaultSettingsContent.tsx | 4 +- .../modals/vault-settings/constants.ts | 2 + .../details/EditAllocatorsDetail.tsx | 41 ++++ .../details/EditMetadataDetail.tsx | 43 ++++ .../modals/vault-settings/details/index.ts | 2 + .../vault-settings/panels/GeneralPanel.tsx | 158 +++----------- .../vault-settings/panels/RolesPanel.tsx | 157 +------------- .../vault-detail/settings/CurrentCaps.tsx | 8 +- .../vault-detail/settings/EditAllocators.tsx | 197 ++++++++++++++++++ .../vault-detail/settings/EditMetadata.tsx | 180 ++++++++++++++++ src/stores/vault-settings-modal-store.ts | 2 +- 11 files changed, 514 insertions(+), 280 deletions(-) create mode 100644 src/features/autovault/components/vault-detail/modals/vault-settings/details/EditAllocatorsDetail.tsx create mode 100644 src/features/autovault/components/vault-detail/modals/vault-settings/details/EditMetadataDetail.tsx create mode 100644 src/features/autovault/components/vault-detail/settings/EditAllocators.tsx create mode 100644 src/features/autovault/components/vault-detail/settings/EditMetadata.tsx 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/constants.ts b/src/features/autovault/components/vault-detail/modals/vault-settings/constants.ts index ee7be5f4..30fcd2d5 100644 --- a/src/features/autovault/components/vault-detail/modals/vault-settings/constants.ts +++ b/src/features/autovault/components/vault-detail/modals/vault-settings/constants.ts @@ -17,4 +17,6 @@ export const VAULT_SETTINGS_CATEGORIES: CategoryConfig[] = [ export const VAULT_DETAIL_TITLES: Record, 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/EditMetadataDetail.tsx b/src/features/autovault/components/vault-detail/modals/vault-settings/details/EditMetadataDetail.tsx new file mode 100644 index 00000000..cb2ef88b --- /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, symbol })} + onCancel={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..c5f6b7cd 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'; \ No newline at end of file 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..4b1ffad3 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,134 +27,40 @@ 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 || defaultName; + const currentSymbol = 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', - }} - /> +
+
+

Vault Metadata

+

Basic information about the vault.

+
+ {isOwner && ( + + )}
-
- - 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 Name

+

{currentName}

+
+ +
+

Vault Symbol

+

{currentSymbol}

+
- - {metadataError &&

{metadataError}

} - -
); -} +} \ No newline at end of file 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..52cbafd0 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,29 @@ '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 { findAgent } from '@/utils/monarch-agent'; import { RoleAddressItem } from '../../../settings/RoleAddressItem'; +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,51 +35,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; @@ -147,112 +104,18 @@ export function RolesPanel({ vaultAddress, chainId }: RolesPanelProps) {

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 ) : (
@@ -275,4 +138,4 @@ export function RolesPanel({ vaultAddress, chainId }: RolesPanelProps) { })}
); -} +} \ No newline at end of file diff --git a/src/features/autovault/components/vault-detail/settings/CurrentCaps.tsx b/src/features/autovault/components/vault-detail/settings/CurrentCaps.tsx index b6a9bd88..b123f933 100644 --- a/src/features/autovault/components/vault-detail/settings/CurrentCaps.tsx +++ b/src/features/autovault/components/vault-detail/settings/CurrentCaps.tsx @@ -124,22 +124,22 @@ export function CurrentCaps({ existingCaps, isOwner, onStartEdit, chainId, vault return (
{/* 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 && ( )}
diff --git a/src/features/autovault/components/vault-detail/settings/EditAllocators.tsx b/src/features/autovault/components/vault-detail/settings/EditAllocators.tsx new file mode 100644 index 00000000..75c228ef --- /dev/null +++ b/src/features/autovault/components/vault-detail/settings/EditAllocators.tsx @@ -0,0 +1,197 @@ +'use client'; + +import { useCallback, useState } from 'react'; +import Image from 'next/image'; +import type { Address } from 'viem'; +import { Button } from '@/components/ui/button'; +import { Spinner } from '@/components/ui/spinner'; +import { useMarketNetwork } from '@/hooks/useMarketNetwork'; +import type { SupportedNetworks } from '@/utils/networks'; +import { v2AgentsBase, findAgent } from '@/utils/monarch-agent'; +import { RoleAddressItem } from './RoleAddressItem'; + +type EditAllocatorsProps = { + allocators: Address[]; + chainId: SupportedNetworks; + isOwner: boolean; + isUpdating: boolean; + onAddAllocator: (allocator: Address) => 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 handleAddAllocator = useCallback( + async (allocator: Address) => { + if (needSwitchChain) { + switchToNetwork(); + return; + } + setTargetAllocator(allocator); + try { + await onAddAllocator(allocator); + } finally { + setTargetAllocator(null); + } + }, + [onAddAllocator, needSwitchChain, switchToNetwork], + ); + + const handleRemoveAllocator = useCallback( + async (allocator: Address) => { + if (needSwitchChain) { + switchToNetwork(); + return; + } + setTargetAllocator(allocator); + try { + await onRemoveAllocator(allocator); + } finally { + setTargetAllocator(null); + } + }, + [onRemoveAllocator, needSwitchChain, switchToNetwork], + ); + + 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} + ); + }; + + 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/EditMetadata.tsx b/src/features/autovault/components/vault-detail/settings/EditMetadata.tsx new file mode 100644 index 00000000..95da1d87 --- /dev/null +++ b/src/features/autovault/components/vault-detail/settings/EditMetadata.tsx @@ -0,0 +1,180 @@ +'use client'; + +import { useCallback, useEffect, useId, useMemo, useState } from 'react'; +import type { Address } from 'viem'; +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; + onCancel: () => void; +}; + +export function EditMetadata({ + chainId, + isOwner, + isUpdating, + defaultName, + defaultSymbol, + currentName, + currentSymbol, + onUpdate, + onCancel, +}: EditMetadataProps) { + 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 onUpdate( + trimmedName !== previousName ? (trimmedName ?? undefined) : undefined, + trimmedSymbol !== previousSymbol ? (trimmedSymbol ?? undefined) : undefined, + ); + + if (success) { + setMetadataError(null); + } + }, [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/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; From 9e815d89d3780f924f4e585a5d709d6e4af9eeb7 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Mon, 26 Jan 2026 15:17:03 +0800 Subject: [PATCH 2/4] chore: review fixes --- .../vault-settings/VaultSettingsModal.tsx | 10 +-- .../vault-settings/details/EditCapsDetail.tsx | 2 +- .../details/EditMetadataDetail.tsx | 4 +- .../modals/vault-settings/details/index.ts | 2 +- .../vault-settings/panels/GeneralPanel.tsx | 8 +- .../vault-settings/panels/RolesPanel.tsx | 24 +---- .../vault-detail/settings/CurrentCaps.tsx | 1 + .../vault-detail/settings/EditAllocators.tsx | 83 +++++------------- .../vault-detail/settings/EditCaps.tsx | 8 +- .../vault-detail/settings/EditMetadata.tsx | 87 ++++++++----------- .../vault-detail/settings/agent-display.tsx | 21 +++++ src/features/autovault/vault-list-view.tsx | 7 +- src/hooks/useVaultV2.ts | 1 + src/hooks/useVaultV2Data.ts | 7 +- src/stores/useVaultKeysCache.ts | 18 ++-- 15 files changed, 114 insertions(+), 169 deletions(-) create mode 100644 src/features/autovault/components/vault-detail/settings/agent-display.tsx 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} /> { 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 index cb2ef88b..da2fb549 100644 --- 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 @@ -36,8 +36,8 @@ export function EditMetadataDetail({ vaultAddress, chainId, onBack }: EditMetada defaultSymbol={defaultSymbol} currentName={name} currentSymbol={symbol} - onUpdate={(name, symbol) => updateNameAndSymbol({ name, symbol })} - onCancel={onBack} + onUpdate={(newName, newSymbol) => 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 c5f6b7cd..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,3 +1,3 @@ export { EditCapsDetail } from './EditCapsDetail'; export { EditAllocatorsDetail } from './EditAllocatorsDetail'; -export { EditMetadataDetail } from './EditMetadataDetail'; \ No newline at end of file +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 4b1ffad3..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 @@ -27,8 +27,8 @@ export function GeneralPanel({ vaultAddress, chainId, onNavigateToDetail }: Gene const defaultName = vaultData?.displayName ?? ''; const defaultSymbol = vaultData?.displaySymbol ?? ''; - const currentName = name || defaultName; - const currentSymbol = symbol || defaultSymbol; + const currentName = name !== '' ? name : defaultName; + const currentSymbol = symbol !== '' ? symbol : defaultSymbol; return (
@@ -54,7 +54,7 @@ export function GeneralPanel({ vaultAddress, chainId, onNavigateToDetail }: Gene

Vault Name

{currentName}

- +

Vault Symbol

{currentSymbol}

@@ -63,4 +63,4 @@ export function GeneralPanel({ vaultAddress, chainId, onNavigateToDetail }: Gene
); -} \ No newline at end of file +} 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 52cbafd0..4814191e 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,6 +1,5 @@ 'use client'; -import Image from 'next/image'; import type { Address } from 'viem'; import { zeroAddress } from 'viem'; import { useConnection } from 'wagmi'; @@ -9,7 +8,7 @@ import { useMorphoMarketV1Adapters } from '@/hooks/useMorphoMarketV1Adapters'; import { useVaultV2Data } from '@/hooks/useVaultV2Data'; import { useVaultV2 } from '@/hooks/useVaultV2'; import type { SupportedNetworks } from '@/utils/networks'; -import { findAgent } from '@/utils/monarch-agent'; +import { getAgentLabel, getAgentIcon } from '../../../settings/agent-display'; import { RoleAddressItem } from '../../../settings/RoleAddressItem'; import type { VaultDetailView } from '@/stores/vault-settings-modal-store'; @@ -38,25 +37,6 @@ export function RolesPanel({ vaultAddress, chainId, onNavigateToDetail }: RolesP const isMarketV1Adapter = (addr: string) => morphoMarketV1Adapter !== zeroAddress && addr.toLowerCase() === morphoMarketV1Adapter.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, @@ -138,4 +118,4 @@ export function RolesPanel({ vaultAddress, chainId, onNavigateToDetail }: RolesP })}
); -} \ No newline at end of file +} diff --git a/src/features/autovault/components/vault-detail/settings/CurrentCaps.tsx b/src/features/autovault/components/vault-detail/settings/CurrentCaps.tsx index b123f933..c31fd0ce 100644 --- a/src/features/autovault/components/vault-detail/settings/CurrentCaps.tsx +++ b/src/features/autovault/components/vault-detail/settings/CurrentCaps.tsx @@ -158,6 +158,7 @@ export function CurrentCaps({ existingCaps, isOwner, onStartEdit, chainId, vault >
{ + const handleAllocatorAction = useCallback( + async (allocator: Address, action: (a: Address) => Promise) => { if (needSwitchChain) { switchToNetwork(); return; } setTargetAllocator(allocator); try { - await onAddAllocator(allocator); + await action(allocator); } finally { setTargetAllocator(null); } }, - [onAddAllocator, needSwitchChain, switchToNetwork], + [needSwitchChain, switchToNetwork], ); - const handleRemoveAllocator = useCallback( - async (allocator: Address) => { - if (needSwitchChain) { - switchToNetwork(); - return; - } - setTargetAllocator(allocator); - try { - await onRemoveAllocator(allocator); - } finally { - setTargetAllocator(null); - } - }, - [onRemoveAllocator, needSwitchChain, switchToNetwork], - ); - - 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 availableAllocators = useMemo(() => { + const currentAddresses = new Set(allocators.map((a) => a.toLowerCase())); + return v2AgentsBase.filter((agent) => !currentAddresses.has(agent.address.toLowerCase())); + }, [allocators]); - const getAgentIcon = (address: string) => { - const agent = findAgent(address); - if (!agent?.image) return undefined; - return ( - {agent.name} - ); - }; + const isTargetLoading = (address: string) => isUpdating && targetAllocator === address; return (
@@ -116,10 +85,10 @@ export function EditAllocators({
)} -
+
- -
+
+ +
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/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..90bb0a18 100644 --- a/src/hooks/useVaultV2.ts +++ b/src/hooks/useVaultV2.ts @@ -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({ 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, From 953825bdafe36fb9cb46e94b29ea23b49cd1f789 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Mon, 26 Jan 2026 15:32:13 +0800 Subject: [PATCH 3/4] refactor: role item --- src/components/shared/address-identity.tsx | 24 +++++------ .../vault-settings/panels/RolesPanel.tsx | 6 +-- .../vault-detail/settings/EditAllocators.tsx | 6 +-- .../vault-detail/settings/RoleAddressItem.tsx | 41 ------------------- .../components/vault-detail/settings/index.ts | 1 - 5 files changed, 17 insertions(+), 61 deletions(-) delete mode 100644 src/features/autovault/components/vault-detail/settings/RoleAddressItem.tsx 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/panels/RolesPanel.tsx b/src/features/autovault/components/vault-detail/modals/vault-settings/panels/RolesPanel.tsx index 4814191e..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 @@ -9,7 +9,7 @@ import { useVaultV2Data } from '@/hooks/useVaultV2Data'; import { useVaultV2 } from '@/hooks/useVaultV2'; import type { SupportedNetworks } from '@/utils/networks'; import { getAgentLabel, getAgentIcon } from '../../../settings/agent-display'; -import { RoleAddressItem } from '../../../settings/RoleAddressItem'; +import { AddressIdentity } from '@/components/shared/address-identity'; import type { VaultDetailView } from '@/stores/vault-settings-modal-store'; type RolesPanelProps = { @@ -56,7 +56,7 @@ export function RolesPanel({ vaultAddress, chainId, onNavigateToDetail }: RolesP ) : (
{addresses.map((addr) => ( - {allocators.map((address) => ( - -
- - {icon ?? ( - - )} - {label && {label}} - - {address.slice(0, 6)}...{address.slice(-4)} - - - - ); -} 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'; From 862397271920f553decd693662263a4506289b5e Mon Sep 17 00:00:00 2001 From: antoncoding Date: Mon, 26 Jan 2026 15:33:52 +0800 Subject: [PATCH 4/4] chore: do not abdicate --- src/hooks/useVaultV2.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/hooks/useVaultV2.ts b/src/hooks/useVaultV2.ts index 90bb0a18..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'; @@ -208,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({