diff --git a/src/data-sources/morpho-api/v2-vaults.ts b/src/data-sources/morpho-api/v2-vaults.ts index bef75cb9..102f7512 100644 --- a/src/data-sources/morpho-api/v2-vaults.ts +++ b/src/data-sources/morpho-api/v2-vaults.ts @@ -60,6 +60,11 @@ type ApiVaultV2 = { address: string; }; }[]; + adapters: { + items: { + address: string; + }[]; + }; caps: { items: ApiVaultV2Cap[]; }; @@ -99,7 +104,7 @@ function transformVault(apiVault: ApiVaultV2): VaultV2Details { allocators: apiVault.allocators.map((a) => a.allocator.address), sentinels: [], // Not available in API response caps: apiVault.caps.items.map(transformCap), - adapters: [], // Not available in API response + adapters: apiVault.adapters.items.map((a) => a.address), avgApy: apiVault.avgApy, }; } 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 0dabdbdf..8efd43e2 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 @@ -5,7 +5,7 @@ import type { Address } from 'viem'; import { slideVariants, slideTransition, type SlideDirection } from '@/components/common/settings-modal'; import type { SupportedNetworks } from '@/utils/networks'; import { VaultSettingsHeader } from './VaultSettingsHeader'; -import { GeneralPanel, AgentsPanel, CapsPanel } from './panels'; +import { GeneralPanel, RolesPanel, CapsPanel } from './panels'; import { EditCapsDetail } from './details'; import type { VaultSettingsCategory, VaultDetailView } from '@/stores/vault-settings-modal-store'; @@ -17,7 +17,7 @@ type PanelProps = { const PANEL_COMPONENTS: Record> = { general: GeneralPanel, - agents: AgentsPanel, + roles: RolesPanel, caps: CapsPanel, }; 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 66adba23..ee7be5f4 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 @@ -11,7 +11,7 @@ export type CategoryConfig = { export const VAULT_SETTINGS_CATEGORIES: CategoryConfig[] = [ { id: 'general', label: 'GENERAL', icon: FiSettings }, - { id: 'agents', label: 'AGENTS', icon: FiUsers }, + { id: 'roles', label: 'ROLES', icon: FiUsers }, { id: 'caps', label: 'CAPS', icon: MdFilterList }, ]; diff --git a/src/features/autovault/components/vault-detail/modals/vault-settings/panels/AgentsPanel.tsx b/src/features/autovault/components/vault-detail/modals/vault-settings/panels/RolesPanel.tsx similarity index 62% rename from src/features/autovault/components/vault-detail/modals/vault-settings/panels/AgentsPanel.tsx rename to src/features/autovault/components/vault-detail/modals/vault-settings/panels/RolesPanel.tsx index a154d45b..279c8663 100644 --- a/src/features/autovault/components/vault-detail/modals/vault-settings/panels/AgentsPanel.tsx +++ b/src/features/autovault/components/vault-detail/modals/vault-settings/panels/RolesPanel.tsx @@ -1,37 +1,41 @@ '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 { AccountIdentity } from '@/components/shared/account-identity'; 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 } from '@/utils/monarch-agent'; -import { AgentListItem } from '../../../settings/AgentListItem'; +import { v2AgentsBase, findAgent } from '@/utils/monarch-agent'; +import { RoleAddressItem } from '../../../settings/RoleAddressItem'; -type AgentsPanelProps = { +type RolesPanelProps = { vaultAddress: Address; chainId: SupportedNetworks; }; -export function AgentsPanel({ vaultAddress, chainId }: AgentsPanelProps) { +export function RolesPanel({ vaultAddress, chainId }: RolesPanelProps) { 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 { morphoMarketV1Adapter } = useMorphoMarketV1Adapters({ vaultAddress, chainId }); const owner = vaultData?.owner; const curator = vaultData?.curator; const allocators = vaultData?.allocators ?? []; + const adapters = vaultData?.adapters ?? []; + const [allocatorToAdd, setAllocatorToAdd] = useState
(null); const [allocatorToRemove, setAllocatorToRemove] = useState
(null); const [isEditingAllocators, setIsEditingAllocators] = useState(false); @@ -42,12 +46,10 @@ export function AgentsPanel({ vaultAddress, chainId }: AgentsPanelProps) { const handleAddAllocator = useCallback( async (allocator: Address) => { - // Switch network if needed if (needSwitchChain) { switchToNetwork(); return; } - setAllocatorToAdd(allocator); try { await setAllocator(allocator, true); @@ -60,12 +62,10 @@ export function AgentsPanel({ vaultAddress, chainId }: AgentsPanelProps) { const handleRemoveAllocator = useCallback( async (allocator: Address) => { - // Switch network if needed if (needSwitchChain) { switchToNetwork(); return; } - setAllocatorToRemove(allocator); const success = await setAllocator(allocator, false); if (success) { @@ -75,39 +75,73 @@ export function AgentsPanel({ vaultAddress, chainId }: AgentsPanelProps) { [setAllocator, needSwitchChain, switchToNetwork], ); - const renderSingleRole = (label: string, description: string, addressValue?: string) => { - const normalized = addressValue ? (addressValue as Address) : undefined; + 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 ( -
-
-

{label}

-

{description}

-
- {normalized ? ( - - ) : ( - Not assigned - )} -
+ {agent.name} ); }; - const currentAllocatorAddresses = allocators.map((a) => a.toLowerCase()); - const availableAllocators = v2AgentsBase.filter((agent) => !currentAllocatorAddresses.includes(agent.address.toLowerCase())); + const renderRoleSection = ( + label: string, + description: string, + addresses: string[], + options?: { + getLabelOverride?: (addr: string) => string | undefined; + getIconOverride?: (addr: string) => React.ReactNode | undefined; + }, + ) => ( +
+
+

{label}

+

{description}

+
+ {addresses.length === 0 ? ( + Not assigned + ) : ( +
+ {addresses.map((addr) => ( + + ))} +
+ )} +
+ ); return (
- {renderSingleRole('Owner', 'Primary controller of vault permissions.', owner)} - {renderSingleRole('Curator', 'Defines risk guardrails for automation.', curator)} + {/* Owner */} + {renderRoleSection('Owner', 'Primary controller of vault permissions.', owner ? [owner] : [])} -
+ {/* Curator */} + {renderRoleSection('Curator', 'Defines risk guardrails for automation.', curator ? [curator] : [])} + + {/* Allocators */} +

Allocators

@@ -120,13 +154,12 @@ export function AgentsPanel({ vaultAddress, chainId }: AgentsPanelProps) { onClick={() => setIsEditingAllocators(true)} disabled={!isOwner} > - {allocators.length === 0 ? 'Add allocators' : 'Edit'} + {allocators.length === 0 ? 'Add' : 'Edit'} )}
{isEditingAllocators ? ( - // Edit mode
{allocators.length > 0 && (
@@ -134,12 +167,13 @@ export function AgentsPanel({ vaultAddress, chainId }: AgentsPanelProps) { {allocators.map((address) => (
-
- ) : // Read-only view - allocators.length === 0 ? ( -

No allocators assigned

+ ) : allocators.length === 0 ? ( + Not assigned ) : ( -
+
{allocators.map((address) => ( -
- -
+ address={address} + chainId={chainId} + label={getAgentLabel(address)} + icon={getAgentIcon(address)} + /> ))}
)}
+ + {/* Adapters */} + {renderRoleSection('Adapters', 'Contracts enabling vault interactions with underlying protocols.', adapters, { + getLabelOverride: (addr) => (isMarketV1Adapter(addr) ? 'MorphoBlue Adapter' : undefined), + })}
); } diff --git a/src/features/autovault/components/vault-detail/modals/vault-settings/panels/index.ts b/src/features/autovault/components/vault-detail/modals/vault-settings/panels/index.ts index 987390b5..dfdcda87 100644 --- a/src/features/autovault/components/vault-detail/modals/vault-settings/panels/index.ts +++ b/src/features/autovault/components/vault-detail/modals/vault-settings/panels/index.ts @@ -1,3 +1,3 @@ export { GeneralPanel } from './GeneralPanel'; -export { AgentsPanel } from './AgentsPanel'; +export { RolesPanel } from './RolesPanel'; export { CapsPanel } from './CapsPanel'; diff --git a/src/features/autovault/components/vault-detail/settings/AgentListItem.tsx b/src/features/autovault/components/vault-detail/settings/AgentListItem.tsx deleted file mode 100644 index 398c37be..00000000 --- a/src/features/autovault/components/vault-detail/settings/AgentListItem.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import type { Address } from 'viem'; -import { Avatar } from '@/components/Avatar/Avatar'; -import { AgentIcon } from '@/components/shared/agent-icon'; -import { AccountIdentity } from '@/components/shared/account-identity'; -import { findAgent } from '@/utils/monarch-agent'; -import { SupportedNetworks } from '@/utils/networks'; - -type AgentListItemProps = { - address: Address; - chainId?: number; - ownerAddress?: Address; -}; - -export function AgentListItem({ address, chainId, ownerAddress }: AgentListItemProps) { - const agent = findAgent(address); - const isOwner = ownerAddress && address.toLowerCase() === ownerAddress.toLowerCase(); - - return ( -
- {isOwner ? ( - - ) : ( - - )} - {isOwner && Owner} - {agent && !isOwner && {agent.name}} - -
- ); -} diff --git a/src/features/autovault/components/vault-detail/settings/EditCaps.tsx b/src/features/autovault/components/vault-detail/settings/EditCaps.tsx index 828db345..954f1e60 100644 --- a/src/features/autovault/components/vault-detail/settings/EditCaps.tsx +++ b/src/features/autovault/components/vault-detail/settings/EditCaps.tsx @@ -411,9 +411,7 @@ export function EditCaps({ existingCaps, vaultAsset, chainId, isOwner, isUpdatin

Collateral Caps ({collateralCaps.size})

-

- Shared limit across all markets using the same collateral. -

+

Shared limit across all markets using the same collateral.

{/* Column Headers */} diff --git a/src/features/autovault/components/vault-detail/settings/RoleAddressItem.tsx b/src/features/autovault/components/vault-detail/settings/RoleAddressItem.tsx new file mode 100644 index 00000000..6db2b35a --- /dev/null +++ b/src/features/autovault/components/vault-detail/settings/RoleAddressItem.tsx @@ -0,0 +1,41 @@ +'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/index.ts b/src/features/autovault/components/vault-detail/settings/index.ts index 7a7c2dd1..1ff395c7 100644 --- a/src/features/autovault/components/vault-detail/settings/index.ts +++ b/src/features/autovault/components/vault-detail/settings/index.ts @@ -1,4 +1,4 @@ export { CurrentCaps } from './CurrentCaps'; export { EditCaps } from './EditCaps'; -export { AgentListItem } from './AgentListItem'; +export { RoleAddressItem } from './RoleAddressItem'; export * from './types'; diff --git a/src/features/autovault/vault-view.tsx b/src/features/autovault/vault-view.tsx index 676649f3..10b7c5b8 100644 --- a/src/features/autovault/vault-view.tsx +++ b/src/features/autovault/vault-view.tsx @@ -207,25 +207,23 @@ export default function VaultContent() { - {vaultContract.isOwner && ( - - } + + } + > + - - )} + + +
@@ -253,16 +251,16 @@ export default function VaultContent() { {isVaultInitialized && hasNoAllocators && vaultContract.isOwner && (
-

Choose an agent

-

Add an agent to enable automated allocation and rebalancing.

+

Choose an allocator

+

Add an allocator to enable automated allocation and rebalancing.

)} diff --git a/src/graphql/morpho-api-queries.ts b/src/graphql/morpho-api-queries.ts index 4af22a86..97f796c6 100644 --- a/src/graphql/morpho-api-queries.ts +++ b/src/graphql/morpho-api-queries.ts @@ -561,6 +561,11 @@ export const vaultV2Query = ` relativeCap } } + adapters { + items { + address + } + } } } `; diff --git a/src/stores/vault-settings-modal-store.ts b/src/stores/vault-settings-modal-store.ts index 2971bcd4..ccd981ec 100644 --- a/src/stores/vault-settings-modal-store.ts +++ b/src/stores/vault-settings-modal-store.ts @@ -1,6 +1,6 @@ import { create } from 'zustand'; -export type VaultSettingsCategory = 'general' | 'agents' | 'caps'; +export type VaultSettingsCategory = 'general' | 'roles' | 'caps'; export type VaultDetailView = 'edit-caps' | null; type VaultSettingsModalState = { @@ -51,9 +51,9 @@ type VaultSettingsModalStore = VaultSettingsModalState & VaultSettingsModalActio * * @example * ```tsx - * // Open settings modal on agents category + * // Open settings modal on roles category * const { open } = useVaultSettingsModalStore(); - * open('agents'); + * open('roles'); * * // Navigate to edit caps detail view * const { navigateToDetail } = useVaultSettingsModalStore();