Adapter cap (%)
@@ -141,14 +141,14 @@ function FinalizeSetupStep({
Adapter
{adapterIsReady ? (
-
+
) : (
Adapter not detected yet.
)}
- Only Morpho-approved adapters can be enabled after this step.
diff --git a/app/autovault/[chainId]/[vaultAddress]/components/settings/AgentListItem.tsx b/app/autovault/[chainId]/[vaultAddress]/components/settings/AgentListItem.tsx
index 0afd1ab0..862c2e8a 100644
--- a/app/autovault/[chainId]/[vaultAddress]/components/settings/AgentListItem.tsx
+++ b/app/autovault/[chainId]/[vaultAddress]/components/settings/AgentListItem.tsx
@@ -1,6 +1,6 @@
import { Address } from 'viem';
import { AgentIcon } from '@/components/AgentIcon';
-import { AddressDisplay } from '@/components/common/AddressDisplay';
+import { AccountIdentity } from '@/components/common/AccountIdentity';
import { findAgent } from '@/utils/monarch-agent';
type AgentListItemProps = {
@@ -14,7 +14,7 @@ export function AgentListItem({ address }: AgentListItemProps) {
{agent &&
{agent.name}}
-
+
);
}
diff --git a/app/autovault/[chainId]/[vaultAddress]/components/settings/AgentsTab.tsx b/app/autovault/[chainId]/[vaultAddress]/components/settings/AgentsTab.tsx
index 31e87a7b..84d4b778 100644
--- a/app/autovault/[chainId]/[vaultAddress]/components/settings/AgentsTab.tsx
+++ b/app/autovault/[chainId]/[vaultAddress]/components/settings/AgentsTab.tsx
@@ -1,6 +1,6 @@
import { useCallback, useState } from 'react';
import { Address } from 'viem';
-import { AddressDisplay } from '@/components/common/AddressDisplay';
+import { AccountIdentity } from '@/components/common/AccountIdentity';
import { Button } from '@/components/common/Button';
import { Spinner } from '@/components/common/Spinner';
import { useMarketNetwork } from '@/hooks/useMarketNetwork';
@@ -75,12 +75,7 @@ export function AgentsTab({
{description}
{normalized ? (
-
+
) : (
Not assigned
)}
@@ -235,11 +230,11 @@ export function AgentsTab({
) : (
{sentinels.map((address) => (
-
))}
diff --git a/app/autovault/[chainId]/[vaultAddress]/content.tsx b/app/autovault/[chainId]/[vaultAddress]/content.tsx
index 8b8c26bd..9e21aa74 100644
--- a/app/autovault/[chainId]/[vaultAddress]/content.tsx
+++ b/app/autovault/[chainId]/[vaultAddress]/content.tsx
@@ -9,7 +9,7 @@ import { IoRefreshOutline } from 'react-icons/io5';
import { Address } from 'viem';
import { useAccount } from 'wagmi';
import { Button } from '@/components/common';
-import { AddressDisplay } from '@/components/common/AddressDisplay';
+import { AccountIdentity } from '@/components/common/AccountIdentity';
import Header from '@/components/layout/header/Header';
import { useVaultPage } from '@/hooks/useVaultPage';
import { getSlicedAddress } from '@/utils/address';
@@ -146,7 +146,11 @@ export default function VaultContent() {
)}
-
+
-
+
diff --git a/docs/Styling.md b/docs/Styling.md
index 2f907eee..5b007b21 100644
--- a/docs/Styling.md
+++ b/docs/Styling.md
@@ -437,6 +437,98 @@ Add an action link (like explorer) in the top-right corner:
- Render token avatars with `TokenIcon` (`@/components/TokenIcon`) so chain-specific fallbacks, glyph sizing, and tooltips stay consistent.
- Display oracle provenance data with `OracleVendorBadge` (`@/components/OracleVendorBadge`) instead of plain text to benefit from vendor icons, warnings, and tooltips.
+### Account Identity Component
+
+**AccountIdentity** (`@/components/common/AccountIdentity`)
+- Unified component for displaying addresses, vault names, and ENS names
+- Three variants: `badge`, `compact`, `full`
+- All avatars are round by default
+
+**Variant Behaviors:**
+
+**Badge** - Minimal inline (no avatar)
+- Shows: Vault name → ENS name → Shortened address
+
+**Compact** - Avatar (16px) wrapped in badge
+- Avatar + (Vault name → ENS name → Shortened address)
+- Single badge wraps both avatar and text
+
+**Full** - Horizontal layout with all info
+- Avatar (36px) + Address badge + Extra badges (all on one line, centered)
+- **Address badge**: Always shows shortened address (e.g., 0x1234...5678), click to copy
+- **Extra badges** (shown based on conditions):
+ - Connected badge (if wallet is connected)
+ - ENS badge (if `showAddress=true` and no vault name)
+ - Vault badge (if address is a known vault)
+
+**Styling Rules:**
+- Use `rounded-sm` for badges (not `rounded`)
+- Background: `bg-hovered` (or `bg-green-500/10` for connected)
+- Text: `font-zen` with `text-secondary` or `text-primary`
+- No underscores in variable names
+- All avatars are round
+- Full variant: all elements centered vertically
+- Smooth Framer Motion animations on all interactions
+
+```tsx
+import { AccountIdentity } from '@/components/common/AccountIdentity';
+
+// Badge variant - minimal inline (no avatar)
+
+
+// Compact variant - avatar (16px) wrapped in badge background
+
+
+// Full variant - avatar + address + extra info badges
+
+
+// Full variant for vault address
+
+```
+
+**Props:**
+- `variant`: `'badge'` | `'compact'` | `'full'`
+- `linkTo`: `'explorer'` | `'profile'` | `'none'`
+- `showCopy`: Show copy icon at end of badge
+- `copyable`: Make entire component clickable to copy
+- `showAddress`: Show ENS badge (full variant only)
+- `showActions`: Show actions popover on click (default: `true`)
+
+**Actions Popover (Default Behavior):**
+
+By default, clicking any AccountIdentity shows a minimal popover with:
+1. **Copy Address** - Copies address to clipboard
+2. **View Account** - Navigate to positions page
+3. **View on Explorer** - Opens Etherscan in new tab
+
+To disable: `showActions={false}`
+
+```tsx
+// Default - shows actions popover on click
+
+
+// Disable actions (e.g., in dropdown menus)
+
+```
+
### Market Display Components
Use the right component for displaying market information:
diff --git a/src/components/Account/AccountWithAvatar.tsx b/src/components/Account/AccountWithAvatar.tsx
deleted file mode 100644
index e14e5b71..00000000
--- a/src/components/Account/AccountWithAvatar.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { Address } from 'viem';
-import { Avatar } from '@/components/Avatar/Avatar';
-import { getSlicedAddress } from '@/utils/address';
-
-type AccountWithAvatarProps = {
- address: Address;
-};
-
-function AccountWithSmallAvatar({ address }: AccountWithAvatarProps) {
- return (
-
-
-
- {getSlicedAddress(address as `0x${string}`)}
-
-
- );
-}
-
-export default AccountWithSmallAvatar;
diff --git a/src/components/Account/AccountWithENS.tsx b/src/components/Account/AccountWithENS.tsx
deleted file mode 100644
index 77ca856e..00000000
--- a/src/components/Account/AccountWithENS.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import { Address } from 'viem';
-import { Avatar } from '@/components/Avatar/Avatar';
-import { getSlicedAddress } from '@/utils/address';
-import { Name } from '../common/Name';
-
-type AccountWithENSProps = {
- address: Address;
-};
-
-function AccountWithENS({ address }: AccountWithENSProps) {
- return (
-
-
-
-
-
-
-
- {getSlicedAddress(address)}
-
-
-
- );
-}
-
-export default AccountWithENS;
diff --git a/src/components/common/AccountActionsPopover.tsx b/src/components/common/AccountActionsPopover.tsx
new file mode 100644
index 00000000..f18c1403
--- /dev/null
+++ b/src/components/common/AccountActionsPopover.tsx
@@ -0,0 +1,117 @@
+'use client';
+
+import { useState, useCallback, type ReactNode } from 'react';
+import { Popover, PopoverTrigger, PopoverContent } from '@heroui/react';
+import { motion, AnimatePresence } from 'framer-motion';
+import { LuCopy, LuUser } from 'react-icons/lu';
+import { SiEthereum } from 'react-icons/si';
+import { useStyledToast } from '@/hooks/useStyledToast';
+import { getExplorerURL } from '@/utils/external';
+import { SupportedNetworks } from '@/utils/networks';
+import type { Address } from 'viem';
+
+type AccountActionsPopoverProps = {
+ address: Address;
+ children: ReactNode;
+};
+
+/**
+ * Minimal popover showing account actions:
+ * - Copy address
+ * - View account (positions page)
+ * - View on Etherscan
+ */
+export function AccountActionsPopover({
+ address,
+ children,
+}: AccountActionsPopoverProps) {
+ const [isOpen, setIsOpen] = useState(false);
+ const toast = useStyledToast();
+
+ const handleCopy = useCallback(async () => {
+ try {
+ await navigator.clipboard.writeText(address);
+ toast.success('Address copied', `${address.slice(0, 6)}...${address.slice(-4)}`);
+ setIsOpen(false);
+ } catch (error) {
+ console.error('Failed to copy address', error);
+ }
+ }, [address, toast]);
+
+ const handleViewAccount = useCallback(() => {
+ window.location.href = `/positions/${address}`;
+ setIsOpen(false);
+ }, [address]);
+
+ const handleViewExplorer = useCallback(() => {
+ const explorerUrl = getExplorerURL(address, SupportedNetworks.Mainnet);
+ window.open(explorerUrl, '_blank', 'noopener,noreferrer');
+ setIsOpen(false);
+ }, [address]);
+
+ return (
+
+
+ {children}
+
+
+
+ {isOpen && (
+
+ {/* Copy Address */}
+ void handleCopy()}
+ className="flex items-center gap-3 px-4 py-2.5 text-sm text-secondary transition-colors hover:bg-hovered hover:text-primary"
+ whileHover={{ x: 2 }}
+ whileTap={{ scale: 0.98 }}
+ >
+
+ Copy Address
+
+
+ {/* View Account */}
+
+
+ View Account
+
+
+ {/* View on Explorer */}
+
+
+ View on Explorer
+
+
+ )}
+
+
+
+ );
+}
diff --git a/src/components/common/AccountIdentity.tsx b/src/components/common/AccountIdentity.tsx
new file mode 100644
index 00000000..f2f728ff
--- /dev/null
+++ b/src/components/common/AccountIdentity.tsx
@@ -0,0 +1,346 @@
+'use client';
+
+import { useMemo, useState, useEffect, useCallback } from 'react';
+import clsx from 'clsx';
+import { motion } from 'framer-motion';
+import Link from 'next/link';
+import { FaCircle } from 'react-icons/fa';
+import { LuExternalLink, LuCopy } from 'react-icons/lu';
+import { useAccount, useEnsName } from 'wagmi';
+import { Avatar } from '@/components/Avatar/Avatar';
+import { AccountActionsPopover } from '@/components/common/AccountActionsPopover';
+import { Name } from '@/components/common/Name';
+import { useAddressLabel } from '@/hooks/useAddressLabel';
+import { useStyledToast } from '@/hooks/useStyledToast';
+import { getExplorerURL } from '@/utils/external';
+import { SupportedNetworks } from '@/utils/networks';
+import type { Address } from 'viem';
+
+type AccountIdentityProps = {
+ address: Address;
+ variant?: 'badge' | 'compact' | 'full';
+ linkTo?: 'explorer' | 'profile' | 'none';
+ copyable?: boolean;
+ showCopy?: boolean;
+ showAddress?: boolean;
+ showActions?: boolean;
+ className?: string;
+};
+
+/**
+ * Unified component for displaying account identities across the app.
+ *
+ * Badge & Compact: Show vault name → ENS name → shortened address
+ * Full: Always show address badge + optional extra badges (connected, ENS, vault)
+ *
+ * Variants:
+ * - badge: Minimal inline badge (no avatar)
+ * - compact: Avatar (16px) wrapped in badge background
+ * - full: Avatar (36px) + address badge + extra badges (all centered on one line)
+ */
+export function AccountIdentity({
+ address,
+ variant = 'badge',
+ linkTo = 'none',
+ copyable = false,
+ showCopy = false,
+ showAddress = false,
+ showActions = true,
+ className,
+}: AccountIdentityProps) {
+ const { address: connectedAddress, isConnected } = useAccount();
+ const [mounted, setMounted] = useState(false);
+ const toast = useStyledToast();
+ const { vaultName, shortAddress } = useAddressLabel(address);
+ const { data: ensName } = useEnsName({
+ address: address as `0x${string}`,
+ chainId: 1,
+ });
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ const isOwner = useMemo(() => {
+ return mounted && isConnected && address === connectedAddress;
+ }, [address, connectedAddress, isConnected, mounted]);
+
+ const href = useMemo(() => {
+ // When showActions is enabled, don't use linkTo - popover handles navigation
+ if (showActions) return null;
+
+ if (linkTo === 'none') return null;
+ if (linkTo === 'explorer') {
+ return getExplorerURL(address as `0x${string}`, SupportedNetworks.Mainnet);
+ }
+ if (linkTo === 'profile') {
+ return `/positions/${address}`;
+ }
+ return null;
+ }, [linkTo, address, showActions]);
+
+ const handleCopy = useCallback(async () => {
+ try {
+ await navigator.clipboard.writeText(address);
+ toast.success('Address copied', `${address.slice(0, 6)}...${address.slice(-4)}`);
+ } catch (error) {
+ console.error('Failed to copy address', error);
+ }
+ }, [address, toast]);
+
+ // Badge variant - minimal inline badge (no avatar)
+ if (variant === 'badge') {
+ const content = (
+ <>
+ {vaultName ?? }
+ {linkTo === 'explorer' && }
+ {showCopy && (
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ void handleCopy();
+ }}
+ />
+ )}
+ >
+ );
+
+ const badgeClasses = clsx(
+ 'inline-flex items-center gap-1 rounded-sm bg-hovered px-2 py-1 font-zen text-xs text-secondary',
+ copyable && 'cursor-pointer transition-colors hover:brightness-110',
+ href &&
+ 'no-underline transition-colors hover:bg-gray-300 hover:text-primary hover:no-underline dark:hover:bg-gray-700',
+ className,
+ );
+
+ const badgeElement = href ? (
+
+ {
+ if (copyable) {
+ e.preventDefault();
+ void handleCopy();
+ } else if (linkTo === 'explorer') {
+ e.stopPropagation();
+ }
+ }}
+ >
+ {content}
+
+
+ ) : (
+ void handleCopy() : undefined}
+ style={{ cursor: copyable ? 'pointer' : 'default' }}
+ whileHover={{ scale: 1.02 }}
+ whileTap={{ scale: 0.98 }}
+ transition={{ type: 'spring', stiffness: 400, damping: 25 }}
+ >
+ {content}
+
+ );
+
+ if (showActions) {
+ return (
+
+ {badgeElement}
+
+ );
+ }
+
+ return badgeElement;
+ }
+
+ // Compact variant - avatar (16px) wrapped in badge background
+ if (variant === 'compact') {
+ const badgeContent = (
+ <>
+
+
+ {vaultName ?? }
+
+ {linkTo === 'explorer' && }
+ {showCopy && (
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ void handleCopy();
+ }}
+ />
+ )}
+ >
+ );
+
+ const compactClasses = clsx(
+ 'inline-flex items-center gap-1.5 rounded-sm px-1.5 py-1 font-zen text-xs',
+ mounted && isOwner ? 'bg-green-500/10 text-green-500' : 'bg-hovered text-secondary',
+ copyable && 'cursor-pointer transition-colors hover:brightness-110',
+ href &&
+ 'no-underline transition-colors hover:bg-gray-300 hover:text-primary hover:no-underline dark:hover:bg-gray-700',
+ className,
+ );
+
+ const compactElement = href ? (
+
+ {
+ if (copyable) {
+ e.preventDefault();
+ void handleCopy();
+ } else if (linkTo === 'explorer') {
+ e.stopPropagation();
+ }
+ }}
+ >
+ {badgeContent}
+
+
+ ) : (
+ void handleCopy() : undefined}
+ style={{ cursor: copyable ? 'pointer' : 'default' }}
+ whileHover={{ scale: 1.02 }}
+ whileTap={{ scale: 0.98 }}
+ transition={{ type: 'spring', stiffness: 400, damping: 25 }}
+ >
+ {badgeContent}
+
+ );
+
+ if (showActions) {
+ return (
+
+ {compactElement}
+
+ );
+ }
+
+ return compactElement;
+ }
+
+ // Full variant - avatar + address badge + extra info badges (all on one line, centered)
+ const fullContent = (
+ <>
+
+
+ {/* Address badge - always shows shortened address, click to copy */}
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ void handleCopy();
+ }}
+ role="button"
+ tabIndex={0}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ void handleCopy();
+ }
+ }}
+ >
+ {shortAddress}
+
+
+
+ {/* Connected indicator badge */}
+ {mounted && isOwner && (
+
+
+ Connected
+
+ )}
+
+ {/* ENS badge (only show if there's an actual ENS name) */}
+ {showAddress && !vaultName && ensName && (
+
+ {ensName}
+
+ )}
+
+ {/* Vault name badge (if it's a vault) */}
+ {vaultName && (
+
+ {vaultName}
+
+ )}
+
+ {/* Explorer link */}
+ {linkTo === 'explorer' && href && (
+ e.stopPropagation()}
+ >
+
+
+ )}
+ >
+ );
+
+ const fullClasses = clsx(
+ 'flex items-center gap-2',
+ copyable && 'cursor-pointer transition-colors hover:brightness-110',
+ className,
+ );
+
+ const fullElement =
+ href && linkTo === 'profile' ? (
+
+
+ {fullContent}
+
+
+ ) : (
+ void handleCopy() : undefined}
+ style={{ cursor: copyable ? 'pointer' : 'default' }}
+ whileHover={{ scale: 1.01 }}
+ whileTap={{ scale: 0.99 }}
+ transition={{ type: 'spring', stiffness: 400, damping: 25 }}
+ >
+ {fullContent}
+
+ );
+
+ if (showActions) {
+ return (
+
+ {fullElement}
+
+ );
+ }
+
+ return fullElement;
+}
diff --git a/src/components/common/AddressDisplay.tsx b/src/components/common/AddressDisplay.tsx
deleted file mode 100644
index 6b25beec..00000000
--- a/src/components/common/AddressDisplay.tsx
+++ /dev/null
@@ -1,164 +0,0 @@
-'use client';
-
-import { useMemo, useState, useEffect, useCallback, HTMLAttributes } from 'react';
-import clsx from 'clsx';
-import { FaCircle } from 'react-icons/fa';
-import { LuExternalLink } from 'react-icons/lu';
-import { Address } from 'viem';
-import { useAccount } from 'wagmi';
-import { Avatar } from '@/components/Avatar/Avatar';
-import { Name } from '@/components/common/Name';
-import { useStyledToast } from '@/hooks/useStyledToast';
-import { getExplorerURL } from '@/utils/external';
-import { SupportedNetworks } from '@/utils/networks';
-
-type AddressDisplayProps = {
- address: Address;
- chainId?: SupportedNetworks | number;
- size?: 'md' | 'sm';
- showExplorerLink?: boolean;
- className?: string;
- copyable?: boolean;
-};
-
-export function AddressDisplay({
- address,
- chainId,
- size = 'md',
- showExplorerLink = false,
- className,
- copyable = false,
-}: AddressDisplayProps) {
- const { address: connectedAddress, isConnected } = useAccount();
- const [mounted, setMounted] = useState(false);
- const { success: toastSuccess } = useStyledToast();
-
- useEffect(() => {
- setMounted(true);
- }, []);
-
- const isOwner = useMemo(() => {
- return address === connectedAddress;
- }, [address, connectedAddress]);
-
- const explorerHref = useMemo(() => {
- if (!showExplorerLink) return null;
- const numericChainId = Number(chainId ?? 1);
- if (!Number.isFinite(numericChainId)) return null;
- return getExplorerURL(address as `0x${string}`, numericChainId as SupportedNetworks);
- }, [address, chainId, showExplorerLink]);
-
- const handleCopy = useCallback(async () => {
- if (!copyable) return;
-
- try {
- await navigator.clipboard.writeText(address);
- toastSuccess('Address copied', `${address.slice(0, 6)}...${address.slice(-4)}`);
- } catch (error) {
- console.error('Failed to copy address', error);
- }
- }, [address, copyable, toastSuccess]);
-
- const handleKeyDown = useCallback['onKeyDown']>>(
- (event) => {
- if (!copyable) return;
- if (event.key === 'Enter' || event.key === ' ') {
- event.preventDefault();
- void handleCopy();
- }
- },
- [copyable, handleCopy],
- );
-
- // Only add interactive props when copyable=true to satisfy a11y lint rules
- const interactiveProps: HTMLAttributes = copyable
- ? {
- role: 'button',
- tabIndex: 0,
- onClick: () => void handleCopy(),
- onKeyDown: handleKeyDown,
- }
- : {};
-
- if (size === 'sm') {
- return (
-
- );
- }
-
- return (
-
-
-
- {mounted && isOwner && isConnected && (
-
- )}
-
-
-
- );
-}
diff --git a/src/components/common/AddressIdentity.tsx b/src/components/common/AddressIdentity.tsx
deleted file mode 100644
index 88e8ceda..00000000
--- a/src/components/common/AddressIdentity.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-'use client';
-
-import { useMemo } from 'react';
-import { ExternalLinkIcon } from '@radix-ui/react-icons';
-import Link from 'next/link';
-import { Address } from 'viem';
-import { Name } from '@/components/common/Name';
-import { getExplorerURL } from '@/utils/external';
-import { SupportedNetworks } from '@/utils/networks';
-
-type AddressIdentityProps = {
- address: Address;
- chainId: SupportedNetworks;
- showExplorerLink?: boolean;
- className?: string;
-};
-
-export function AddressIdentity({
- address,
- chainId,
- showExplorerLink = true,
- className = '',
-}: AddressIdentityProps) {
- const explorerHref = useMemo(() => {
- return getExplorerURL(address as `0x${string}`, chainId);
- }, [address, chainId]);
-
- if (!showExplorerLink) {
- return (
-
-
-
- );
- }
-
- return (
- e.stopPropagation()}
- >
-
-
-
- );
-}
diff --git a/src/components/common/AllocatorCard.tsx b/src/components/common/AllocatorCard.tsx
index 54c05bd4..49977bbf 100644
--- a/src/components/common/AllocatorCard.tsx
+++ b/src/components/common/AllocatorCard.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import { Address } from 'viem';
-import { AddressDisplay } from './AddressDisplay';
+import { AccountIdentity } from './AccountIdentity';
type AllocatorCardProps = {
name: string;
@@ -50,7 +50,7 @@ export function AllocatorCard({
)}
{description}
diff --git a/src/components/common/index.ts b/src/components/common/index.ts
index b61f8abb..8d412f78 100644
--- a/src/components/common/index.ts
+++ b/src/components/common/index.ts
@@ -1,3 +1,4 @@
export * from './Button';
export * from './TransactionIdentity';
-export * from './AddressIdentity';
+export * from './AccountIdentity';
+export * from './AccountActionsPopover';
diff --git a/src/components/layout/header/AccountDropdown.tsx b/src/components/layout/header/AccountDropdown.tsx
index 82acfca3..7b4440c6 100644
--- a/src/components/layout/header/AccountDropdown.tsx
+++ b/src/components/layout/header/AccountDropdown.tsx
@@ -5,8 +5,8 @@ import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem } from '@heroui/r
import { ExitIcon, ExternalLinkIcon, CopyIcon } from '@radix-ui/react-icons';
import { clsx } from 'clsx';
import { useAccount, useDisconnect } from 'wagmi';
-import AccountWithENS from '@/components/Account/AccountWithENS';
import { Avatar } from '@/components/Avatar/Avatar';
+import { AccountIdentity } from '@/components/common/AccountIdentity';
import { useStyledToast } from '@/hooks/useStyledToast';
import { getExplorerURL } from '@/utils/external';
@@ -59,8 +59,9 @@ export function AccountDropdown() {
isReadOnly
showDivider={false}
>
-
-
+
diff --git a/src/contexts/VaultRegistryContext.tsx b/src/contexts/VaultRegistryContext.tsx
new file mode 100644
index 00000000..6a42bfcc
--- /dev/null
+++ b/src/contexts/VaultRegistryContext.tsx
@@ -0,0 +1,50 @@
+'use client';
+
+import { createContext, useContext, useMemo, type ReactNode } from 'react';
+import type { MorphoVault } from '@/data-sources/morpho-api/vaults';
+import { useAllMorphoVaults } from '@/hooks/useAllMorphoVaults';
+import type { Address } from 'viem';
+
+type VaultRegistryContextType = {
+ vaults: MorphoVault[];
+ loading: boolean;
+ error: Error | null;
+ getVaultByAddress: (address: Address, chainId?: number) => MorphoVault | undefined;
+};
+
+const VaultRegistryContext = createContext
(undefined);
+
+export function VaultRegistryProvider({ children }: { children: ReactNode }) {
+ const { vaults, loading, error } = useAllMorphoVaults();
+
+ const getVaultByAddress = useMemo(
+ () => (address: Address, chainId?: number) => {
+ const normalizedAddress = address.toLowerCase();
+ return vaults.find(
+ (v) =>
+ v.address.toLowerCase() === normalizedAddress && (!chainId || v.chainId === chainId),
+ );
+ },
+ [vaults],
+ );
+
+ const value = useMemo(
+ () => ({
+ vaults,
+ loading,
+ error,
+ getVaultByAddress,
+ }),
+ [vaults, loading, error, getVaultByAddress],
+ );
+
+ return {children};
+}
+
+export function useVaultRegistry() {
+ const context = useContext(VaultRegistryContext);
+ if (context === undefined) {
+ throw new Error('useVaultRegistry must be used within a VaultRegistryProvider');
+ }
+ return context;
+}
diff --git a/src/hooks/useAddressLabel.ts b/src/hooks/useAddressLabel.ts
new file mode 100644
index 00000000..6064cdb5
--- /dev/null
+++ b/src/hooks/useAddressLabel.ts
@@ -0,0 +1,33 @@
+import { useMemo } from 'react';
+import { useVaultRegistry } from '@/contexts/VaultRegistryContext';
+import { getSlicedAddress } from '@/utils/address';
+import type { Address } from 'viem';
+
+type UseAddressLabelReturn = {
+ vaultName: string | undefined;
+ shortAddress: string;
+};
+
+/**
+ * Hook to resolve address labels in priority order:
+ * 1. Vault name (if address is a known vault)
+ * 2. ENS name (handled by Name component)
+ * 3. Shortened address (0x1234...5678)
+ */
+export function useAddressLabel(address: Address, chainId?: number): UseAddressLabelReturn {
+ const { getVaultByAddress } = useVaultRegistry();
+
+ const vaultName = useMemo(() => {
+ const vault = getVaultByAddress(address, chainId);
+ return vault?.name;
+ }, [address, chainId, getVaultByAddress]);
+
+ const shortAddress = useMemo(() => {
+ return getSlicedAddress(address as `0x${string}`);
+ }, [address]);
+
+ return {
+ vaultName,
+ shortAddress,
+ };
+}