diff --git a/docs/VALIDATIONS.md b/docs/VALIDATIONS.md
index cd084511..ec7f30da 100644
--- a/docs/VALIDATIONS.md
+++ b/docs/VALIDATIONS.md
@@ -74,6 +74,7 @@ Use this file at the end of non-trivial work. Do not front-load it at task start
- 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.
+- Vault adapter selection must be cap-aware when a vault has multiple active adapters; do not let list order alone choose the adapter used for positions, activity, withdrawals, or settings.
- 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.
@@ -96,6 +97,8 @@ Use this file at the end of non-trivial work. Do not front-load it at task start
- Avoid repeated large UI blocks; extract or reuse only when it reduces real duplication.
- Validate loading, empty, disabled, error, and success states for changed flows; period-derived metrics must not show stale values while recalculating.
+- Related analytics sections that share period-dependent data should expose one period control at the shared container level, not competing child-level dropdowns.
+- Yield, rate, and share-price charts must use period-aware minimum y-axis bands when comparing growth; do not let data-only auto-fit scaling make materially different APRs look visually identical.
- Cold-start loading for optional metadata or enrichment must not trigger warning/error banners; warn only after a source is partial, stale, or unavailable.
- Product routes must not add render-blocking external font CSS or multi-megabyte custom font assets to the app shell; prefer system font stacks or prove a small subset budget.
- Font performance changes must preserve the intended design-token font families. Optimize with self-hosted subsets or scoped loading, not by silently mapping custom font utilities to system stacks.
@@ -108,6 +111,7 @@ 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 adapter labels must come from confirmed adapter type metadata or fall back to a generic "vault adapter" label; do not infer protocol-specific adapter types from a missing `adapterType`.
- 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.
diff --git a/src/features/position-detail/components/position-period-selector.tsx b/src/components/common/period-selector.tsx
similarity index 63%
rename from src/features/position-detail/components/position-period-selector.tsx
rename to src/components/common/period-selector.tsx
index 5da5e74a..39a352fd 100644
--- a/src/features/position-detail/components/position-period-selector.tsx
+++ b/src/components/common/period-selector.tsx
@@ -1,24 +1,37 @@
'use client';
-import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@/components/ui/select';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import type { EarningsPeriod } from '@/stores/usePositionsFilters';
-type PositionPeriodSelectorProps = {
- period: EarningsPeriod;
- onPeriodChange: (period: EarningsPeriod) => void;
- className?: string;
- contentClassName?: string;
+export type PeriodSelectorOption = {
+ value: EarningsPeriod;
+ label: string;
};
-const PERIOD_OPTIONS: { value: EarningsPeriod; label: string }[] = [
+export const EARNINGS_PERIOD_OPTIONS: readonly PeriodSelectorOption[] = [
{ value: 'day', label: '24h' },
{ value: 'week', label: '7 days' },
{ value: 'month', label: '30 days' },
+ { value: 'threemonth', label: '3 months' },
{ value: 'sixmonth', label: '6 months' },
{ value: 'all', label: 'All time' },
];
-export function PositionPeriodSelector({ period, onPeriodChange, className, contentClassName }: PositionPeriodSelectorProps) {
+type PeriodSelectorProps = {
+ period: EarningsPeriod;
+ onPeriodChange: (period: EarningsPeriod) => void;
+ options?: readonly PeriodSelectorOption[];
+ className?: string;
+ contentClassName?: string;
+};
+
+export function PeriodSelector({
+ period,
+ onPeriodChange,
+ options = EARNINGS_PERIOD_OPTIONS,
+ className,
+ contentClassName,
+}: PeriodSelectorProps) {
return (
- {PERIOD_OPTIONS.map((option) => (
+ {options.map((option) => (
=> {
const response = await monarchGraphqlFetcher(userVaultsQuery, {
owner: owner.toLowerCase(),
@@ -359,10 +393,36 @@ const transformAdapterAliasRecord = (adapter: MonarchAdapterAliasRecord): VaultA
};
};
+const transformVaultAdapterAliasRecord = (vault: MonarchVaultAdapterAliasRecord): VaultAdapterAlias[] => {
+ const chainId = toSupportedNetwork(vault.chainId);
+ const vaultAddress = normalizeAddress(vault.vaultAddress);
+ const vaultName = vault.name?.trim() || vault.symbol?.trim() || '';
+ if (!chainId || !vaultAddress || !vaultName) {
+ return [];
+ }
+
+ return vault.adapters
+ .filter((adapter) => adapter.isActive)
+ .map((adapter) => normalizeAddress(adapter.adapterAddress))
+ .filter(Boolean)
+ .map((address) => ({
+ address,
+ chainId,
+ vaultAddress,
+ vaultName,
+ }));
+};
+
export const fetchMonarchVaultAdapterAliases = async (): Promise => {
try {
- const aliases: VaultAdapterAlias[] = [];
- const seenKeys = new Set();
+ const aliasesByKey = new Map();
+ const addAlias = (alias: VaultAdapterAlias) => {
+ const key = `${alias.chainId}:${alias.address}`;
+ const existing = aliasesByKey.get(key);
+ if (!existing || (!existing.adapterType && alias.adapterType)) {
+ aliasesByKey.set(key, alias);
+ }
+ };
for (let page = 0; page < MONARCH_ADAPTER_ALIAS_MAX_PAGES; page++) {
const response = await monarchGraphqlFetcher(adapterAliasesQuery, {
@@ -378,13 +438,25 @@ export const fetchMonarchVaultAdapterAliases = async (): Promise(activeVaultAdapterAliasesQuery, {
+ limit: MONARCH_ADAPTER_ALIAS_PAGE_SIZE,
+ offset: page * MONARCH_ADAPTER_ALIAS_PAGE_SIZE,
+ });
+
+ const records = response.data?.Vault ?? [];
+ for (const record of records) {
+ for (const alias of transformVaultAdapterAliasRecord(record)) {
+ addAlias(alias);
+ }
}
if (records.length < MONARCH_ADAPTER_ALIAS_PAGE_SIZE) {
@@ -392,7 +464,7 @@ export const fetchMonarchVaultAdapterAliases = async (): Promise
+ left.timestamp - right.timestamp;
+
+function normalizeSharePrice(value: unknown): number | null {
+ const numericValue = typeof value === 'string' ? Number(value) : value;
+
+ if (typeof numericValue !== 'number' || !Number.isFinite(numericValue)) {
+ return null;
+ }
+
+ return numericValue;
+}
+
+export async function fetchMorphoVaultV2SharePriceHistory({
+ vaultAddress,
+ chainId,
+ options,
+}: {
+ vaultAddress: string;
+ chainId: SupportedNetworks;
+ options: TimeseriesOptions;
+}): Promise {
+ try {
+ const response = await morphoGraphqlFetcher(
+ vaultV2SharePriceHistoryQuery,
+ {
+ address: vaultAddress,
+ chainId,
+ options,
+ },
+ { timeoutMs: 8000 },
+ );
+
+ const points = response?.data?.vaultV2ByAddress?.historicalState?.sharePrice;
+ if (!points || points.length === 0) {
+ return null;
+ }
+
+ const normalizedPoints = points
+ .map((point) => {
+ const timestamp = Number(point.x);
+ const sharePrice = normalizeSharePrice(point.y);
+ if (!Number.isFinite(timestamp) || sharePrice === null) {
+ return null;
+ }
+
+ return {
+ timestamp,
+ sharePrice,
+ };
+ })
+ .filter((point): point is MorphoVaultSharePricePoint => point !== null)
+ .sort(sortByTimestamp);
+
+ return normalizedPoints.length > 0 ? normalizedPoints : null;
+ } catch (error) {
+ console.warn('[vaultSharePriceHistory] Morpho API share price history unavailable:', error);
+ return null;
+ }
+}
diff --git a/src/features/autovault/components/vault-detail/vault-header.tsx b/src/features/autovault/components/vault-detail/vault-header.tsx
index b03eea15..69fe85ee 100644
--- a/src/features/autovault/components/vault-detail/vault-header.tsx
+++ b/src/features/autovault/components/vault-detail/vault-header.tsx
@@ -23,6 +23,11 @@ import { CollateralIconsDisplay } from '@/features/positions/components/collater
import { getSlicedAddress } from '@/utils/address';
import { formatVaultAdapterType } from '@/utils/vaults';
+type VaultHeaderAdapterRow = {
+ adapter: Address;
+ adapterType?: string;
+};
+
type VaultHeaderProps = {
vaultAddress: Address;
chainId: SupportedNetworks;
@@ -39,7 +44,8 @@ type VaultHeaderProps = {
collaterals?: { address: string; symbol: string; amount: number }[];
curator?: string;
adapter?: string;
- adapters?: { adapter: Address; adapterType?: string }[];
+ adapters?: VaultHeaderAdapterRow[];
+ capsAdapters?: VaultHeaderAdapterRow[];
onDeposit: () => void;
onWithdraw: () => void;
onRefresh: () => void;
@@ -67,6 +73,7 @@ export function VaultHeader({
curator,
adapter,
adapters = [],
+ capsAdapters = [],
onDeposit,
onWithdraw,
onRefresh,
@@ -92,6 +99,13 @@ export function VaultHeader({
// Filter for known agents
const knownAllocators = allocators.filter((addr) => findAgent(addr) !== undefined);
const adapterRows = adapters.length > 0 ? adapters : adapter ? [{ adapter: adapter as Address }] : [];
+ const capsAdapterRows = capsAdapters;
+ const capsAdapterKeys = new Set(capsAdapterRows.map((row) => row.adapter.toLowerCase()));
+ const showCapsAdapter = adapterRows.length > 1 && capsAdapterRows.length > 0;
+ const capsAdapterLabel =
+ capsAdapterRows.length === 1
+ ? `${formatVaultAdapterType(capsAdapterRows[0].adapterType)} ${getSlicedAddress(capsAdapterRows[0].adapter)}`
+ : `${capsAdapterRows.length} configured adapters`;
return (