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
5 changes: 4 additions & 1 deletion docs/TECHNICAL_OVERVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ Market metrics: external data API via `/v1/markets/metrics`
| Vaults list | Morpho API | 5 min | `useAllMorphoVaultsQuery` |
| User autovault metadata | Monarch GraphQL + on-chain enrichment | 60s | `useUserVaultsV2Query` |
| Vault detail/settings metadata | Monarch GraphQL + narrow RPC fallback | 30s | `useVaultV2Data` |
| Vault detail native-yield/deposits/share-price history | Morpho Vault V2 historical state → archive RPC fallback for deposits and share price | 5 min stale | `useVaultHistoryQuery` |
| Vault V2 rewards | Merkl API opportunities via `/api/merkl` | 5 min | `useVaultV2RewardsQuery` |
| Market detail participants/activity | Monarch GraphQL + Morpho API fallback | 2-5 min stale | `useMarketSuppliers` / `useMarketBorrowers` / `useMarketSupplies` / `useMarketBorrows` |
| Vault allocations | On-chain multicall | 30s | `useAllocationsQuery` |
Expand Down Expand Up @@ -268,6 +269,7 @@ Hooks omitted from this matrix are local-state hooks or pure view/composition he
| `usePublicAllocatorVaults` | Public allocator config for supplying vaults in a market | Morpho API only | Intentionally Morpho-only today |
| `useAllocationsQuery` | Live vault `allocation(capId)` values | Pure RPC multicall | No Envio gap |
| `usePublicAllocatorLiveData` | Live flow caps, vault supply, and liquidity for allocator UX | Pure RPC multicall | No Envio gap |
| `useVaultHistoryQuery` | Vault detail selected-period realized yield, current 6-hour native yield, total-deposit, and share-price charts | Morpho Vault V2 historical `avgApy` + `totalAssets` + `sharePrice`; realized/implied yield derived from share-price growth; archive RPC data source for `totalAssets` + `previewRedeem` fallback | Historical vault state snapshots or an equivalent accrued-assets series; cumulative Monarch deposit/withdraw totals do not include accrued yield |
| `useVaultHistoricalApy` / `useErc4626VaultAPR` | Historical 4626 yield and expected carry calculations | Pure RPC share-price snapshots + RPC Morpho market reads | No Envio gap |

#### RPC Helpers And External Reads
Expand Down Expand Up @@ -306,7 +308,8 @@ Split: allMarkets vs whitelistedMarkets
2. Enrich user-specific balances / totalAssets via multicall where needed
3. Use narrow RPC fallback only when Monarch vault metadata is unavailable
4. Fetch live allocations from on-chain `allocation(capId)` reads
5. After vault writes, use shared bounded retry refreshes so Monarch indexing can catch up
5. Fetch detail-page native yield, deposits, and share price from Morpho Vault V2 history; fall back to archive RPC reads for deposits and share price
6. After vault writes, use shared bounded retry refreshes so Monarch indexing can catch up
```

---
Expand Down
20 changes: 15 additions & 5 deletions src/components/layout/header/HeaderMenuItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,20 @@ import { useRouter } from 'next/navigation';
import { useTheme } from 'next-themes';
import { FaRegMoon } from 'react-icons/fa';
import { LuSunMedium } from 'react-icons/lu';
import { RiBookLine, RiBriefcaseLine, RiDiscordFill, RiLineChartLine, RiSafeLine, RiSwapLine } from 'react-icons/ri';
import { RiBarChart2Line, RiBookLine, RiBriefcaseLine, RiDiscordFill, RiLineChartLine, RiSafeLine, RiSwapLine } from 'react-icons/ri';
import { useConnection } from 'wagmi';
import { DropdownMenuItem, DropdownMenuSeparator } from '@/components/ui/dropdown-menu';
import { useModal } from '@/hooks/useModal';
import { EXTERNAL_LINKS } from '@/utils/external';

type HeaderMenuItemsProps = {
includeAutovault?: boolean;
iconSide?: 'start' | 'end';
itemClassName?: string;
onSelect?: () => void;
};

export function HeaderMenuItems({ iconSide = 'end', itemClassName, onSelect }: HeaderMenuItemsProps) {
export function HeaderMenuItems({ includeAutovault = true, iconSide = 'end', itemClassName, onSelect }: HeaderMenuItemsProps) {
const router = useRouter();
const { open: openModal } = useModal();
const { address } = useConnection();
Expand Down Expand Up @@ -84,12 +85,21 @@ export function HeaderMenuItems({ iconSide = 'end', itemClassName, onSelect }: H
>
Positions
</DropdownMenuItem>
{includeAutovault && (
<DropdownMenuItem
{...iconProps(<RiSafeLine className="h-4 w-4" />)}
className={itemClassName}
onClick={() => handleNavigation('/autovault')}
>
Autovaults
</DropdownMenuItem>
)}
<DropdownMenuItem
{...iconProps(<RiSafeLine className="h-4 w-4" />)}
{...iconProps(<RiBarChart2Line className="h-4 w-4" />)}
className={itemClassName}
onClick={() => handleNavigation('/autovault')}
onClick={() => handleNavigation('/analysis')}
>
Autovaults
Analytics
</DropdownMenuItem>

<DropdownMenuSeparator />
Expand Down
8 changes: 4 additions & 4 deletions src/components/layout/header/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,10 @@ export function Navbar() {
</NavbarLink>
<span className="mx-1 h-4 border-l border-dashed border-[var(--grid-cell-muted)]" />
<NavbarLink
href="/analysis"
matchKey="/analysis"
href="/autovault"
matchKey="/autovault"
>
Analytics
Autovaults
</NavbarLink>
<span className="mx-1 h-4 border-l border-dashed border-[var(--grid-cell-muted)]" />

Expand All @@ -129,7 +129,7 @@ export function Navbar() {
align="end"
className="min-w-[180px]"
>
<HeaderMenuItems />
<HeaderMenuItems includeAutovault={false} />
</DropdownMenuContent>
</DropdownMenu>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/data-sources/monarch-api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export {
type MonarchUserPositionState,
} from './positions';
export { fetchMonarchUserTransactions } from './user-transactions';
export { fetchCompletedPositionDailyFlows } from './position-daily-flows';
export { fetchCompletedPositionDailyFlows, fetchPositionDailyFlows } from './position-daily-flows';
export {
fetchMonarchMarketBorrowers,
fetchMonarchMarketBorrows,
Expand Down
18 changes: 14 additions & 4 deletions src/data-sources/monarch-api/position-daily-flows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,30 @@ const MAX_PAGES = 50;

const getDayStart = (timestamp: number): number => Math.floor(timestamp / SECONDS_PER_DAY) * SECONDS_PER_DAY;

export async function fetchCompletedPositionDailyFlows({
export async function fetchPositionDailyFlows({
userAddress,
chainId,
marketIds,
startTimestamp,
endTimestamp,
includeCurrentBucket = false,
}: {
userAddress: string;
chainId: number;
marketIds: string[];
startTimestamp: number;
endTimestamp: number;
includeCurrentBucket?: boolean;
}): Promise<PositionDailyFlow[]> {
const startBucket = getDayStart(startTimestamp);
// The current UTC bucket is mutable. Excluding it keeps every paged row
// stable; the chart uses the live on-chain position for its final point.
const endBucket = Math.min(getDayStart(endTimestamp), getDayStart(Math.floor(Date.now() / 1000)));
const currentBucket = getDayStart(Math.floor(Date.now() / 1000));
const shouldIncludeCurrentBucket = includeCurrentBucket && endTimestamp >= currentBucket;
// All-time history excludes the mutable current bucket so pagination stays
// stable. Bounded charts include it to avoid a stale penultimate point; the
// live on-chain position remains the final source of truth.
const lastBucket = shouldIncludeCurrentBucket ? currentBucket + SECONDS_PER_DAY : currentBucket;
const requestedEndBucket = getDayStart(endTimestamp) + (shouldIncludeCurrentBucket ? SECONDS_PER_DAY : 0);
const endBucket = Math.min(requestedEndBucket, lastBucket);
if (marketIds.length === 0 || endBucket <= startBucket) {
return [];
}
Expand Down Expand Up @@ -87,3 +94,6 @@ export async function fetchCompletedPositionDailyFlows({

throw new Error(`Position daily flow history exceeded the safe pagination limit (${MAX_PAGES * PAGE_SIZE} rows)`);
}

export const fetchCompletedPositionDailyFlows = (options: Omit<Parameters<typeof fetchPositionDailyFlows>[0], 'includeCurrentBucket'>) =>
fetchPositionDailyFlows(options);
2 changes: 1 addition & 1 deletion src/data-sources/monarch-api/user-transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { dedupeUserTransactions, sortUserTransactions } from '@/utils/user-trans
import { monarchGraphqlFetcher } from './fetchers';

const MAX_PAGES = 50;
const MONARCH_USER_TRANSACTIONS_BATCH_SIZE = 500;
const MONARCH_USER_TRANSACTIONS_BATCH_SIZE = 1_000;
const MONARCH_USER_TRANSACTIONS_TIMEOUT_MS = 15_000;

type MonarchUserActivityRow = {
Expand Down
89 changes: 89 additions & 0 deletions src/data-sources/morpho-api/vault-history.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { supportsMorphoApi } from '@/config/dataSources';
import { vaultV2HistoryQuery } from '@/graphql/vault-queries';
import type { SupportedNetworks } from '@/utils/networks';
import type { TimeseriesOptions } from '@/utils/types';
import { morphoGraphqlFetcher } from './fetchers';

export type MorphoVaultHistoryPoint = {
timestamp: number;
value: number;
};

export type MorphoVaultHistory = {
nativeApy: MorphoVaultHistoryPoint[];
sharePrice: MorphoVaultHistoryPoint[];
totalAssets: MorphoVaultHistoryPoint[];
};

type RawHistoryPoint = {
x: unknown;
y: unknown;
};

type VaultV2HistoryResponse = {
data?: {
vaultV2ByAddress?: {
historicalState?: {
avgApy?: RawHistoryPoint[] | null;
sharePrice?: RawHistoryPoint[] | null;
totalAssets?: RawHistoryPoint[] | null;
} | null;
} | null;
};
};

function normalizePoints(points: RawHistoryPoint[] | null | undefined): MorphoVaultHistoryPoint[] {
return (points ?? [])
.map((point) => {
if (point.x === null || point.x === undefined || point.y === null || point.y === undefined) {
return null;
}

const timestamp = Number(point.x);
const value = Number(point.y);

if (!Number.isFinite(timestamp) || !Number.isFinite(value)) {
return null;
}

return { timestamp, value };
})
.filter((point): point is MorphoVaultHistoryPoint => point !== null)
.sort((left, right) => left.timestamp - right.timestamp);
}

export async function fetchMorphoVaultV2History({
vaultAddress,
chainId,
options,
}: {
vaultAddress: string;
chainId: SupportedNetworks;
options: TimeseriesOptions;
}): Promise<MorphoVaultHistory | null> {
if (!supportsMorphoApi(chainId)) {
return null;
}

try {
const response = await morphoGraphqlFetcher<VaultV2HistoryResponse>(
vaultV2HistoryQuery,
{ address: vaultAddress, chainId, options },
{ timeoutMs: 8000 },
);
const history = response?.data?.vaultV2ByAddress?.historicalState;

if (!history) {
return null;
}

return {
nativeApy: normalizePoints(history.avgApy),
sharePrice: normalizePoints(history.sharePrice),
totalAssets: normalizePoints(history.totalAssets),
};
} catch (error) {
console.warn('[vaultHistory] Morpho API vault history unavailable:', error);
return null;
}
}
90 changes: 0 additions & 90 deletions src/data-sources/morpho-api/vault-share-price-history.ts

This file was deleted.

Loading