Skip to content
12 changes: 0 additions & 12 deletions app/history/[account]/page.tsx

This file was deleted.

22 changes: 22 additions & 0 deletions app/position/[chainId]/[loanAssetAddress]/[userAddress]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { Metadata } from 'next';
import PositionDetailContent from '@/features/position-detail/position-view';

export const metadata: Metadata = {
title: 'Position Detail | Monarch',
description: 'View detailed position information on Monarch',
};

export default async function PositionDetailPage({
params,
}: {
params: Promise<{ chainId: string; loanAssetAddress: string; userAddress: string }>;
}) {
const p = await params;
return (
<PositionDetailContent
chainId={Number(p.chainId)}
loanAssetAddress={p.loanAssetAddress}
userAddress={p.userAddress}
Comment on lines +9 to +19
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "page.tsx" | grep -i position | head -20

Repository: antoncoding/monarch

Length of output: 231


🏁 Script executed:

cd app && find . -type f -path "*position*" -name "page.tsx"

Repository: antoncoding/monarch

Length of output: 215


🏁 Script executed:

git ls-files | grep -i "position.*page.tsx"

Repository: antoncoding/monarch

Length of output: 223


🏁 Script executed:

cat -n app/position/[chainId]/[loanAssetAddress]/[userAddress]/page.tsx

Repository: antoncoding/monarch

Length of output: 823


🏁 Script executed:

cat -n app/features/position-detail/position-view.tsx | head -50

Repository: antoncoding/monarch

Length of output: 141


🏁 Script executed:

rg "notFound" --type tsx --type ts | head -20

Repository: antoncoding/monarch

Length of output: 90


🏁 Script executed:

rg "Number\(" app/position -A 2 -B 2 --type tsx

Repository: antoncoding/monarch

Length of output: 90


🏁 Script executed:

find . -type f \( -name "*.tsx" -o -name "*.ts" \) | xargs grep -l "PositionDetailContent" | head -5

Repository: antoncoding/monarch

Length of output: 178


🏁 Script executed:

rg "notFound" | head -20

Repository: antoncoding/monarch

Length of output: 399


🏁 Script executed:

find . -type f \( -name "page.tsx" -o -name "page.ts" \) | xargs grep -l "Number(" | head -10

Repository: antoncoding/monarch

Length of output: 129


🏁 Script executed:

cat -n src/features/position-detail/position-view.tsx | head -80

Repository: antoncoding/monarch

Length of output: 3795


🏁 Script executed:

find . -type f \( -name "*.tsx" -o -name "*.ts" \) -path "*position*" | xargs grep -l "chainId" | head -10

Repository: antoncoding/monarch

Length of output: 633


🏁 Script executed:

rg "Number\(" app/ --context 3 | head -40

Repository: antoncoding/monarch

Length of output: 1055


🏁 Script executed:

cat -n app/positions/\[account\]/page.tsx

Repository: antoncoding/monarch

Length of output: 532


🏁 Script executed:

cat -n app/positions/page.tsx

Repository: antoncoding/monarch

Length of output: 638


🏁 Script executed:

rg "import.*notFound.*from.*next"

Repository: antoncoding/monarch

Length of output: 45


🏁 Script executed:

rg "notFound\(\)" --context 2

Repository: antoncoding/monarch

Length of output: 45


🏁 Script executed:

rg "SupportedNetworks" --context 2 | head -40

Repository: antoncoding/monarch

Length of output: 2750


🏁 Script executed:

find . -type f \( -name "*.ts" -o -name "*.tsx" \) | xargs grep -l "SupportedNetworks" | head -5

Repository: antoncoding/monarch

Length of output: 276


🏁 Script executed:

cat -n src/utils/networks.ts

Repository: antoncoding/monarch

Length of output: 9798


🏁 Script executed:

rg "isSupportedChain" --context 3

Repository: antoncoding/monarch

Length of output: 6827


🏁 Script executed:

grep -n "useUserPositionsSummaryData" src/hooks/*.ts src/hooks/*.tsx | head -20

Repository: antoncoding/monarch

Length of output: 561


🏁 Script executed:

cat -n src/hooks/useUserPositionsSummaryData.ts | head -60

Repository: antoncoding/monarch

Length of output: 2890


Add validation for invalid chainId in URL segment.

Non-numeric URL segments like /position/abc/... convert to NaN via Number(), which bypasses type safety and causes downstream issues. Add a guard before passing to the component.

Suggested fix
-import type { Metadata } from 'next';
+import type { Metadata } from 'next';
+import { notFound } from 'next/navigation';
@@
-  const p = await params;
+  const p = await params;
+  const chainId = Number(p.chainId);
+  if (!Number.isFinite(chainId)) {
+    notFound();
+  }
   return (
     <PositionDetailContent
-      chainId={Number(p.chainId)}
+      chainId={chainId}
       loanAssetAddress={p.loanAssetAddress}
       userAddress={p.userAddress}
     />
   );
🤖 Prompt for AI Agents
In `@app/position/`[chainId]/[loanAssetAddress]/[userAddress]/page.tsx around
lines 9 - 19, Validate the chainId URL segment before converting and passing it
on: in PositionDetailPage, parse p.chainId into a numeric value (e.g., const
chainIdNum = Number(p.chainId) or parseInt(p.chainId, 10)) and guard against
invalid values using Number.isFinite/Number.isInteger or Number.isNaN; if the
value is invalid, return a proper Next.js fallback (e.g., call notFound() from
next/navigation or render an error) instead of passing NaN into
PositionDetailContent, then pass the validated chainIdNum to
PositionDetailContent.

/>
Comment on lines +1 to +20
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "page.tsx" | grep -E "position.*chainId.*loanAsset.*userAddress" | head -20

Repository: antoncoding/monarch

Length of output: 129


🏁 Script executed:

fd "page.tsx" app/position

Repository: antoncoding/monarch

Length of output: 127


🏁 Script executed:

find app -type d -name "\[chainId\]"

Repository: antoncoding/monarch

Length of output: 130


🏁 Script executed:

cat -n app/position/[chainId]/[loanAssetAddress]/[userAddress]/page.tsx

Repository: antoncoding/monarch

Length of output: 823


🏁 Script executed:

rg "PositionDetailContent" --type tsx -A 10 -B 2

Repository: antoncoding/monarch

Length of output: 90


🏁 Script executed:

rg "PositionDetailContent" --type ts -A 10 -B 2

Repository: antoncoding/monarch

Length of output: 3535


🏁 Script executed:

rg "export.*PositionDetailContent" -A 50

Repository: antoncoding/monarch

Length of output: 4294


🏁 Script executed:

find . -name "position-view.*" -type f

Repository: antoncoding/monarch

Length of output: 111


🏁 Script executed:

cat -n src/features/position-detail/position-view.tsx | head -50

Repository: antoncoding/monarch

Length of output: 2440


🏁 Script executed:

rg "SupportedNetworks" -A 2 -B 2 | head -40

Repository: antoncoding/monarch

Length of output: 2681


🏁 Script executed:

rg "\[chainId\]" app/ --type-list

Repository: antoncoding/monarch

Length of output: 452


🏁 Script executed:

grep -r "notFound\(\)" app/ | head -10

Repository: antoncoding/monarch

Length of output: 45


🏁 Script executed:

rg "params.*Promise" app/ -A 5 | head -60

Repository: antoncoding/monarch

Length of output: 1871


🏁 Script executed:

find app -name "page.tsx" -exec grep -l "notFound\|redirect" {} \; | head -10

Repository: antoncoding/monarch

Length of output: 45


🏁 Script executed:

rg "Number\(" app/ --type ts | head -20

Repository: antoncoding/monarch

Length of output: 248


🏁 Script executed:

rg "SupportedNetworks" src/utils/networks.ts -A 10 -B 2

Repository: antoncoding/monarch

Length of output: 5920


🏁 Script executed:

cat -n src/utils/networks.ts | head -40

Repository: antoncoding/monarch

Length of output: 1772


🏁 Script executed:

rg "useUserPositionsSummaryData" src/hooks/ -A 15 | head -50

Repository: antoncoding/monarch

Length of output: 3458


🏁 Script executed:

cat -n src/utils/networks.ts | grep -A 3 "isSupportedChain"

Repository: antoncoding/monarch

Length of output: 210


🏁 Script executed:

rg "isSupportedChain" --type ts

Repository: antoncoding/monarch

Length of output: 1393


Validate chainId before using downstream.

Line 17 casts to number without checking validity. An invalid route param produces NaN, which breaks the chainId === chainId comparison in the useMemo and fails downstream hook calls. Use isSupportedChain() to validate against the supported networks enum, or check Number.isFinite() and call notFound().

Suggested fix
 import type { Metadata } from 'next';
+import { notFound } from 'next/navigation';
+import { isSupportedChain } from '@/utils/networks';
 import PositionDetailContent from '@/features/position-detail/position-view';

@@
 }) {
   const p = await params;
+  const chainIdNum = Number(p.chainId);
+  if (!isSupportedChain(chainIdNum)) notFound();
   return (
     <PositionDetailContent
-      chainId={Number(p.chainId)}
+      chainId={chainIdNum}
       loanAssetAddress={p.loanAssetAddress}
       userAddress={p.userAddress}
     />
🤖 Prompt for AI Agents
In `@app/position/`[chainId]/[loanAsset]/[userAddress]/page.tsx around lines 1 -
20, PositionDetailPage currently casts params.chainId to Number without
validating, which can produce NaN and break downstream logic; update
PositionDetailPage to validate the parsed chainId (e.g., use
isSupportedChain(parsed) against your supported networks enum or check
Number.isFinite(parsed)) and call notFound() if invalid, then pass the validated
numeric chainId into PositionDetailContent; reference the PositionDetailPage
function and the chainId prop on PositionDetailContent when making the change.

);
}
9 changes: 2 additions & 7 deletions app/positions/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import SearchOrConnect from '@/components/SearchOrConnect/SearchOrConnect';
import { generateMetadata } from '@/utils/generateMetadata';
import PositionsLandingView from '@/features/positions/positions-landing-view';

export const metadata = generateMetadata({
title: 'Portfolio | Monarch',
Expand All @@ -9,10 +9,5 @@ export const metadata = generateMetadata({
});

export default function LogIn() {
return (
<SearchOrConnect
path="positions"
title="Positions"
/>
);
return <PositionsLandingView />;
}
9 changes: 2 additions & 7 deletions app/rewards/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import SearchOrConnect from '@/components/SearchOrConnect/SearchOrConnect';
import { generateMetadata } from '@/utils/generateMetadata';
import RewardsLandingView from '@/features/rewards/rewards-landing-view';

export const metadata = generateMetadata({
title: 'Rewards | Monarch',
Expand All @@ -9,10 +9,5 @@ export const metadata = generateMetadata({
});

export default function LogIn() {
return (
<SearchOrConnect
path="rewards"
title="Rewards"
/>
);
return <RewardsLandingView />;
}
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 @@ -32,7 +32,7 @@ type TableContainerWithHeaderProps = {
export function TableContainerWithHeader({ title, actions, children, className = '' }: TableContainerWithHeaderProps) {
return (
<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-0.5">
<div className="flex items-center justify-between border-b border-gray-200 dark:border-gray-800 px-6 py-3">
<h3 className="font-monospace text-xs uppercase text-secondary">{title}</h3>
{actions && <div className="flex items-center gap-2">{actions}</div>}
</div>
Expand Down
21 changes: 18 additions & 3 deletions src/components/layout/header/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,12 @@ export function Navbar() {
<div className="flex items-center gap-1">
{/* Nav links with dashed dividers */}
<div className="flex items-center">
<NavbarLink href="/markets">Markets</NavbarLink>
<NavbarLink
href="/markets"
matchKey="/market"
>
Markets
</NavbarLink>
<span className="mx-1 h-4 border-l border-dashed border-[var(--grid-cell-muted)]" />
<NavbarLink
href="/autovault"
Expand All @@ -109,7 +114,12 @@ export function Navbar() {
<span className="mx-1 h-4 border-l border-dashed border-[var(--grid-cell-muted)]" />
{mounted ? (
<>
<NavbarLink href={address ? `/positions/${address}` : '/positions'}>Portfolio</NavbarLink>
<NavbarLink
href={address ? `/positions/${address}` : '/positions'}
matchKey="/position"
>
Portfolio
</NavbarLink>
<span className="mx-1 h-4 border-l border-dashed border-[var(--grid-cell-muted)]" />
<NavbarLink
href={address ? `/rewards/${address}` : '/rewards'}
Expand All @@ -120,7 +130,12 @@ export function Navbar() {
</>
) : (
<>
<NavbarLink href="/positions">Portfolio</NavbarLink>
<NavbarLink
href="/positions"
matchKey="/position"
>
Portfolio
</NavbarLink>
<span className="mx-1 h-4 border-l border-dashed border-[var(--grid-cell-muted)]" />
<NavbarLink
href="/rewards"
Expand Down
22 changes: 16 additions & 6 deletions src/components/shared/account-actions-popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import { useCallback, type ReactNode } from 'react';
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';
import { SiEthereum } from 'react-icons/si';
import { useStyledToast } from '@/hooks/useStyledToast';
import { usePortfolioBookmarks } from '@/stores/usePortfolioBookmarks';
import { getExplorerURL } from '@/utils/external';
import { SupportedNetworks } from '@/utils/networks';
import type { Address } from 'viem';
Expand All @@ -23,6 +25,8 @@ type AccountActionsPopoverProps = {
*/
export function AccountActionsPopover({ address, chainId, children }: AccountActionsPopoverProps) {
const toast = useStyledToast();
const { toggleAddressBookmark, isAddressBookmarked } = usePortfolioBookmarks();
const isBookmarked = isAddressBookmarked(address);

const handleCopy = useCallback(async () => {
try {
Expand Down Expand Up @@ -53,16 +57,16 @@ export function AccountActionsPopover({ address, chainId, children }: AccountAct
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
onClick={() => void handleCopy()}
startContent={<LuCopy className="h-4 w-4" />}
onClick={handleViewAccount}
startContent={<LuUser className="h-4 w-4" />}
>
Copy Address
View Portfolio
</DropdownMenuItem>
<DropdownMenuItem
onClick={handleViewAccount}
startContent={<LuUser className="h-4 w-4" />}
onClick={() => toggleAddressBookmark(address)}
startContent={isBookmarked ? <RiBookmarkFill className="h-4 w-4" /> : <RiBookmarkLine className="h-4 w-4" />}
>
View Monarch Portfolio
{isBookmarked ? 'Remove Bookmark' : 'Bookmark Address'}
</DropdownMenuItem>
<DropdownMenuItem
onClick={handleViewExplorer}
Expand All @@ -76,6 +80,12 @@ export function AccountActionsPopover({ address, chainId, children }: AccountAct
>
View on DeBank
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => void handleCopy()}
startContent={<LuCopy className="h-4 w-4" />}
>
Copy Address
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
Expand Down
22 changes: 22 additions & 0 deletions src/components/shared/account-identity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import Link from 'next/link';
import { FaCircle } from 'react-icons/fa';
import { ExternalLinkIcon } from '@radix-ui/react-icons';
import { LuCopy } from 'react-icons/lu';
import { RiBookmarkFill, RiBookmarkLine } from 'react-icons/ri';
import { useConnection, useEnsName } from 'wagmi';
import { Avatar } from '@/components/Avatar/Avatar';
import { AccountActionsPopover } from '@/components/shared/account-actions-popover';
import { Name } from '@/components/shared/name';
import { useAddressLabel } from '@/hooks/useAddressLabel';
import { useStyledToast } from '@/hooks/useStyledToast';
import { usePortfolioBookmarks } from '@/stores/usePortfolioBookmarks';
import { getExplorerURL } from '@/utils/external';
import { SupportedNetworks } from '@/utils/networks';
import type { Address } from 'viem';
Expand All @@ -26,6 +28,7 @@ type AccountIdentityProps = {
showCopy?: boolean;
showAddress?: boolean;
showActions?: boolean;
showBookmark?: boolean;
className?: string;
};

Expand All @@ -49,11 +52,13 @@ export function AccountIdentity({
showCopy = false,
showAddress = false,
showActions = true,
showBookmark = false,
className,
}: AccountIdentityProps) {
const { address: connectedAddress, isConnected } = useConnection();
const [mounted, setMounted] = useState(false);
const toast = useStyledToast();
const { toggleAddressBookmark, isAddressBookmarked } = usePortfolioBookmarks();
const { vaultName, shortAddress } = useAddressLabel(address);
const { data: ensName } = useEnsName({
address: address as `0x${string}`,
Expand Down Expand Up @@ -250,6 +255,8 @@ export function AccountIdentity({
return compactElement;
}

const isBookmarked = showBookmark ? isAddressBookmarked(address) : false;

// Full variant - avatar + address badge + extra info badges (all on one line, centered)
const fullContent = (
<>
Expand Down Expand Up @@ -310,6 +317,21 @@ export function AccountIdentity({
<ExternalLinkIcon className="h-4 w-4" />
</a>
)}

{showBookmark && (
<button
type="button"
className={clsx('rounded-sm p-1 transition-colors', isBookmarked ? 'text-primary' : 'text-secondary hover:text-primary')}
aria-label={isBookmarked ? 'Remove address bookmark' : 'Bookmark address'}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
toggleAddressBookmark(address);
}}
>
{isBookmarked ? <RiBookmarkFill className="h-4 w-4" /> : <RiBookmarkLine className="h-4 w-4" />}
</button>
)}
</>
);

Expand Down
43 changes: 43 additions & 0 deletions src/components/shared/breadcrumbs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { ReactNode } from 'react';
import Link from 'next/link';
import { cn } from '@/utils/components';

type BreadcrumbItem = {
label: ReactNode;
href?: string;
isCurrent?: boolean;
};

type BreadcrumbsProps = {
items: BreadcrumbItem[];
className?: string;
};

export function Breadcrumbs({ items, className }: BreadcrumbsProps) {
return (
<nav className={cn('flex items-center gap-2 text-sm text-secondary flex-nowrap overflow-x-auto leading-none py-1 font-zen', className)}>
{items.map((item, index) => {
const content = item.href ? (
<Link
href={item.href}
className={cn('no-underline hover:no-underline text-secondary', item.isCurrent ? 'text-primary' : 'hover:text-primary')}
>
{item.label}
</Link>
) : (
<span className={cn(item.isCurrent ? 'text-primary' : 'text-secondary')}>{item.label}</span>
);

return (
<span
key={index}
className="flex items-center gap-2"
>
{index > 0 && <span className="text-primary/60">/</span>}
{content}
</span>
);
})}
</nav>
);
}
15 changes: 13 additions & 2 deletions src/components/shared/date-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,20 @@ export type DatePickerProps = {
isInvalid?: boolean;
errorMessage?: string;
granularity?: 'day' | 'hour';
popoverClassName?: string;
};

function DatePicker({ label, value, onChange, minValue, maxValue, isInvalid, errorMessage, granularity = 'day' }: DatePickerProps) {
function DatePicker({
label,
value,
onChange,
minValue,
maxValue,
isInvalid,
errorMessage,
granularity = 'day',
popoverClassName,
}: DatePickerProps) {
const [open, setOpen] = React.useState(false);

// Get timezone from value, or use local timezone
Expand Down Expand Up @@ -149,7 +160,7 @@ function DatePicker({ label, value, onChange, minValue, maxValue, isInvalid, err
</button>
</PopoverTrigger>
<PopoverContent
className="w-auto p-0 font-zen"
className={cn('w-auto p-0 font-zen', popoverClassName)}
align="start"
>
<Calendar
Expand Down
13 changes: 10 additions & 3 deletions src/features/autovault/vault-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ export default function VaultContent() {
const tokenDecimals = vaultData?.tokenDecimals;
const tokenSymbol = vaultData?.tokenSymbol;
const assetAddress = vaultData?.assetAddress as Address | undefined;
const adapterAddress = adapterQuery.morphoMarketV1Adapter as Address | undefined;

const adapterPortfolioHref = useMemo(() => {
if (!adapterAddress || !assetAddress) return undefined;
return `/position/${chainId}/${assetAddress}/${adapterAddress}`;
}, [adapterAddress, assetAddress, chainId]);

// UI state from Zustand stores (for vault-view banners only)
const { open: openSettings } = useVaultSettingsModalStore();
Expand Down Expand Up @@ -222,7 +228,7 @@ export default function VaultContent() {
allocators={vaultData?.allocators}
collaterals={collateralAddresses}
curator={vaultData?.curator}
adapter={adapterQuery.morphoMarketV1Adapter ?? undefined}
adapter={adapterAddress}
onDeposit={handleDeposit}
onWithdraw={handleWithdraw}
onRefresh={handleRefreshVault}
Expand Down Expand Up @@ -296,12 +302,13 @@ export default function VaultContent() {
/>

{/* Transaction History Preview - only show when vault is fully set up */}
{adapterQuery.morphoMarketV1Adapter && isVaultInitialized && !capsUninitialized && (
{adapterAddress && isVaultInitialized && !capsUninitialized && (
<TransactionHistoryPreview
account={adapterQuery.morphoMarketV1Adapter}
account={adapterAddress}
chainId={chainId}
isVaultAdapter={true}
emptyMessage="Setup complete, your automated rebalance will show up here once it's triggered."
viewAllHref={adapterPortfolioHref}
/>
)}

Expand Down
Loading