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
91 changes: 82 additions & 9 deletions src/contexts/VaultRegistryContext.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,111 @@
'use client';

import { createContext, useContext, useMemo, type ReactNode } from 'react';
import { createContext, useCallback, useContext, useMemo, type ReactNode } from 'react';
import type { MorphoVault } from '@/data-sources/morpho-api/vaults';
import { useAllMorphoVaultsQuery } from '@/hooks/queries/useAllMorphoVaultsQuery';
import { useVaultAdapterAliasesQuery } from '@/hooks/queries/useVaultAdapterAliasesQuery';
import type { Address } from 'viem';

type AddressLabel = {
displayName: string;
kind: 'vault' | 'vault-adapter';
vaultAddress: string;
adapterType?: string;
};

type AdapterAddressAlias = {
adapterType: string;
vaultAddress: string;
vaultName: string;
};

type VaultRegistryContextType = {
vaults: MorphoVault[];
loading: boolean;
error: Error | null;
getVaultByAddress: (address: Address, chainId?: number) => MorphoVault | undefined;
getAddressLabel: (address: Address, chainId?: number) => AddressLabel | undefined;
};

const VaultRegistryContext = createContext<VaultRegistryContextType | undefined>(undefined);

const getAddressKey = (address: string, chainId: number) => `${chainId}:${address.toLowerCase()}`;

export function VaultRegistryProvider({ children }: { children: ReactNode }) {
const { data: vaults = [], isLoading: loading, error } = useAllMorphoVaultsQuery();
const { data: vaults = [], isLoading: vaultsLoading, error: vaultsError } = useAllMorphoVaultsQuery();
const { data: adapterAliases = [], isLoading: adapterAliasesLoading, error: adapterAliasesError } = useVaultAdapterAliasesQuery();

const vaultsByScopedAddress = useMemo(() => {
const lookup = new Map<string, MorphoVault>();
for (const vault of vaults) {
lookup.set(getAddressKey(vault.address, vault.chainId), vault);
}
return lookup;
}, [vaults]);

const adapterAliasesByScopedAddress = useMemo(() => {
const lookup = new Map<string, AdapterAddressAlias>();
for (const adapterAlias of adapterAliases) {
lookup.set(getAddressKey(adapterAlias.address, adapterAlias.chainId), {
vaultAddress: adapterAlias.vaultAddress,
vaultName: adapterAlias.vaultName,
adapterType: adapterAlias.adapterType,
});
}
return lookup;
}, [adapterAliases]);

const getVaultByAddress = useMemo(
() => (address: Address, chainId?: number) => {
const getVaultByAddress = useCallback(
(address: Address, chainId?: number) => {
const normalizedAddress = address.toLowerCase();
return vaults.find((v) => v.address.toLowerCase() === normalizedAddress && (!chainId || v.chainId === chainId));
if (chainId) {
return vaultsByScopedAddress.get(getAddressKey(normalizedAddress, chainId));
}

return vaults.find((vault) => vault.address.toLowerCase() === normalizedAddress);
},
[vaults, vaultsByScopedAddress],
);

const getAddressLabel = useCallback(
(address: Address, chainId?: number): AddressLabel | undefined => {
const vault = getVaultByAddress(address, chainId);
if (vault?.name) {
return {
displayName: vault.name,
kind: 'vault',
vaultAddress: vault.address.toLowerCase(),
};
}

if (!chainId) {
return undefined;
}

const adapterAlias = adapterAliasesByScopedAddress.get(getAddressKey(address, chainId));
if (!adapterAlias) {
return undefined;
}

return {
displayName: `${adapterAlias.vaultName} (adapter)`,
kind: 'vault-adapter',
vaultAddress: adapterAlias.vaultAddress,
adapterType: adapterAlias.adapterType,
};
},
[vaults],
[adapterAliasesByScopedAddress, getVaultByAddress],
);

const value = useMemo(
() => ({
vaults,
loading,
error,
loading: vaultsLoading || adapterAliasesLoading,
error: vaultsError ?? adapterAliasesError,
getVaultByAddress,
getAddressLabel,
}),
[vaults, loading, error, getVaultByAddress],
[vaults, vaultsLoading, adapterAliasesLoading, vaultsError, adapterAliasesError, getVaultByAddress, getAddressLabel],
);

return <VaultRegistryContext.Provider value={value}>{children}</VaultRegistryContext.Provider>;
Expand Down
2 changes: 2 additions & 0 deletions src/data-sources/monarch-api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ export {
type TimeRange,
} from './transactions';
export {
fetchMonarchVaultAdapterAliases,
fetchMonarchVaultDetails,
fetchUserVaultV2DetailsAllNetworks,
type VaultAdapterAlias,
type VaultAdapterDetails,
type UserVaultV2,
type VaultV2Cap,
Expand Down
113 changes: 113 additions & 0 deletions src/data-sources/monarch-api/vaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ export type VaultAdapterDetails = {
factoryAddress: string;
};

export type VaultAdapterAlias = {
adapterType: string;
address: string;
chainId: SupportedNetworks;
vaultAddress: string;
vaultName: string;
};

export const MORPHO_MARKET_ADAPTER_TYPES = ['MorphoMarketV1AdapterV2', 'MorphoMarketV1Adapter'] as const;

export const isRecognizedMorphoMarketAdapterType = (adapterType: string | null | undefined): boolean => {
Expand Down Expand Up @@ -93,6 +101,15 @@ type MonarchAdapterRecord = {
vaultAddress: string;
};

type MonarchAdapterAliasRecord = MonarchAdapterRecord & {
vault: {
vaultAddress: string;
chainId: number;
name: string | null;
symbol: string | null;
} | null;
};

type MonarchVaultsResponse = {
data?: {
Vault?: MonarchVault[];
Expand All @@ -112,6 +129,15 @@ type MonarchAdapterLookupResponse = {
};
};

type MonarchAdapterAliasesResponse = {
data?: {
Adapter?: MonarchAdapterAliasRecord[];
};
};

const MONARCH_ADAPTER_ALIAS_PAGE_SIZE = 1000;
const MONARCH_ADAPTER_ALIAS_MAX_PAGES = 20;

const MONARCH_VAULT_FIELDS = `
id
vaultAddress
Expand Down Expand Up @@ -230,6 +256,28 @@ const adaptersByAddressQuery = `
}
`;

const adapterAliasesQuery = `
query MonarchVaultAdapterAliases($adapterTypes: [String!]!, $limit: Int!, $offset: Int!) {
Adapter(
where: { adapterType: { _in: $adapterTypes } }
order_by: [{ createdAt: desc }]
limit: $limit
offset: $offset
) {
adapterAddress
adapterType
chainId
vaultAddress
vault {
vaultAddress
chainId
name
symbol
}
}
}
`;

export const fetchUserVaultV2DetailsAllNetworks = async (owner: string): Promise<UserVaultV2[]> => {
const response = await monarchGraphqlFetcher<MonarchVaultsResponse>(userVaultsQuery, {
owner: owner.toLowerCase(),
Expand Down Expand Up @@ -285,3 +333,68 @@ export const fetchMonarchAdaptersByAddress = async (
return true;
});
};

const transformAdapterAliasRecord = (adapter: MonarchAdapterAliasRecord): VaultAdapterAlias | null => {
if (!isRecognizedMorphoMarketAdapterType(adapter.adapterType)) {
return null;
}

const chainId = toSupportedNetwork(adapter.chainId);
if (!chainId || (adapter.vault && adapter.vault.chainId !== adapter.chainId)) {
return null;
}

const vaultAddress = normalizeAddress(adapter.vault?.vaultAddress ?? adapter.vaultAddress);
const vaultName = adapter.vault?.name?.trim() || adapter.vault?.symbol?.trim() || '';
if (!vaultName) {
return null;
}

return {
address: normalizeAddress(adapter.adapterAddress),
adapterType: adapter.adapterType,
chainId,
vaultAddress,
vaultName,
};
};

export const fetchMonarchVaultAdapterAliases = async (): Promise<VaultAdapterAlias[]> => {
try {
const aliases: VaultAdapterAlias[] = [];
const seenKeys = new Set<string>();

for (let page = 0; page < MONARCH_ADAPTER_ALIAS_MAX_PAGES; page++) {
const response = await monarchGraphqlFetcher<MonarchAdapterAliasesResponse>(adapterAliasesQuery, {
adapterTypes: MORPHO_MARKET_ADAPTER_TYPES,
limit: MONARCH_ADAPTER_ALIAS_PAGE_SIZE,
offset: page * MONARCH_ADAPTER_ALIAS_PAGE_SIZE,
});

const records = response.data?.Adapter ?? [];
for (const record of records) {
const alias = transformAdapterAliasRecord(record);
if (!alias) {
continue;
}

const key = `${alias.chainId}:${alias.address}`;
if (seenKeys.has(key)) {
continue;
}

seenKeys.add(key);
aliases.push(alias);
}

if (records.length < MONARCH_ADAPTER_ALIAS_PAGE_SIZE) {
break;
}
}

return aliases;
} catch (error) {
console.warn('Error fetching Monarch vault adapter aliases:', error);
return [];
}
Comment on lines +362 to +399
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 | 🟠 Major | ⚡ Quick win

Don't hide Monarch alias fetch failures behind an empty list.

Returning [] from the catch block makes the alias source look healthy even when the query is broken, so consumers silently fall back to short addresses and the registry never sees an error. Let the request reject after logging so React Query can surface the failure.

Small fix
 } catch (error) {
   console.warn('Error fetching Monarch vault adapter aliases:', error);
-  return [];
+  throw error;
 }
As per coding guidelines, handle errors appropriately with try-catch blocks and normalize remote errors into typed app errors.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/data-sources/monarch-api/vaults.ts` around lines 362 - 399, The catch in
fetchMonarchVaultAdapterAliases currently swallows failures by returning [] —
instead log the error (keep the console.warn) and then rethrow a
normalized/typed error so callers (e.g., React Query) can surface the failure;
specifically, replace the "return []" in fetchMonarchVaultAdapterAliases's catch
with throwing a new application-level error (wrapping the original error from
monarchGraphqlFetcher) or rethrowing the original after normalization, ensuring
the thrown error includes context like "Failed fetching Monarch adapter aliases"
and the original error details.

};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { useMemo, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import type { Address } from 'viem';
import { formatUnits } from 'viem';
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from 'recharts';
Expand Down Expand Up @@ -98,16 +98,19 @@ function BorrowersPieTooltip({

export function BorrowersPieChart({ chainId, market, oraclePrice }: BorrowersPieChartProps) {
const { data: borrowers, isLoading, totalCount } = useAllMarketBorrowers(market.uniqueKey, chainId, market.state);
const { getVaultByAddress } = useVaultRegistry();
const { getAddressLabel } = useVaultRegistry();
const [expandedOther, setExpandedOther] = useState(false);
const chartColors = useChartColors();

// Helper to get display name for an address (vault name or shortened address)
const getDisplayName = (address: string): string => {
const vault = getVaultByAddress(address as Address, chainId);
if (vault?.name) return vault.name;
return getSlicedAddress(address as `0x${string}`);
};
// Helper to get display name for an address (vault/adapter label or shortened address)
const getDisplayName = useCallback(
(address: string): string => {
const addressLabel = getAddressLabel(address as Address, chainId);
if (addressLabel?.displayName) return addressLabel.displayName;
return getSlicedAddress(address as `0x${string}`);
},
[getAddressLabel, chainId],
);

const pieData = useMemo(() => {
if (!borrowers || borrowers.length === 0) return [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export function SupplierPositionsChart({ marketId, chainId, market }: SupplierPo
const selectedTimeframe = useMarketDetailChartState((s) => s.selectedTimeframe);
const selectedTimeRange = useMarketDetailChartState((s) => s.selectedTimeRange);
const chartColors = useChartColors();
const { getVaultByAddress } = useVaultRegistry();
const { getAddressLabel } = useVaultRegistry();

const { data: suppliers, isLoading: suppliersLoading } = useAllMarketSuppliers(market.uniqueKey, chainId);

Expand Down Expand Up @@ -120,11 +120,11 @@ export function SupplierPositionsChart({ marketId, chainId, market }: SupplierPo
// Get display name for a supplier address
const getDisplayName = useCallback(
(address: string): string => {
const vault = getVaultByAddress(address as Address, chainId);
if (vault?.name) return vault.name;
const addressLabel = getAddressLabel(address as Address, chainId);
if (addressLabel?.displayName) return addressLabel.displayName;
return getSlicedAddress(address as `0x${string}`);
},
[getVaultByAddress, chainId],
[getAddressLabel, chainId],
);

const handleLegendClick = useCallback(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { useMemo, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import type { Address } from 'viem';
import { formatUnits } from 'viem';
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from 'recharts';
Expand Down Expand Up @@ -73,16 +73,19 @@ function SuppliersPieTooltip({

export function SuppliersPieChart({ chainId, market }: SuppliersPieChartProps) {
const { data: suppliers, isLoading, totalCount } = useAllMarketSuppliers(market.uniqueKey, chainId);
const { getVaultByAddress } = useVaultRegistry();
const { getAddressLabel } = useVaultRegistry();
const [expandedOther, setExpandedOther] = useState(false);
const chartColors = useChartColors();

// Helper to get display name for an address (vault name or shortened address)
const getDisplayName = (address: string): string => {
const vault = getVaultByAddress(address as Address, chainId);
if (vault?.name) return vault.name;
return getSlicedAddress(address as `0x${string}`);
};
// Helper to get display name for an address (vault/adapter label or shortened address)
const getDisplayName = useCallback(
(address: string): string => {
const addressLabel = getAddressLabel(address as Address, chainId);
if (addressLabel?.displayName) return addressLabel.displayName;
return getSlicedAddress(address as `0x${string}`);
},
[getAddressLabel, chainId],
);

const pieData = useMemo(() => {
if (!suppliers || suppliers.length === 0) return [];
Expand Down
16 changes: 16 additions & 0 deletions src/hooks/queries/useVaultAdapterAliasesQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useQuery } from '@tanstack/react-query';
import { fetchMonarchVaultAdapterAliases, type VaultAdapterAlias } from '@/data-sources/monarch-api/vaults';

const VAULT_ADAPTER_ALIASES_STALE_TIME_MS = 10 * 60 * 1000;
const VAULT_ADAPTER_ALIASES_GC_TIME_MS = 30 * 60 * 1000;

export const useVaultAdapterAliasesQuery = () => {
return useQuery<VaultAdapterAlias[], Error>({
queryKey: ['monarch-vault-adapter-aliases'],
queryFn: fetchMonarchVaultAdapterAliases,
staleTime: VAULT_ADAPTER_ALIASES_STALE_TIME_MS,
gcTime: VAULT_ADAPTER_ALIASES_GC_TIME_MS,
refetchOnWindowFocus: false,
retry: false,
});
};
Loading