Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions app/vault/[chainId]/[vaultAddress]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { generateMetadata as buildMetadata } from '@/utils/generateMetadata';
import VaultContent from '@/features/vault/vault-view';

export async function generateMetadata({ params }: { params: Promise<{ chainId: string; vaultAddress: string }> }) {
const { chainId, vaultAddress } = await params;

return buildMetadata({
title: 'Vault | Monarch',
description: 'Vault exposure, allocation caps, and market position detail',
images: 'themes.png',
pathname: `/vault/${chainId}/${vaultAddress}`,
});
}

export default function VaultPage() {
return <VaultContent />;
}
3 changes: 3 additions & 0 deletions docs/VALIDATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ Use this file at the end of non-trivial work. Do not front-load it at task start
- Metadata-backed display guards must expose readiness through the shared dependency-status layer, must not treat missing metadata as a negative match, and must preserve the list or previous data while the guard cannot be evaluated.
- Market-table data enrichments that affect visible columns or sorting must report degraded readiness to the shared market-data notice surface instead of silently replacing values with empty placeholders.
- Large optional metadata or enrichment queries used only for secondary badges, warnings, filters, or tooltips must be gated or deferred so core table rendering does not wait on them during cold start.
- Vault-scoped pages with configured cap or market IDs must use targeted market reads for first render; do not wait on the global market registry when the vault metadata already identifies the relevant markets.
- Expensive queries must not start with placeholder dependency data that immediately invalidates the same query. Gate on prerequisite readiness, or use a stable query key that does not refetch equivalent work.
- Expensive enrichment queries derived from filtered, sorted, or paginated rows must wait for the inputs that can change those rows, such as USD price enrichment, before they start.
- Periodic refreshes for RPC or API data must use React Query polling with background refetch disabled, or explicitly pause when `document.visibilityState` is hidden. Do not use raw `setInterval` for mounted data refresh unless hidden-tab behavior is handled.
Expand Down Expand Up @@ -107,8 +108,10 @@ Use this file at the end of non-trivial work. Do not front-load it at task start
- Relationship metadata must not link to the current page's own account again; only link to counterpart accounts, external protocol pages, or expandable details.
- V2 vault position pages must not render native vault-address market or vault tables when the meaningful market exposure is held by a linked adapter.
- Vault and adapter relationship UI should prefer short chips, address badges, and structural grouping over explanatory paragraphs.
- Vault identity and vault action links should resolve to Monarch's canonical `/vault/:chainId/:address` route by default; external Morpho vault links belong only in explicit "View on Morpho" actions.
- Use available entity icons in compact metadata chips before adding extra explanatory text.
- Dense product headers should use compact chips, short address links, icon buttons, and tooltips for secondary navigation; avoid long text buttons unless they are the primary action.
- Downstream detail-panel empty states must not contradict confirmed parent/header metadata; if a relationship is configured but no child detail exists, hide the optional panel or use neutral child-specific copy.
- Make sure Mobile view is considered.
- Simplify wording to provide a clear call to action; remove unnecessary explanations. Focus on what a user should do and what they should see, not what you want to say.
- Modals should let the main panel own vertical scrolling; avoid nested scroll regions inside sections unless the content needs an independent fixed context.
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/table-container-with-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export function TableContainerWithDescription({
<div className={`bg-surface rounded-md font-zen shadow-sm ${className}`}>
<div className="flex items-center justify-between border-b border-gray-200 dark:border-gray-800 px-6 py-4">
<div className="flex-1">
<h3 className="text-sm font-medium mb-1">{title}</h3>
<h3 className="mb-1 font-monospace text-xs uppercase text-secondary">{title}</h3>
{description && <p className="text-xs text-secondary">{description}</p>}
</div>
{actions && <div className="flex items-center gap-2 ml-4">{actions}</div>}
Expand Down
18 changes: 14 additions & 4 deletions src/components/shared/account-actions-popover.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';

import { useCallback, type ReactNode } from 'react';
import { useRouter } from 'next/navigation';
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@/components/ui/dropdown-menu';
import { LuCopy, LuUser, LuWallet } from 'react-icons/lu';
import { RiBookmarkFill, RiBookmarkLine } from 'react-icons/ri';
Expand All @@ -15,6 +16,8 @@ type AccountActionsPopoverProps = {
address: Address;
children?: ReactNode;
chainId?: number;
profileHref?: string;
profileLabel?: string;
};

/**
Expand All @@ -23,7 +26,14 @@ type AccountActionsPopoverProps = {
* - View account (positions page)
* - View on Etherscan
*/
export function AccountActionsPopover({ address, chainId, children }: AccountActionsPopoverProps) {
export function AccountActionsPopover({
address,
chainId,
children,
profileHref = `/positions/${address}`,
profileLabel = 'View Portfolio',
}: AccountActionsPopoverProps) {
const router = useRouter();
const toast = useStyledToast();
const { toggleAddressBookmark, isAddressBookmarked } = usePortfolioBookmarks();
const isBookmarked = isAddressBookmarked(address);
Expand All @@ -38,8 +48,8 @@ export function AccountActionsPopover({ address, chainId, children }: AccountAct
}, [address, toast]);

const handleViewAccount = useCallback(() => {
window.location.href = `/positions/${address}`;
}, [address]);
router.push(profileHref);
}, [profileHref, router]);

const handleViewExplorer = useCallback(() => {
const explorerUrl = getExplorerURL(address, (chainId ?? SupportedNetworks.Mainnet) as SupportedNetworks);
Expand All @@ -60,7 +70,7 @@ export function AccountActionsPopover({ address, chainId, children }: AccountAct
onClick={handleViewAccount}
startContent={<LuUser className="h-4 w-4" />}
>
View Portfolio
{profileLabel}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => toggleAddressBookmark(address)}
Expand Down
18 changes: 15 additions & 3 deletions src/components/shared/account-identity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { usePortfolioBookmarks } from '@/stores/usePortfolioBookmarks';
import type { VaultAccountIdentity } from '@/contexts/VaultRegistryContext';
import { getExplorerURL } from '@/utils/external';
import { SupportedNetworks } from '@/utils/networks';
import { formatVaultAdapterType } from '@/utils/vaults';
import { formatVaultAdapterType, getMonarchVaultHref } from '@/utils/vaults';
import type { Address } from 'viem';

const ACCOUNT_IDENTITY_LABEL_MAX_WIDTH_CLASS = 'max-w-[22rem]';
Expand Down Expand Up @@ -119,10 +119,13 @@ export function AccountIdentity({
);
}
if (linkTo === 'profile') {
if (vaultIdentity?.kind === 'vault-v2') {
return getMonarchVaultHref(vaultIdentity.chainId, vaultIdentity.vaultAddress);
}
return `/positions/${address}`;
}
return null;
}, [linkTo, address, showActions, chainId, vaultIdentity?.chainId]);
}, [linkTo, address, showActions, chainId, vaultIdentity?.chainId, vaultIdentity?.kind, vaultIdentity?.vaultAddress]);

const handleCopy = useCallback(async () => {
try {
Expand Down Expand Up @@ -154,8 +157,11 @@ export function AccountIdentity({
) : null;
const linkedVaultHref =
vaultIdentity?.kind === 'vault-adapter' && vaultIdentity.vaultAddress.toLowerCase() !== address.toLowerCase()
? `/positions/${vaultIdentity.vaultAddress}`
? getMonarchVaultHref(vaultIdentity.chainId, vaultIdentity.vaultAddress)
: undefined;
const actionsProfileHref =
vaultIdentity?.kind === 'vault-v2' ? getMonarchVaultHref(vaultIdentity.chainId, vaultIdentity.vaultAddress) : `/positions/${address}`;
const actionsProfileLabel = vaultIdentity?.kind === 'vault-v2' ? 'View Vault' : 'View Portfolio';

// Badge variant - minimal inline badge (no avatar)
if (variant === 'badge') {
Expand Down Expand Up @@ -236,6 +242,8 @@ export function AccountIdentity({
<AccountActionsPopover
address={address}
chainId={chainId ?? vaultIdentity?.chainId}
profileHref={actionsProfileHref}
profileLabel={actionsProfileLabel}
>
{badgeElement}
</AccountActionsPopover>
Expand Down Expand Up @@ -332,6 +340,8 @@ export function AccountIdentity({
<AccountActionsPopover
address={address}
chainId={chainId ?? vaultIdentity?.chainId}
profileHref={actionsProfileHref}
profileLabel={actionsProfileLabel}
>
{compactElement}
</AccountActionsPopover>
Expand Down Expand Up @@ -480,6 +490,8 @@ export function AccountIdentity({
<AccountActionsPopover
address={address}
chainId={chainId ?? vaultIdentity?.chainId}
profileHref={actionsProfileHref}
profileLabel={actionsProfileLabel}
>
{identityTrigger}
</AccountActionsPopover>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useMarketNetwork } from '@/hooks/useMarketNetwork';
import { abi as vaultFactoryAbi } from '@/abis/vaultv2factory';
import type { SupportedNetworks } from '@/utils/networks';
import { addDeployedVault } from '@/utils/vault-storage';
import { getMonarchVaultHref } from '@/utils/vaults';

export type DeploymentPhase = 'selection' | 'deploying' | 'success';

Expand Down Expand Up @@ -114,7 +115,7 @@ export function DeploymentProvider({ children }: { children: React.ReactNode })

const navigateToVault = useCallback(() => {
if (deployedVaultAddress && selectedTokenAndNetwork) {
router.push(`/autovault/${selectedTokenAndNetwork.networkId}/${deployedVaultAddress}`);
router.push(getMonarchVaultHref(selectedTokenAndNetwork.networkId, deployedVaultAddress));
}
}, [deployedVaultAddress, selectedTokenAndNetwork, router]);

Expand Down

This file was deleted.

Loading