From ecf0433cb5361fff9b699049668e88b3fc1ab711 Mon Sep 17 00:00:00 2001
From: antoncoding
Date: Wed, 23 Apr 2025 15:11:01 +0800
Subject: [PATCH 01/20] chore: add subgraph alternative for supplies and
borrows table
---
.../[marketid]/components/BorrowsTable.tsx | 20 +--
.../[marketid]/components/SuppliesTable.tsx | 20 +--
src/config/dataSources.ts | 4 +-
src/data-sources/morpho-api/market-borrows.ts | 64 +++++++++
.../morpho-api/market-supplies.ts | 69 +++++++++
src/data-sources/subgraph/market-borrows.ts | 84 +++++++++++
src/data-sources/subgraph/market-supplies.ts | 98 +++++++++++++
src/graphql/morpho-subgraph-queries.ts | 66 +++++++++
src/hooks/useMarketBorrows.ts | 124 +++++++---------
src/hooks/useMarketSupplies.ts | 132 ++++++++----------
src/utils/types.ts | 9 ++
11 files changed, 529 insertions(+), 161 deletions(-)
create mode 100644 src/data-sources/morpho-api/market-borrows.ts
create mode 100644 src/data-sources/morpho-api/market-supplies.ts
create mode 100644 src/data-sources/subgraph/market-borrows.ts
create mode 100644 src/data-sources/subgraph/market-supplies.ts
diff --git a/app/market/[chainId]/[marketid]/components/BorrowsTable.tsx b/app/market/[chainId]/[marketid]/components/BorrowsTable.tsx
index b2288d1f..073eaab4 100644
--- a/app/market/[chainId]/[marketid]/components/BorrowsTable.tsx
+++ b/app/market/[chainId]/[marketid]/components/BorrowsTable.tsx
@@ -8,7 +8,7 @@ import { formatUnits } from 'viem';
import AccountWithAvatar from '@/components/Account/AccountWithAvatar';
import { Badge } from '@/components/common/Badge';
import { TokenIcon } from '@/components/TokenIcon';
-import useMarketBorrows from '@/hooks/useMarketBorrows';
+import { useMarketBorrows } from '@/hooks/useMarketBorrows';
import { getExplorerURL, getExplorerTxURL } from '@/utils/external';
import { Market } from '@/utils/types';
@@ -26,7 +26,11 @@ export function BorrowsTable({ chainId, market }: BorrowsTableProps) {
const [currentPage, setCurrentPage] = useState(1);
const pageSize = 8;
- const { borrows, loading, error } = useMarketBorrows(market?.uniqueKey);
+ const { data: borrows, isLoading, error } = useMarketBorrows(
+ market?.uniqueKey,
+ market.loanAsset.id,
+ chainId,
+ );
const totalPages = Math.ceil((borrows || []).length / pageSize);
@@ -42,7 +46,7 @@ export function BorrowsTable({ chainId, market }: BorrowsTableProps) {
const tableKey = `borrows-table-${currentPage}`;
if (error) {
- return Error loading borrows: {error}
;
+ return Error loading borrows: {error instanceof Error ? error.message : 'Unknown error'}
;
}
return (
@@ -82,19 +86,19 @@ export function BorrowsTable({ chainId, market }: BorrowsTableProps) {
{paginatedBorrows.map((borrow) => (
-
+
@@ -104,7 +108,7 @@ export function BorrowsTable({ chainId, market }: BorrowsTableProps) {
- {formatUnits(BigInt(borrow.data.assets), market.loanAsset.decimals)}
+ {formatUnits(BigInt(borrow.amount), market.loanAsset.decimals)}
{market?.loanAsset?.symbol && (
Error loading supplies: {error}
;
- }
-
return (
Supply & Withdraw
@@ -82,19 +82,19 @@ export function SuppliesTable({ chainId, market }: SuppliesTableProps) {
{paginatedSupplies.map((supply) => (
-
+
@@ -104,7 +104,7 @@ export function SuppliesTable({ chainId, market }: SuppliesTableProps) {
- {formatUnits(BigInt(supply.data.assets), market.loanAsset.decimals)}
+ {formatUnits(BigInt(supply.amount), market.loanAsset.decimals)}
{market?.loanAsset?.symbol && (
{
switch (network) {
// case SupportedNetworks.Mainnet:
- // case SupportedNetworks.Base:
- // return 'subgraph';
+ case SupportedNetworks.Base:
+ return 'subgraph';
default:
return 'morpho'; // Default to Morpho API
}
diff --git a/src/data-sources/morpho-api/market-borrows.ts b/src/data-sources/morpho-api/market-borrows.ts
new file mode 100644
index 00000000..32b9919d
--- /dev/null
+++ b/src/data-sources/morpho-api/market-borrows.ts
@@ -0,0 +1,64 @@
+import { marketBorrowsQuery } from '@/graphql/morpho-api-queries';
+// Import the shared type from its new location
+import { MarketActivityTransaction } from '@/utils/types';
+import { morphoGraphqlFetcher } from './fetchers';
+
+// Type specifically for the raw Morpho API response structure for borrows/repays
+type MorphoAPIBorrowsResponse = {
+ data?: {
+ transactions?: {
+ items?: Array<{
+ type: 'MarketBorrow' | 'MarketRepay'; // Specific types for this query
+ hash: string;
+ timestamp: number;
+ data: {
+ assets: string;
+ shares: string; // Present but ignored in unified type
+ };
+ user: {
+ address: string;
+ };
+ }>;
+ };
+ };
+};
+
+/**
+ * Fetches market borrow/repay activities from the Morpho Blue API.
+ * Uses the shared Morpho API fetcher.
+ * @param marketId The unique key or ID of the market.
+ * @returns A promise resolving to an array of unified MarketActivityTransaction objects.
+ */
+export const fetchMorphoMarketBorrows = async (
+ marketId: string,
+): Promise => {
+ const variables = {
+ uniqueKey: marketId,
+ first: 1000,
+ skip: 0,
+ };
+
+ try {
+ const result = await morphoGraphqlFetcher(
+ marketBorrowsQuery,
+ variables,
+ );
+
+ const items = result.data?.transactions?.items ?? [];
+
+ // Map to unified type (reusing MarketActivityTransaction)
+ return items.map((item) => ({
+ type: item.type, // Directly use 'MarketBorrow' or 'MarketRepay'
+ hash: item.hash,
+ timestamp: item.timestamp,
+ amount: item.data.assets, // Map 'assets' to 'amount'
+ userAddress: item.user.address,
+ }));
+ } catch (error) {
+ console.error(`Error fetching or processing Morpho API market borrows for ${marketId}:`, error);
+ if (error instanceof Error) {
+ throw error;
+ }
+ throw new Error('An unknown error occurred while fetching Morpho API market borrows');
+ }
+};
\ No newline at end of file
diff --git a/src/data-sources/morpho-api/market-supplies.ts b/src/data-sources/morpho-api/market-supplies.ts
new file mode 100644
index 00000000..5384c677
--- /dev/null
+++ b/src/data-sources/morpho-api/market-supplies.ts
@@ -0,0 +1,69 @@
+import { marketSuppliesQuery } from '@/graphql/morpho-api-queries';
+import { MarketActivityTransaction } from '@/utils/types';
+import { morphoGraphqlFetcher } from './fetchers'; // Import shared fetcher
+
+// Type specifically for the raw Morpho API response structure within this module
+type MorphoAPISuppliesResponse = {
+ data?: { // Mark data as optional to align with fetcher's generic handling
+ transactions?: {
+ items?: Array<{
+ type: 'MarketSupply' | 'MarketWithdraw';
+ hash: string;
+ timestamp: number;
+ data: {
+ assets: string;
+ shares: string;
+ };
+ user: {
+ address: string;
+ };
+ }>;
+ };
+ };
+ // Error handling is now done by the fetcher
+};
+
+/**
+ * Fetches market supply/withdraw activities from the Morpho Blue API.
+ * Uses the shared Morpho API fetcher.
+ * @param marketId The unique key or ID of the market.
+ * @returns A promise resolving to an array of unified MarketActivityTransaction objects.
+ */
+export const fetchMorphoMarketSupplies = async (
+ marketId: string,
+): Promise => {
+ const variables = {
+ uniqueKey: marketId, // Ensure this matches the variable name in the query
+ first: 1000,
+ skip: 0,
+ };
+
+ try {
+ // Use the shared fetcher
+ const result = await morphoGraphqlFetcher(
+ marketSuppliesQuery,
+ variables,
+ );
+
+ // Fetcher handles network and basic GraphQL errors
+ const items = result.data?.transactions?.items ?? [];
+
+ // Map to unified type
+ return items.map((item) => ({
+ type: item.type,
+ hash: item.hash,
+ timestamp: item.timestamp,
+ amount: item.data.assets,
+ userAddress: item.user.address,
+ // Note: 'shares' from Morpho API is omitted in the unified type
+ }));
+ } catch (error) {
+ // Catch errors from the fetcher or during processing
+ console.error(`Error fetching or processing Morpho API market supplies for ${marketId}:`, error);
+ // Re-throw the error to be handled by the calling hook
+ if (error instanceof Error) {
+ throw error;
+ }
+ throw new Error('An unknown error occurred while fetching Morpho API market supplies');
+ }
+};
\ No newline at end of file
diff --git a/src/data-sources/subgraph/market-borrows.ts b/src/data-sources/subgraph/market-borrows.ts
new file mode 100644
index 00000000..7eecafb7
--- /dev/null
+++ b/src/data-sources/subgraph/market-borrows.ts
@@ -0,0 +1,84 @@
+import { marketBorrowsRepaysQuery } from '@/graphql/morpho-subgraph-queries';
+import { MarketActivityTransaction } from '@/utils/types'; // Import shared type
+import { SupportedNetworks } from '@/utils/networks';
+import { getSubgraphUrl } from '@/utils/subgraph-urls';
+import { subgraphGraphqlFetcher } from './fetchers';
+
+// Types specific to the Subgraph response for this query
+type SubgraphBorrowRepayItem = {
+ amount: string;
+ account: {
+ id: string;
+ };
+ timestamp: number | string;
+ hash: string;
+};
+
+type SubgraphBorrowsRepaysResponse = {
+ data?: {
+ borrows?: SubgraphBorrowRepayItem[];
+ repays?: SubgraphBorrowRepayItem[];
+ };
+};
+
+/**
+ * Fetches market borrow/repay activities from the Subgraph.
+ * @param marketId The ID of the market.
+ * @param loanAssetId The address of the loan asset.
+ * @param network The blockchain network.
+ * @returns A promise resolving to an array of unified MarketActivityTransaction objects.
+ */
+export const fetchSubgraphMarketBorrows = async (
+ marketId: string,
+ loanAssetId: string,
+ network: SupportedNetworks,
+): Promise => {
+ const subgraphUrl = getSubgraphUrl(network);
+ if (!subgraphUrl) {
+ console.error(`No Subgraph URL configured for network: ${network}`);
+ throw new Error(`Subgraph URL not available for network ${network}`);
+ }
+
+ const variables = { marketId, loanAssetId };
+
+ try {
+ const result = await subgraphGraphqlFetcher(
+ subgraphUrl,
+ marketBorrowsRepaysQuery,
+ variables,
+ );
+
+ const borrows = result.data?.borrows ?? [];
+ const repays = result.data?.repays ?? [];
+
+ // Map borrows to the unified type
+ const mappedBorrows: MarketActivityTransaction[] = borrows.map((b) => ({
+ type: 'MarketBorrow',
+ hash: b.hash,
+ timestamp: typeof b.timestamp === 'string' ? parseInt(b.timestamp, 10) : b.timestamp,
+ amount: b.amount,
+ userAddress: b.account.id,
+ }));
+
+ // Map repays to the unified type
+ const mappedRepays: MarketActivityTransaction[] = repays.map((r) => ({
+ type: 'MarketRepay',
+ hash: r.hash,
+ timestamp: typeof r.timestamp === 'string' ? parseInt(r.timestamp, 10) : r.timestamp,
+ amount: r.amount,
+ userAddress: r.account.id,
+ }));
+
+ // Combine and sort by timestamp descending
+ const combined = [...mappedBorrows, ...mappedRepays];
+ combined.sort((a, b) => b.timestamp - a.timestamp);
+
+ return combined;
+ } catch (error) {
+ console.error(`Error fetching or processing Subgraph market borrows for ${marketId}:`, error);
+ if (error instanceof Error) {
+ throw error;
+ }
+ throw new Error('An unknown error occurred while fetching subgraph market borrows');
+ }
+};
\ No newline at end of file
diff --git a/src/data-sources/subgraph/market-supplies.ts b/src/data-sources/subgraph/market-supplies.ts
new file mode 100644
index 00000000..8e9c1334
--- /dev/null
+++ b/src/data-sources/subgraph/market-supplies.ts
@@ -0,0 +1,98 @@
+import { marketDepositsWithdrawsQuery } from '@/graphql/morpho-subgraph-queries';
+import { MarketActivityTransaction } from '@/utils/types';
+import { SupportedNetworks } from '@/utils/networks';
+import { getSubgraphUrl } from '@/utils/subgraph-urls'; // Import shared utility
+import { subgraphGraphqlFetcher } from './fetchers'; // Import shared fetcher
+
+// Types specific to the Subgraph response for this query
+type SubgraphSupplyWithdrawItem = {
+ amount: string;
+ account: {
+ id: string;
+ };
+ timestamp: number | string; // Allow string timestamp from subgraph
+ hash: string;
+};
+
+type SubgraphSuppliesWithdrawsResponse = {
+ data?: {
+ deposits?: SubgraphSupplyWithdrawItem[];
+ withdraws?: SubgraphSupplyWithdrawItem[];
+ };
+ // Error handling is now done by the fetcher
+};
+
+/**
+ * Fetches market supply/withdraw activities (deposits/withdraws of loan asset) from the Subgraph.
+ * Uses the shared subgraph fetcher and URL utility.
+ * @param marketId The ID of the market.
+ * @param loanAssetId The address of the loan asset.
+ * @param network The blockchain network.
+ * @returns A promise resolving to an array of unified MarketActivityTransaction objects.
+ */
+export const fetchSubgraphMarketSupplies = async (
+ marketId: string,
+ loanAssetId: string,
+ network: SupportedNetworks,
+): Promise => {
+ const subgraphUrl = getSubgraphUrl(network);
+ if (!subgraphUrl) {
+ // Error handling for missing URL remains important
+ console.error(`No Subgraph URL configured for network: ${network}`);
+ throw new Error(`Subgraph URL not available for network ${network}`);
+ }
+
+ const variables = {
+ marketId, // Ensure these match the types expected by the Subgraph query (e.g., Bytes)
+ loanAssetId,
+ };
+
+ try {
+ // Use the shared fetcher
+ const result = await subgraphGraphqlFetcher(
+ subgraphUrl,
+ marketDepositsWithdrawsQuery,
+ variables,
+ );
+
+ // Fetcher handles network and basic GraphQL errors, proceed with data processing
+ const deposits = result.data?.deposits ?? [];
+ const withdraws = result.data?.withdraws ?? [];
+
+ // Map deposits and withdraws to the unified type
+ const mappedDeposits: MarketActivityTransaction[] = deposits.map((d) => ({
+ type: 'MarketSupply',
+ hash: d.hash,
+ // Ensure timestamp is treated as a number
+ timestamp: typeof d.timestamp === 'string' ? parseInt(d.timestamp, 10) : d.timestamp,
+ amount: d.amount,
+ userAddress: d.account.id,
+ }));
+
+ const mappedWithdraws: MarketActivityTransaction[] = withdraws.map((w) => ({
+ type: 'MarketWithdraw',
+ hash: w.hash,
+ timestamp: typeof w.timestamp === 'string' ? parseInt(w.timestamp, 10) : w.timestamp,
+ amount: w.amount,
+ userAddress: w.account.id,
+ }));
+
+ // Combine and sort by timestamp descending (most recent first)
+ const combined = [...mappedDeposits, ...mappedWithdraws];
+
+ console.log('combined', combined.length)
+
+ combined.sort((a, b) => b.timestamp - a.timestamp);
+
+ return combined;
+ } catch (error) {
+ // Catch errors from the fetcher or during processing
+ console.error(`Error fetching or processing Subgraph market supplies for ${marketId}:`, error);
+ // Re-throw the error to be handled by the calling hook (useQuery)
+ // Ensuring the error object is an instance of Error
+ if (error instanceof Error) {
+ throw error;
+ }
+ throw new Error('An unknown error occurred while fetching subgraph market supplies');
+ }
+};
\ No newline at end of file
diff --git a/src/graphql/morpho-subgraph-queries.ts b/src/graphql/morpho-subgraph-queries.ts
index 98d0d332..f625a346 100644
--- a/src/graphql/morpho-subgraph-queries.ts
+++ b/src/graphql/morpho-subgraph-queries.ts
@@ -145,3 +145,69 @@ export const marketHourlySnapshotsQuery = `
${tokenFragment} # Ensure TokenFields fragment is included
`;
// --- End Added Section ---
+
+// --- Query for Market Supplies/Withdraws (Deposits/Withdraws of Loan Asset) ---
+export const marketDepositsWithdrawsQuery = `
+ query getMarketDepositsWithdraws($marketId: Bytes!, $loanAssetId: Bytes!) {
+ deposits(
+ first: 1000, # Subgraph max limit
+ orderBy: timestamp,
+ orderDirection: desc,
+ where: { market: $marketId, asset: $loanAssetId }
+ ) {
+ amount
+ account {
+ id
+ }
+ timestamp
+ hash
+ }
+ withdraws(
+ first: 1000, # Subgraph max limit
+ orderBy: timestamp,
+ orderDirection: desc,
+ where: { market: $marketId, asset: $loanAssetId }
+ ) {
+ amount
+ account {
+ id
+ }
+ timestamp
+ hash
+ }
+ }
+`;
+// --- End Query ---
+
+// --- Query for Market Borrows/Repays (Borrows/Repays of Loan Asset) ---
+export const marketBorrowsRepaysQuery = `
+ query getMarketBorrowsRepays($marketId: Bytes!, $loanAssetId: Bytes!) {
+ borrows(
+ first: 1000,
+ orderBy: timestamp,
+ orderDirection: desc,
+ where: { market: $marketId, asset: $loanAssetId }
+ ) {
+ amount
+ account {
+ id
+ }
+ timestamp
+ hash
+ }
+ repays(
+ first: 1000,
+ orderBy: timestamp,
+ orderDirection: desc,
+ where: { market: $marketId, asset: $loanAssetId }
+ ) {
+ amount
+ account {
+ id
+ }
+ timestamp
+ hash
+ }
+ }
+`;
+// --- End Query ---
diff --git a/src/hooks/useMarketBorrows.ts b/src/hooks/useMarketBorrows.ts
index f45279f0..c83d1e23 100644
--- a/src/hooks/useMarketBorrows.ts
+++ b/src/hooks/useMarketBorrows.ts
@@ -1,86 +1,68 @@
-import { useState, useEffect, useCallback } from 'react';
-import { marketBorrowsQuery } from '@/graphql/morpho-api-queries';
-import { URLS } from '@/utils/urls';
-
-export type MarketBorrowTransaction = {
- type: 'MarketBorrow' | 'MarketRepay';
- hash: string;
- timestamp: number;
- data: {
- assets: string;
- shares: string;
- };
- user: {
- address: string;
- };
-};
+import { useQuery } from '@tanstack/react-query';
+import { getMarketDataSource } from '@/config/dataSources';
+import { fetchMorphoMarketBorrows } from '@/data-sources/morpho-api/market-borrows';
+import { fetchSubgraphMarketBorrows } from '@/data-sources/subgraph/market-borrows';
+import { SupportedNetworks } from '@/utils/networks';
+import { MarketActivityTransaction } from '@/utils/types';
/**
- * Hook to fetch all borrow and repay activities for a specific market
- * @param marketUniqueKey The unique key of the market
- * @returns List of all borrow and repay transactions for the market
+ * Hook to fetch all borrow and repay activities for a specific market's loan asset,
+ * using the appropriate data source based on the network.
+ * @param marketId The ID or unique key of the market.
+ * @param loanAssetId The address of the loan asset for the market.
+ * @param network The blockchain network.
+ * @returns List of borrow and repay transactions for the market's loan asset.
*/
-const useMarketBorrows = (marketUniqueKey: string | undefined) => {
- const [borrows, setBorrows] = useState([]);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(null);
-
- const fetchBorrows = useCallback(async () => {
- if (!marketUniqueKey) {
- setBorrows([]);
- return;
- }
-
- setLoading(true);
- setError(null);
-
- try {
- const variables = {
- uniqueKey: marketUniqueKey,
- first: 1000, // Limit to 100 most recent transactions
- skip: 0,
- };
+export const useMarketBorrows = (
+ marketId: string | undefined,
+ loanAssetId: string | undefined,
+ network: SupportedNetworks | undefined,
+) => {
+ const queryKey = ['marketBorrows', marketId, loanAssetId, network];
- const response = await fetch(`${URLS.MORPHO_BLUE_API}`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- query: marketBorrowsQuery,
- variables,
- }),
- });
+ // Determine the data source
+ const dataSource = network ? getMarketDataSource(network) : null;
- if (!response.ok) {
- throw new Error('Failed to fetch market borrows');
+ const { data, isLoading, error, refetch } = useQuery({
+ queryKey: queryKey,
+ queryFn: async (): Promise => {
+ // Guard clauses
+ if (!marketId || !loanAssetId || !network || !dataSource) {
+ return null;
}
- const result = (await response.json()) as {
- data: { transactions: { items: MarketBorrowTransaction[] } };
- };
+ console.log(
+ `Fetching market borrows for market ${marketId} (loan asset ${loanAssetId}) on ${network} via ${dataSource}`,
+ );
- if (result.data?.transactions?.items) {
- setBorrows(result.data.transactions.items);
- } else {
- setBorrows([]);
+ try {
+ if (dataSource === 'morpho') {
+ // Morpho API might only need marketId for borrows
+ return await fetchMorphoMarketBorrows(marketId);
+ } else if (dataSource === 'subgraph') {
+ return await fetchSubgraphMarketBorrows(marketId, loanAssetId, network);
+ }
+ } catch (fetchError) {
+ console.error(`Failed to fetch market borrows via ${dataSource}:`, fetchError);
+ return null;
}
- } catch (err) {
- console.error('Error fetching market borrows:', err);
- setError(err instanceof Error ? err.message : 'Unknown error');
- } finally {
- setLoading(false);
- }
- }, [marketUniqueKey]);
- useEffect(() => {
- void fetchBorrows();
- }, [fetchBorrows]);
+ console.warn('Unknown market data source determined for borrows');
+ return null;
+ },
+ enabled: !!marketId && !!loanAssetId && !!network && !!dataSource,
+ staleTime: 1000 * 60 * 2, // 2 minutes
+ placeholderData: (previousData) => previousData ?? null,
+ retry: 1,
+ });
+ // Return react-query result structure
return {
- borrows,
- loading,
- error,
+ data: data,
+ isLoading: isLoading,
+ error: error,
+ refetch: refetch,
+ dataSource: dataSource,
};
};
diff --git a/src/hooks/useMarketSupplies.ts b/src/hooks/useMarketSupplies.ts
index 6fb21cd3..acfedd48 100644
--- a/src/hooks/useMarketSupplies.ts
+++ b/src/hooks/useMarketSupplies.ts
@@ -1,87 +1,79 @@
-import { useState, useEffect, useCallback } from 'react';
-import { marketSuppliesQuery } from '@/graphql/morpho-api-queries';
-import { URLS } from '@/utils/urls';
-
-export type MarketSupplyTransaction = {
- type: 'MarketSupply' | 'MarketWithdraw';
- hash: string;
- timestamp: number;
- data: {
- assets: string;
- shares: string;
- };
- user: {
- address: string;
- };
-};
+import { useQuery } from '@tanstack/react-query';
+import { getMarketDataSource } from '@/config/dataSources';
+import { SupportedNetworks } from '@/utils/networks';
+import { fetchMorphoMarketSupplies } from '@/data-sources/morpho-api/market-supplies';
+import { fetchSubgraphMarketSupplies } from '@/data-sources/subgraph/market-supplies';
+import { MarketActivityTransaction } from '@/utils/types';
/**
- * Hook to fetch all supply and withdraw activities for a specific market
- * @param marketUniqueKey The unique key of the market
- * @returns List of all supply and withdraw transactions for the market
+ * Hook to fetch all supply and withdraw activities for a specific market's loan asset,
+ * using the appropriate data source based on the network.
+ * @param marketId The ID of the market (e.g., 0x...).
+ * @param loanAssetId The address of the loan asset for the market.
+ * @param network The blockchain network.
+ * @returns List of supply and withdraw transactions for the market's loan asset.
*/
-const useMarketSupplies = (marketUniqueKey: string | undefined) => {
- const [supplies, setSupplies] = useState([]);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(null);
-
- const fetchSupplies = useCallback(async () => {
- if (!marketUniqueKey) {
- setSupplies([]);
- return;
- }
-
- setLoading(true);
- setError(null);
+export const useMarketSupplies = (
+ marketId: string | undefined,
+ loanAssetId: string | undefined,
+ network: SupportedNetworks | undefined,
+) => {
+ const queryKey = ['marketSupplies', marketId, loanAssetId, network];
- try {
- const variables = {
- uniqueKey: marketUniqueKey,
- first: 1000, // Limit to 100 most recent transactions
- skip: 0,
- };
+ // Determine the data source
+ const dataSource = network ? getMarketDataSource(network) : null;
- const response = await fetch(`${URLS.MORPHO_BLUE_API}`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- query: marketSuppliesQuery,
- variables,
- }),
- });
+ console.log('dataSource', dataSource)
- if (!response.ok) {
- throw new Error('Failed to fetch market supplies');
+ const { data, isLoading, error, refetch } = useQuery<
+ MarketActivityTransaction[] | null // The hook returns the unified type
+ >({
+ queryKey: queryKey,
+ queryFn: async (): Promise => {
+ // Guard clauses
+ if (!marketId || !loanAssetId || !network || !dataSource) {
+ return null;
}
- const result = (await response.json()) as {
- data: { transactions: { items: MarketSupplyTransaction[] } };
- };
+ console.log(
+ `Fetching market supplies for market ${marketId} (loan asset ${loanAssetId}) on ${network} via ${dataSource}`,
+ );
- if (result.data?.transactions?.items) {
- setSupplies(result.data.transactions.items);
- } else {
- setSupplies([]);
+ try {
+ // Call the appropriate imported function
+ if (dataSource === 'morpho') {
+ return await fetchMorphoMarketSupplies(marketId);
+ } else if (dataSource === 'subgraph') {
+ return await fetchSubgraphMarketSupplies(marketId, loanAssetId, network);
+ }
+ } catch (fetchError) {
+ // Log the specific error from the data source function
+ console.error(
+ `Failed to fetch market supplies via ${dataSource} for market ${marketId}:`,
+ fetchError,
+ );
+ return null; // Return null on fetch error
}
- } catch (err) {
- console.error('Error fetching market supplies:', err);
- setError(err instanceof Error ? err.message : 'Unknown error');
- } finally {
- setLoading(false);
- }
- }, [marketUniqueKey]);
- useEffect(() => {
- void fetchSupplies();
- }, [fetchSupplies]);
+ // This case should ideally not be reached if getMarketDataSource is exhaustive
+ console.warn('Unknown market data source determined for supplies');
+ return null;
+ },
+ // enable query only if all parameters are present AND a valid data source exists
+ enabled: !!marketId && !!loanAssetId && !!network && !!dataSource,
+ staleTime: 1000 * 60 * 2, // 2 minutes
+ placeholderData: (previousData) => previousData ?? null,
+ retry: 1,
+ });
return {
- supplies,
- loading,
- error,
+ data: data,
+ isLoading: isLoading,
+ error: error,
+ refetch: refetch,
+ dataSource: dataSource,
};
};
+// Keep export default for potential existing imports, but prefer named export
export default useMarketSupplies;
diff --git a/src/utils/types.ts b/src/utils/types.ts
index 3c1b6a58..e234defc 100644
--- a/src/utils/types.ts
+++ b/src/utils/types.ts
@@ -366,3 +366,12 @@ export type AgentMetadata = {
name: string;
strategyDescription: string;
};
+
+// Define the comprehensive Market Activity Transaction type
+export type MarketActivityTransaction = {
+ type: 'MarketSupply' | 'MarketWithdraw' | 'MarketBorrow' | 'MarketRepay';
+ hash: string;
+ timestamp: number;
+ amount: string; // Unified field for assets/amount
+ userAddress: string; // Unified field for user address
+};
From 41c0160930da1fe94cfd66ac3535b2ebb45a7115 Mon Sep 17 00:00:00 2001
From: antoncoding
Date: Wed, 23 Apr 2025 15:24:19 +0800
Subject: [PATCH 02/20] feat: finish borrow and supply
---
.../[marketid]/components/BorrowsTable.tsx | 20 +++++++++++--------
.../[marketid]/components/SuppliesTable.tsx | 4 ++--
src/data-sources/morpho-api/market-borrows.ts | 6 +++---
.../morpho-api/market-supplies.ts | 14 ++++++++-----
src/data-sources/subgraph/market-borrows.ts | 4 ++--
src/data-sources/subgraph/market-supplies.ts | 6 ++----
src/hooks/useMarketSupplies.ts | 4 +---
7 files changed, 31 insertions(+), 27 deletions(-)
diff --git a/app/market/[chainId]/[marketid]/components/BorrowsTable.tsx b/app/market/[chainId]/[marketid]/components/BorrowsTable.tsx
index 073eaab4..8869107f 100644
--- a/app/market/[chainId]/[marketid]/components/BorrowsTable.tsx
+++ b/app/market/[chainId]/[marketid]/components/BorrowsTable.tsx
@@ -26,27 +26,31 @@ export function BorrowsTable({ chainId, market }: BorrowsTableProps) {
const [currentPage, setCurrentPage] = useState(1);
const pageSize = 8;
- const { data: borrows, isLoading, error } = useMarketBorrows(
- market?.uniqueKey,
- market.loanAsset.id,
- chainId,
- );
+ const {
+ data: borrows,
+ isLoading,
+ error,
+ } = useMarketBorrows(market?.uniqueKey, market.loanAsset.id, chainId);
- const totalPages = Math.ceil((borrows || []).length / pageSize);
+ const totalPages = Math.ceil((borrows ?? []).length / pageSize);
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
const paginatedBorrows = useMemo(() => {
- const sliced = (borrows || []).slice((currentPage - 1) * pageSize, currentPage * pageSize);
+ const sliced = (borrows ?? []).slice((currentPage - 1) * pageSize, currentPage * pageSize);
return sliced;
}, [currentPage, borrows, pageSize]);
const tableKey = `borrows-table-${currentPage}`;
if (error) {
- return Error loading borrows: {error instanceof Error ? error.message : 'Unknown error'}
;
+ return (
+
+ Error loading borrows: {error instanceof Error ? error.message : 'Unknown error'}
+
+ );
}
return (
diff --git a/app/market/[chainId]/[marketid]/components/SuppliesTable.tsx b/app/market/[chainId]/[marketid]/components/SuppliesTable.tsx
index 1e9d58f8..d09b96fc 100644
--- a/app/market/[chainId]/[marketid]/components/SuppliesTable.tsx
+++ b/app/market/[chainId]/[marketid]/components/SuppliesTable.tsx
@@ -32,14 +32,14 @@ export function SuppliesTable({ chainId, market }: SuppliesTableProps) {
chainId,
);
- const totalPages = Math.ceil((supplies || []).length / pageSize);
+ const totalPages = Math.ceil((supplies ?? []).length / pageSize);
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
const paginatedSupplies = useMemo(() => {
- const sliced = (supplies || []).slice((currentPage - 1) * pageSize, currentPage * pageSize);
+ const sliced = (supplies ?? []).slice((currentPage - 1) * pageSize, currentPage * pageSize);
return sliced;
}, [currentPage, supplies, pageSize]);
diff --git a/src/data-sources/morpho-api/market-borrows.ts b/src/data-sources/morpho-api/market-borrows.ts
index 32b9919d..7923a5f9 100644
--- a/src/data-sources/morpho-api/market-borrows.ts
+++ b/src/data-sources/morpho-api/market-borrows.ts
@@ -7,7 +7,7 @@ import { morphoGraphqlFetcher } from './fetchers';
type MorphoAPIBorrowsResponse = {
data?: {
transactions?: {
- items?: Array<{
+ items?: {
type: 'MarketBorrow' | 'MarketRepay'; // Specific types for this query
hash: string;
timestamp: number;
@@ -18,7 +18,7 @@ type MorphoAPIBorrowsResponse = {
user: {
address: string;
};
- }>;
+ }[];
};
};
};
@@ -61,4 +61,4 @@ export const fetchMorphoMarketBorrows = async (
}
throw new Error('An unknown error occurred while fetching Morpho API market borrows');
}
-};
\ No newline at end of file
+};
diff --git a/src/data-sources/morpho-api/market-supplies.ts b/src/data-sources/morpho-api/market-supplies.ts
index 5384c677..72b71494 100644
--- a/src/data-sources/morpho-api/market-supplies.ts
+++ b/src/data-sources/morpho-api/market-supplies.ts
@@ -4,9 +4,10 @@ import { morphoGraphqlFetcher } from './fetchers'; // Import shared fetcher
// Type specifically for the raw Morpho API response structure within this module
type MorphoAPISuppliesResponse = {
- data?: { // Mark data as optional to align with fetcher's generic handling
+ data?: {
+ // Mark data as optional to align with fetcher's generic handling
transactions?: {
- items?: Array<{
+ items?: {
type: 'MarketSupply' | 'MarketWithdraw';
hash: string;
timestamp: number;
@@ -17,7 +18,7 @@ type MorphoAPISuppliesResponse = {
user: {
address: string;
};
- }>;
+ }[];
};
};
// Error handling is now done by the fetcher
@@ -59,11 +60,14 @@ export const fetchMorphoMarketSupplies = async (
}));
} catch (error) {
// Catch errors from the fetcher or during processing
- console.error(`Error fetching or processing Morpho API market supplies for ${marketId}:`, error);
+ console.error(
+ `Error fetching or processing Morpho API market supplies for ${marketId}:`,
+ error,
+ );
// Re-throw the error to be handled by the calling hook
if (error instanceof Error) {
throw error;
}
throw new Error('An unknown error occurred while fetching Morpho API market supplies');
}
-};
\ No newline at end of file
+};
diff --git a/src/data-sources/subgraph/market-borrows.ts b/src/data-sources/subgraph/market-borrows.ts
index 7eecafb7..7a1e3a82 100644
--- a/src/data-sources/subgraph/market-borrows.ts
+++ b/src/data-sources/subgraph/market-borrows.ts
@@ -1,7 +1,7 @@
import { marketBorrowsRepaysQuery } from '@/graphql/morpho-subgraph-queries';
-import { MarketActivityTransaction } from '@/utils/types'; // Import shared type
import { SupportedNetworks } from '@/utils/networks';
import { getSubgraphUrl } from '@/utils/subgraph-urls';
+import { MarketActivityTransaction } from '@/utils/types'; // Import shared type
import { subgraphGraphqlFetcher } from './fetchers';
// Types specific to the Subgraph response for this query
@@ -81,4 +81,4 @@ export const fetchSubgraphMarketBorrows = async (
}
throw new Error('An unknown error occurred while fetching subgraph market borrows');
}
-};
\ No newline at end of file
+};
diff --git a/src/data-sources/subgraph/market-supplies.ts b/src/data-sources/subgraph/market-supplies.ts
index 8e9c1334..c2641ad9 100644
--- a/src/data-sources/subgraph/market-supplies.ts
+++ b/src/data-sources/subgraph/market-supplies.ts
@@ -1,7 +1,7 @@
import { marketDepositsWithdrawsQuery } from '@/graphql/morpho-subgraph-queries';
-import { MarketActivityTransaction } from '@/utils/types';
import { SupportedNetworks } from '@/utils/networks';
import { getSubgraphUrl } from '@/utils/subgraph-urls'; // Import shared utility
+import { MarketActivityTransaction } from '@/utils/types';
import { subgraphGraphqlFetcher } from './fetchers'; // Import shared fetcher
// Types specific to the Subgraph response for this query
@@ -80,8 +80,6 @@ export const fetchSubgraphMarketSupplies = async (
// Combine and sort by timestamp descending (most recent first)
const combined = [...mappedDeposits, ...mappedWithdraws];
- console.log('combined', combined.length)
-
combined.sort((a, b) => b.timestamp - a.timestamp);
return combined;
@@ -95,4 +93,4 @@ export const fetchSubgraphMarketSupplies = async (
}
throw new Error('An unknown error occurred while fetching subgraph market supplies');
}
-};
\ No newline at end of file
+};
diff --git a/src/hooks/useMarketSupplies.ts b/src/hooks/useMarketSupplies.ts
index acfedd48..1debc146 100644
--- a/src/hooks/useMarketSupplies.ts
+++ b/src/hooks/useMarketSupplies.ts
@@ -1,8 +1,8 @@
import { useQuery } from '@tanstack/react-query';
import { getMarketDataSource } from '@/config/dataSources';
-import { SupportedNetworks } from '@/utils/networks';
import { fetchMorphoMarketSupplies } from '@/data-sources/morpho-api/market-supplies';
import { fetchSubgraphMarketSupplies } from '@/data-sources/subgraph/market-supplies';
+import { SupportedNetworks } from '@/utils/networks';
import { MarketActivityTransaction } from '@/utils/types';
/**
@@ -23,8 +23,6 @@ export const useMarketSupplies = (
// Determine the data source
const dataSource = network ? getMarketDataSource(network) : null;
- console.log('dataSource', dataSource)
-
const { data, isLoading, error, refetch } = useQuery<
MarketActivityTransaction[] | null // The hook returns the unified type
>({
From 7edea6e1e0d905482bd3ccbd9ed59b6340a78f2c Mon Sep 17 00:00:00 2001
From: antoncoding
Date: Wed, 23 Apr 2025 16:12:04 +0800
Subject: [PATCH 03/20] refactor useMarketLiquidations
---
.../components/LiquidationsTable.tsx | 72 ++++++-----
src/config/dataSources.ts | 4 +-
src/data-sources/morpho-api/historical.ts | 11 --
.../morpho-api/market-liquidations.ts | 68 ++++++++++
.../subgraph/market-liquidations.ts | 91 ++++++++++++++
src/graphql/morpho-subgraph-queries.ts | 31 +++++
src/hooks/useMarketHistoricalData.ts | 8 +-
src/hooks/useMarketLiquidations.ts | 119 ++++++++----------
src/utils/types.ts | 11 ++
9 files changed, 294 insertions(+), 121 deletions(-)
create mode 100644 src/data-sources/morpho-api/market-liquidations.ts
create mode 100644 src/data-sources/subgraph/market-liquidations.ts
diff --git a/app/market/[chainId]/[marketid]/components/LiquidationsTable.tsx b/app/market/[chainId]/[marketid]/components/LiquidationsTable.tsx
index 7d32d629..9e41b924 100644
--- a/app/market/[chainId]/[marketid]/components/LiquidationsTable.tsx
+++ b/app/market/[chainId]/[marketid]/components/LiquidationsTable.tsx
@@ -6,9 +6,9 @@ import moment from 'moment';
import { Address, formatUnits } from 'viem';
import AccountWithAvatar from '@/components/Account/AccountWithAvatar';
import { TokenIcon } from '@/components/TokenIcon';
-import useMarketLiquidations from '@/hooks/useMarketLiquidations';
+import { useMarketLiquidations } from '@/hooks/useMarketLiquidations';
import { getExplorerTxURL, getExplorerURL } from '@/utils/external';
-import { Market } from '@/utils/types';
+import { Market, MarketLiquidationTransaction } from '@/utils/types';
// Helper functions to format data
const formatAddress = (address: string) => {
@@ -24,23 +24,31 @@ export function LiquidationsTable({ chainId, market }: LiquidationsTableProps) {
const [currentPage, setCurrentPage] = useState(1);
const pageSize = 8;
- const { liquidations, loading, error } = useMarketLiquidations(market?.uniqueKey);
+ const {
+ data: liquidations,
+ isLoading,
+ error,
+ } = useMarketLiquidations(market?.uniqueKey, chainId);
- const totalPages = Math.ceil((liquidations || []).length / pageSize);
+ const totalPages = Math.ceil((liquidations ?? []).length / pageSize);
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
const paginatedLiquidations = useMemo(() => {
- const sliced = (liquidations || []).slice((currentPage - 1) * pageSize, currentPage * pageSize);
+ const sliced = (liquidations ?? []).slice((currentPage - 1) * pageSize, currentPage * pageSize);
return sliced;
}, [currentPage, liquidations, pageSize]);
const tableKey = `liquidations-table-${currentPage}`;
if (error) {
- return Error loading liquidations: {error}
;
+ return (
+
+ Error loading liquidations: {error instanceof Error ? error.message : 'Unknown error'}
+
+ );
}
return (
@@ -71,14 +79,11 @@ export function LiquidationsTable({ chainId, market }: LiquidationsTableProps) {
>
LIQUIDATOR
- REPAID ({market?.loanAsset?.symbol ?? 'USDC'})
+ REPAID ({market?.loanAsset?.symbol ?? 'Loan'})
- SEIZED{' '}
- {market?.collateralAsset?.symbol && (
- {market.collateralAsset.symbol}
- )}
+ SEIZED ({market?.collateralAsset?.symbol ?? 'Collateral'})
- BAD DEBT
+ BAD DEBT ({market?.loanAsset?.symbol ?? 'Loan'})
TIME
TRANSACTION
@@ -86,27 +91,32 @@ export function LiquidationsTable({ chainId, market }: LiquidationsTableProps) {
- {paginatedLiquidations.map((liquidation) => {
- const hasBadDebt = BigInt(liquidation.data.badDebtAssets) !== BigInt(0);
+ {paginatedLiquidations.map((liquidation: MarketLiquidationTransaction) => {
+ const hasBadDebt = BigInt(liquidation.badDebtAssets) !== BigInt(0);
+ const isLiquidatorAddress = liquidation.liquidator?.startsWith('0x');
return (
-
-
-
-
+ {isLiquidatorAddress ? (
+
+
+
+
+ ) : (
+ {liquidation.liquidator}
+ )}
- {formatUnits(BigInt(liquidation.data.repaidAssets), market.loanAsset.decimals)}
+ {formatUnits(BigInt(liquidation.repaidAssets), market.loanAsset.decimals)}
{market?.loanAsset?.symbol && (
- {formatUnits(
- BigInt(liquidation.data.seizedAssets),
- market.collateralAsset.decimals,
- )}
+ {formatUnits(BigInt(liquidation.seizedAssets), market.collateralAsset.decimals)}
{market?.collateralAsset?.symbol && (
{hasBadDebt ? (
<>
- {formatUnits(
- BigInt(liquidation.data.badDebtAssets),
- market.loanAsset.decimals,
- )}
+ {formatUnits(BigInt(liquidation.badDebtAssets), market.loanAsset.decimals)}
{market?.loanAsset?.symbol && (
{
switch (network) {
// case SupportedNetworks.Mainnet:
- case SupportedNetworks.Base:
- return 'subgraph';
+ // case SupportedNetworks.Base:
+ // return 'subgraph';
default:
return 'morpho'; // Default to Morpho API
}
diff --git a/src/data-sources/morpho-api/historical.ts b/src/data-sources/morpho-api/historical.ts
index ec37ee3e..02a2bf35 100644
--- a/src/data-sources/morpho-api/historical.ts
+++ b/src/data-sources/morpho-api/historical.ts
@@ -51,17 +51,6 @@ export const fetchMorphoMarketHistoricalData = async (
const historicalState = response?.data?.marketByUniqueKey?.historicalState;
- // --- Add detailed logging ---
- console.log(
- '[fetchMorphoMarketHistoricalData] Raw API Response:',
- JSON.stringify(response, null, 2),
- );
- console.log(
- '[fetchMorphoMarketHistoricalData] Extracted historicalState:',
- JSON.stringify(historicalState, null, 2),
- );
- // --- End logging ---
-
// Check if historicalState exists and has *any* relevant data points (e.g., supplyApy)
// This check might need refinement based on what fields are essential
if (
diff --git a/src/data-sources/morpho-api/market-liquidations.ts b/src/data-sources/morpho-api/market-liquidations.ts
new file mode 100644
index 00000000..3505887a
--- /dev/null
+++ b/src/data-sources/morpho-api/market-liquidations.ts
@@ -0,0 +1,68 @@
+import { marketLiquidationsQuery } from '@/graphql/morpho-api-queries';
+import { MarketLiquidationTransaction } from '@/utils/types'; // Import unified type
+import { morphoGraphqlFetcher } from './fetchers';
+
+// Type for the raw Morpho API response structure
+type MorphoAPILiquidationItem = {
+ hash: string;
+ timestamp: number;
+ type: string; // Should be 'MarketLiquidation'
+ data: {
+ repaidAssets: string;
+ seizedAssets: string;
+ liquidator: string;
+ badDebtAssets: string;
+ };
+};
+
+type MorphoAPILiquidationsResponse = {
+ data?: {
+ transactions?: {
+ items?: MorphoAPILiquidationItem[];
+ };
+ };
+};
+
+/**
+ * Fetches market liquidation activities from the Morpho Blue API.
+ * @param marketId The unique key or ID of the market.
+ * @returns A promise resolving to an array of unified MarketLiquidationTransaction objects.
+ */
+export const fetchMorphoMarketLiquidations = async (
+ marketId: string,
+): Promise => {
+ const variables = {
+ uniqueKey: marketId,
+ // Morpho API query might not need first/skip for liquidations, adjust if needed
+ };
+
+ try {
+ const result = await morphoGraphqlFetcher(
+ marketLiquidationsQuery,
+ variables,
+ );
+
+ const items = result.data?.transactions?.items ?? [];
+
+ // Map to unified type
+ return items.map((item) => ({
+ type: 'MarketLiquidation', // Standardize type
+ hash: item.hash,
+ timestamp: item.timestamp,
+ liquidator: item.data.liquidator,
+ repaidAssets: item.data.repaidAssets,
+ seizedAssets: item.data.seizedAssets,
+ badDebtAssets: item.data.badDebtAssets,
+ // Removed optional fields not present in the simplified type
+ }));
+ } catch (error) {
+ console.error(
+ `Error fetching or processing Morpho API market liquidations for ${marketId}:`,
+ error,
+ );
+ if (error instanceof Error) {
+ throw error;
+ }
+ throw new Error('An unknown error occurred while fetching Morpho API market liquidations');
+ }
+};
diff --git a/src/data-sources/subgraph/market-liquidations.ts b/src/data-sources/subgraph/market-liquidations.ts
new file mode 100644
index 00000000..44ad3176
--- /dev/null
+++ b/src/data-sources/subgraph/market-liquidations.ts
@@ -0,0 +1,91 @@
+import { marketLiquidationsAndBadDebtQuery } from '@/graphql/morpho-subgraph-queries';
+import { SupportedNetworks } from '@/utils/networks';
+import { getSubgraphUrl } from '@/utils/subgraph-urls';
+import { MarketLiquidationTransaction } from '@/utils/types'; // Import simplified type
+import { subgraphGraphqlFetcher } from './fetchers';
+
+// Types specific to the Subgraph response items
+type SubgraphLiquidateItem = {
+ id: string;
+ hash: string;
+ timestamp: number | string;
+ repaid: string;
+ amount: string;
+ liquidator: {
+ id: string;
+ };
+};
+
+type SubgraphBadDebtItem = {
+ badDebt: string;
+ liquidation: {
+ id: string;
+ };
+};
+
+// Type for the overall Subgraph response
+type SubgraphLiquidationsResponse = {
+ data?: {
+ liquidates?: SubgraphLiquidateItem[];
+ badDebtRealizations?: SubgraphBadDebtItem[];
+ };
+};
+
+/**
+ * Fetches market liquidation activities from the Subgraph.
+ * Combines liquidation events with associated bad debt realizations.
+ * @param marketId The ID of the market.
+ * @param network The blockchain network.
+ * @returns A promise resolving to an array of simplified MarketLiquidationTransaction objects.
+ */
+export const fetchSubgraphMarketLiquidations = async (
+ marketId: string,
+ network: SupportedNetworks,
+): Promise => {
+ const subgraphUrl = getSubgraphUrl(network);
+ if (!subgraphUrl) {
+ console.error(`No Subgraph URL configured for network: ${network}`);
+ throw new Error(`Subgraph URL not available for network ${network}`);
+ }
+
+ const variables = { marketId };
+
+ try {
+ const result = await subgraphGraphqlFetcher(
+ subgraphUrl,
+ marketLiquidationsAndBadDebtQuery,
+ variables,
+ );
+
+ const liquidates = result.data?.liquidates ?? [];
+ const badDebtItems = result.data?.badDebtRealizations ?? [];
+
+ // Create a map for quick lookup of bad debt by liquidation ID
+ const badDebtMap = new Map();
+ badDebtItems.forEach((item) => {
+ badDebtMap.set(item.liquidation.id, item.badDebt);
+ });
+
+ // Map liquidations, adding bad debt information
+ return liquidates.map((liq) => ({
+ type: 'MarketLiquidation',
+ hash: liq.hash,
+ timestamp: typeof liq.timestamp === 'string' ? parseInt(liq.timestamp, 10) : liq.timestamp,
+ // Subgraph query doesn't provide liquidator, use empty string or default
+ liquidator: liq.liquidator.id,
+ repaidAssets: liq.repaid, // Loan asset repaid
+ seizedAssets: liq.amount, // Collateral seized
+ // Fetch bad debt from the map using the liquidate event ID
+ badDebtAssets: badDebtMap.get(liq.id) ?? '0', // Default to '0' if no bad debt entry
+ }));
+ } catch (error) {
+ console.error(
+ `Error fetching or processing Subgraph market liquidations for ${marketId}:`,
+ error,
+ );
+ if (error instanceof Error) {
+ throw error;
+ }
+ throw new Error('An unknown error occurred while fetching subgraph market liquidations');
+ }
+};
diff --git a/src/graphql/morpho-subgraph-queries.ts b/src/graphql/morpho-subgraph-queries.ts
index f625a346..675b673d 100644
--- a/src/graphql/morpho-subgraph-queries.ts
+++ b/src/graphql/morpho-subgraph-queries.ts
@@ -211,3 +211,34 @@ export const marketBorrowsRepaysQuery = `
}
`;
// --- End Query ---
+
+// --- Query for Market Liquidations and Bad Debt ---
+export const marketLiquidationsAndBadDebtQuery = `
+ query getMarketLiquidations($marketId: Bytes!) {
+ liquidates(
+ first: 1000,
+ where: { market: $marketId },
+ orderBy: timestamp,
+ orderDirection: desc
+ ) {
+ id # ID of the liquidate event itself
+ hash
+ timestamp
+ repaid # Amount of loan asset repaid
+ amount # Amount of collateral seized
+ liquidator {
+ id
+ }
+ }
+ badDebtRealizations(
+ first: 1000,
+ where: { market: $marketId }
+ ) {
+ badDebt
+ liquidation {
+ id
+ }
+ }
+ }
+`;
+// --- End Query ---
diff --git a/src/hooks/useMarketHistoricalData.ts b/src/hooks/useMarketHistoricalData.ts
index ad33446a..f1310465 100644
--- a/src/hooks/useMarketHistoricalData.ts
+++ b/src/hooks/useMarketHistoricalData.ts
@@ -40,13 +40,9 @@ export const useMarketHistoricalData = (
console.log(`Fetching historical data for ${uniqueKey} on ${network} via ${dataSource}`);
if (dataSource === 'morpho') {
- const res = await fetchMorphoMarketHistoricalData(uniqueKey, network, options);
- console.log('res morpho', res);
- return res;
+ return fetchMorphoMarketHistoricalData(uniqueKey, network, options);
} else if (dataSource === 'subgraph') {
- const res = await fetchSubgraphMarketHistoricalData(uniqueKey, network, options);
- console.log('res', res);
- return res;
+ return fetchSubgraphMarketHistoricalData(uniqueKey, network, options);
}
console.warn('Unknown historical data source determined');
diff --git a/src/hooks/useMarketLiquidations.ts b/src/hooks/useMarketLiquidations.ts
index 2fd6cfe6..65da8d64 100644
--- a/src/hooks/useMarketLiquidations.ts
+++ b/src/hooks/useMarketLiquidations.ts
@@ -1,83 +1,66 @@
-import { useState, useEffect, useCallback } from 'react';
-import { marketLiquidationsQuery } from '@/graphql/morpho-api-queries';
-import { URLS } from '@/utils/urls';
-
-export type MarketLiquidationTransaction = {
- hash: string;
- timestamp: number;
- type: string;
- data: {
- repaidAssets: string;
- seizedAssets: string;
- liquidator: string;
- badDebtAssets: string;
- };
-};
+import { useQuery } from '@tanstack/react-query';
+import { getMarketDataSource } from '@/config/dataSources';
+import { fetchMorphoMarketLiquidations } from '@/data-sources/morpho-api/market-liquidations';
+import { fetchSubgraphMarketLiquidations } from '@/data-sources/subgraph/market-liquidations';
+import { SupportedNetworks } from '@/utils/networks';
+import { MarketLiquidationTransaction } from '@/utils/types'; // Use simplified type
/**
- * Hook to fetch all liquidations for a specific market
- * @param marketUniqueKey The unique key of the market
- * @returns List of all liquidation transactions for the market
+ * Hook to fetch all liquidations for a specific market, using the appropriate data source.
+ * @param marketId The ID or unique key of the market.
+ * @param network The blockchain network.
+ * @returns List of liquidation transactions for the market.
*/
-const useMarketLiquidations = (marketUniqueKey: string | undefined) => {
- const [liquidations, setLiquidations] = useState([]);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(null);
-
- const fetchLiquidations = useCallback(async () => {
- if (!marketUniqueKey) {
- setLiquidations([]);
- return;
- }
-
- setLoading(true);
- setError(null);
-
- try {
- const variables = {
- uniqueKey: marketUniqueKey,
- };
+export const useMarketLiquidations = (
+ marketId: string | undefined,
+ network: SupportedNetworks | undefined,
+) => {
+ // Note: loanAssetId is not needed for liquidations query
+ const queryKey = ['marketLiquidations', marketId, network];
- const response = await fetch(`${URLS.MORPHO_BLUE_API}`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- query: marketLiquidationsQuery,
- variables,
- }),
- });
+ // Determine the data source
+ const dataSource = network ? getMarketDataSource(network) : null;
- if (!response.ok) {
- throw new Error('Failed to fetch market liquidations');
+ const { data, isLoading, error, refetch } = useQuery({
+ queryKey: queryKey,
+ queryFn: async (): Promise => {
+ // Guard clauses
+ if (!marketId || !network || !dataSource) {
+ return null;
}
- const result = (await response.json()) as {
- data: { transactions: { items: MarketLiquidationTransaction[] } };
- };
+ console.log(
+ `Fetching market liquidations for market ${marketId} on ${network} via ${dataSource}`,
+ );
- if (result.data?.transactions?.items) {
- setLiquidations(result.data.transactions.items);
- } else {
- setLiquidations([]);
+ try {
+ if (dataSource === 'morpho') {
+ return await fetchMorphoMarketLiquidations(marketId);
+ } else if (dataSource === 'subgraph') {
+ console.log('fetching subgraph liquidations');
+ return await fetchSubgraphMarketLiquidations(marketId, network);
+ }
+ } catch (fetchError) {
+ console.error(`Failed to fetch market liquidations via ${dataSource}:`, fetchError);
+ return null;
}
- } catch (err) {
- console.error('Error fetching market liquidations:', err);
- setError(err instanceof Error ? err.message : 'Unknown error');
- } finally {
- setLoading(false);
- }
- }, [marketUniqueKey]);
- useEffect(() => {
- void fetchLiquidations();
- }, [fetchLiquidations]);
+ console.warn('Unknown market data source determined for liquidations');
+ return null;
+ },
+ enabled: !!marketId && !!network && !!dataSource,
+ staleTime: 1000 * 60 * 5, // 5 minutes, liquidations are less frequent
+ placeholderData: (previousData) => previousData ?? null,
+ retry: 1,
+ });
+ // Return standard react-query hook structure
return {
- liquidations,
- loading,
- error,
+ data: data, // Consumers can alias this as 'liquidations' if desired
+ isLoading: isLoading,
+ error: error,
+ refetch: refetch,
+ dataSource: dataSource,
};
};
diff --git a/src/utils/types.ts b/src/utils/types.ts
index e234defc..6b272562 100644
--- a/src/utils/types.ts
+++ b/src/utils/types.ts
@@ -375,3 +375,14 @@ export type MarketActivityTransaction = {
amount: string; // Unified field for assets/amount
userAddress: string; // Unified field for user address
};
+
+// Type for Liquidation Transactions (Simplified based on original hook)
+export type MarketLiquidationTransaction = {
+ type: 'MarketLiquidation';
+ hash: string;
+ timestamp: number;
+ liquidator: string;
+ repaidAssets: string;
+ seizedAssets: string;
+ badDebtAssets: string;
+};
From e322703b95b21198d1bd7e7c844a3e0fc0b3c169 Mon Sep 17 00:00:00 2001
From: antoncoding
Date: Fri, 25 Apr 2025 14:09:58 +0800
Subject: [PATCH 04/20] chore: lint
---
src/config/dataSources.ts | 4 +-
src/contexts/MarketsContext.tsx | 114 ++++++++++++++++---------
src/data-sources/morpho-api/market.ts | 35 +++++++-
src/data-sources/subgraph/market.ts | 53 +++++++++++-
src/graphql/morpho-subgraph-queries.ts | 13 ++-
src/hooks/useUserPosition.ts | 2 -
src/utils/subgraph-types.ts | 2 +-
7 files changed, 175 insertions(+), 48 deletions(-)
diff --git a/src/config/dataSources.ts b/src/config/dataSources.ts
index 35064c1e..6c0935f8 100644
--- a/src/config/dataSources.ts
+++ b/src/config/dataSources.ts
@@ -6,8 +6,8 @@ import { SupportedNetworks } from '@/utils/networks';
export const getMarketDataSource = (network: SupportedNetworks): 'morpho' | 'subgraph' => {
switch (network) {
// case SupportedNetworks.Mainnet:
- // case SupportedNetworks.Base:
- // return 'subgraph';
+ case SupportedNetworks.Base:
+ return 'subgraph';
default:
return 'morpho'; // Default to Morpho API
}
diff --git a/src/contexts/MarketsContext.tsx b/src/contexts/MarketsContext.tsx
index ca90ee33..df78d4b9 100644
--- a/src/contexts/MarketsContext.tsx
+++ b/src/contexts/MarketsContext.tsx
@@ -9,9 +9,11 @@ import {
useState,
useMemo,
} from 'react';
-import { marketsQuery } from '@/graphql/morpho-api-queries';
+import { getMarketDataSource } from '@/config/dataSources';
+import { fetchMorphoMarkets } from '@/data-sources/morpho-api/market';
+import { fetchSubgraphMarkets } from '@/data-sources/subgraph/market';
import useLiquidations from '@/hooks/useLiquidations';
-import { isSupportedChain } from '@/utils/networks';
+import { isSupportedChain, SupportedNetworks } from '@/utils/networks';
import { Market } from '@/utils/types';
import { getMarketWarningsWithDetail } from '@/utils/warnings';
@@ -30,14 +32,6 @@ type MarketsProviderProps = {
children: ReactNode;
};
-type MarketResponse = {
- data: {
- markets: {
- items: Market[];
- };
- };
-};
-
export function MarketsProvider({ children }: MarketsProviderProps) {
const [loading, setLoading] = useState(true);
const [isRefetching, setIsRefetching] = useState(false);
@@ -53,46 +47,78 @@ export function MarketsProvider({ children }: MarketsProviderProps) {
const fetchMarkets = useCallback(
async (isRefetch = false) => {
- try {
- if (isRefetch) {
- setIsRefetching(true);
- } else {
- setLoading(true);
- }
+ if (isRefetch) {
+ setIsRefetching(true);
+ } else {
+ setLoading(true);
+ }
+ setError(null); // Reset error at the start
- const marketsResponse = await fetch('https://blue-api.morpho.org/graphql', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- query: marketsQuery,
- variables: { first: 1000, where: { whitelisted: true } },
- }),
- });
+ // Define the networks to fetch markets for
+ const networksToFetch: SupportedNetworks[] = [
+ SupportedNetworks.Mainnet,
+ SupportedNetworks.Base,
+ ];
+ let combinedMarkets: Market[] = [];
+ let fetchErrors: unknown[] = [];
- const marketsResult = (await marketsResponse.json()) as MarketResponse;
- const rawMarkets = marketsResult.data.markets.items;
+ try {
+ // Fetch markets for each network based on its data source
+ await Promise.all(
+ networksToFetch.map(async (network) => {
+ try {
+ const dataSource = getMarketDataSource(network);
+ let networkMarkets: Market[] = [];
+
+ console.log(`Fetching markets for ${network} via ${dataSource}`);
+
+ if (dataSource === 'morpho') {
+ networkMarkets = await fetchMorphoMarkets(network);
+ } else if (dataSource === 'subgraph') {
+ networkMarkets = await fetchSubgraphMarkets(network);
+ } else {
+ console.warn(`No valid data source found for network ${network}`);
+ }
+ combinedMarkets.push(...networkMarkets);
+ } catch (networkError) {
+ console.error(`Failed to fetch markets for network ${network}:`, networkError);
+ fetchErrors.push(networkError); // Collect errors for each network
+ }
+ }),
+ );
- const filtered = rawMarkets
+ // Process combined markets (filters, warnings, liquidation status)
+ // Existing filters seem appropriate
+ const filtered = combinedMarkets
.filter((market) => market.collateralAsset != undefined)
.filter(
(market) => market.warnings.find((w) => w.type === 'not_whitelisted') === undefined,
)
- .filter((market) => isSupportedChain(market.morphoBlue.chain.id));
+ .filter((market) => isSupportedChain(market.morphoBlue.chain.id)); // Keep this filter
const processedMarkets = filtered.map((market) => {
- const warningsWithDetail = getMarketWarningsWithDetail(market);
+ const warningsWithDetail = getMarketWarningsWithDetail(market); // Recalculate warnings if needed, though fetchers might do this
const isProtectedByLiquidationBots = liquidatedMarketKeys.has(market.uniqueKey);
return {
...market,
- warningsWithDetail,
+ // Ensure warningsWithDetail from fetchers are used or recalculated consistently
+ warningsWithDetail: market.warningsWithDetail ?? warningsWithDetail,
isProtectedByLiquidationBots,
};
});
setMarkets(processedMarkets);
- } catch (_error) {
- setError(_error);
+
+ // If any network fetch failed, set the overall error state
+ if (fetchErrors.length > 0) {
+ // Maybe combine errors or just take the first one
+ setError(fetchErrors[0]);
+ }
+ } catch (err) {
+ // Catch potential errors from Promise.all itself or overall logic
+ console.error('Overall error fetching markets:', err);
+ setError(err);
} finally {
if (isRefetch) {
setIsRefetching(false);
@@ -101,19 +127,26 @@ export function MarketsProvider({ children }: MarketsProviderProps) {
}
}
},
- [liquidatedMarketKeys],
+ [liquidatedMarketKeys], // Dependencies: liquidatedMarketKeys is needed for processing
);
useEffect(() => {
if (!liquidationsLoading && markets.length === 0) {
+ // Fetch markets only if liquidations are loaded and markets aren't already populated
fetchMarkets().catch(console.error);
}
- }, [liquidationsLoading, fetchMarkets]);
+ // Dependency on fetchMarkets is correct here, also depends on liquidationsLoading
+ }, [liquidationsLoading, fetchMarkets, markets.length]);
const refetch = useCallback(
- (onSuccess?: () => void) => {
- refetchLiquidations();
- fetchMarkets(true).then(onSuccess).catch(console.error);
+ async (onSuccess?: () => void) => {
+ try {
+ refetchLiquidations();
+ await fetchMarkets(true);
+ onSuccess?.();
+ } catch (err) {
+ console.error('Error during refetch:', err);
+ }
},
[refetchLiquidations, fetchMarkets],
);
@@ -121,12 +154,17 @@ export function MarketsProvider({ children }: MarketsProviderProps) {
const refresh = useCallback(async () => {
setLoading(true);
setMarkets([]);
+ setError(null);
try {
+ refetchLiquidations();
await fetchMarkets();
} catch (_error) {
console.error('Failed to refresh markets:', _error);
+ setError(_error);
+ } finally {
+ setLoading(false);
}
- }, [fetchMarkets]);
+ }, [refetchLiquidations, fetchMarkets]);
const isLoading = loading || liquidationsLoading;
const combinedError = error || liquidationsError;
diff --git a/src/data-sources/morpho-api/market.ts b/src/data-sources/morpho-api/market.ts
index c28e2ef9..6e85c9ad 100644
--- a/src/data-sources/morpho-api/market.ts
+++ b/src/data-sources/morpho-api/market.ts
@@ -1,4 +1,4 @@
-import { marketDetailQuery } from '@/graphql/morpho-api-queries';
+import { marketDetailQuery, marketsQuery } from '@/graphql/morpho-api-queries';
import { SupportedNetworks } from '@/utils/networks';
import { Market } from '@/utils/types';
import { getMarketWarningsWithDetail } from '@/utils/warnings';
@@ -11,6 +11,16 @@ type MarketGraphQLResponse = {
errors?: { message: string }[];
};
+// Define response type for multiple markets
+type MarketsGraphQLResponse = {
+ data: {
+ markets: {
+ items: Market[];
+ };
+ };
+ errors?: { message: string }[];
+};
+
const processMarketData = (market: Market): Market => {
const warningsWithDetail = getMarketWarningsWithDetail(market);
return {
@@ -34,3 +44,26 @@ export const fetchMorphoMarket = async (
}
return processMarketData(response.data.marketByUniqueKey);
};
+
+// Fetcher for multiple markets from Morpho API
+export const fetchMorphoMarkets = async (network: SupportedNetworks): Promise => {
+ // Construct the full variables object including the where clause
+ const variables = {
+ first: 1000, // Max limit
+ where: {
+ chainId_in: [network],
+ whitelisted: true,
+ // Add other potential filters to 'where' if needed in the future
+ },
+ };
+
+ const response = await morphoGraphqlFetcher(marketsQuery, variables);
+
+ if (!response.data || !response.data.markets || !response.data.markets.items) {
+ console.warn(`Market data not found in Morpho API response for network ${network}.`);
+ return []; // Return empty array if not found
+ }
+
+ // Process each market in the items array
+ return response.data.markets.items.map(processMarketData);
+};
diff --git a/src/data-sources/subgraph/market.ts b/src/data-sources/subgraph/market.ts
index 30c86c15..746a31be 100644
--- a/src/data-sources/subgraph/market.ts
+++ b/src/data-sources/subgraph/market.ts
@@ -1,7 +1,15 @@
import { Address } from 'viem';
-import { marketQuery as subgraphMarketQuery } from '@/graphql/morpho-subgraph-queries'; // Assuming query is here
+import {
+ marketQuery as subgraphMarketQuery,
+ marketsQuery as subgraphMarketsQuery,
+} from '@/graphql/morpho-subgraph-queries'; // Assuming query is here
import { SupportedNetworks } from '@/utils/networks';
-import { SubgraphMarket, SubgraphMarketQueryResponse, SubgraphToken } from '@/utils/subgraph-types';
+import {
+ SubgraphMarket,
+ SubgraphMarketQueryResponse,
+ SubgraphMarketsQueryResponse,
+ SubgraphToken,
+} from '@/utils/subgraph-types';
import { getSubgraphUrl } from '@/utils/subgraph-urls';
import { WarningWithDetail, MorphoChainlinkOracleData, Market } from '@/utils/types';
import { subgraphGraphqlFetcher } from './fetchers';
@@ -154,3 +162,44 @@ export const fetchSubgraphMarket = async (
return transformSubgraphMarketToMarket(marketData, network);
};
+
+// Fetcher for multiple markets from Subgraph
+export const fetchSubgraphMarkets = async (
+ network: SupportedNetworks,
+ // Optional filter, adjust based on actual subgraph schema capabilities
+ // filter?: { [key: string]: any },
+): Promise => {
+ const subgraphApiUrl = getSubgraphUrl(network);
+
+ if (!subgraphApiUrl) {
+ console.error(`Subgraph URL for network ${network} is not defined.`);
+ throw new Error(`Subgraph URL for network ${network} is not defined.`);
+ }
+
+ // Construct variables for the query
+ const variables: { first: number; where?: Record; network?: string } = {
+ first: 1000, // Max limit
+ // If filtering is needed and supported by the schema, add it here
+ // where: filter,
+ // Pass network if the query uses it for filtering
+ // network: network === SupportedNetworks.Base ? 'BASE' : 'MAINNET', // Example mapping
+ };
+
+ // Use the new marketsQuery
+ const response = await subgraphGraphqlFetcher( // Use the new response type
+ subgraphApiUrl,
+ subgraphMarketsQuery, // Use the new query
+ variables,
+ );
+
+ // Assuming the response structure matches the single market query for the list
+ const marketsData = response.data.markets; // Adjust based on actual response structure
+
+ if (!marketsData || !Array.isArray(marketsData)) {
+ console.warn(`No markets found or invalid format in Subgraph response for network ${network}.`);
+ return []; // Return empty array if no markets or error
+ }
+
+ // Transform each market
+ return marketsData.map((market) => transformSubgraphMarketToMarket(market, network));
+};
diff --git a/src/graphql/morpho-subgraph-queries.ts b/src/graphql/morpho-subgraph-queries.ts
index 675b673d..fd62e944 100644
--- a/src/graphql/morpho-subgraph-queries.ts
+++ b/src/graphql/morpho-subgraph-queries.ts
@@ -73,8 +73,17 @@ export const marketFragment = `
`;
export const marketsQuery = `
- query getSubgraphMarkets($first: Int, $where: Market_filter) {
- markets(first: $first, where: $where, orderBy: totalValueLockedUSD, orderDirection: desc) {
+ query getSubgraphMarkets($first: Int, $where: Market_filter, $network: String) {
+ markets(
+ first: $first,
+ where: $where,
+ orderBy: totalValueLockedUSD,
+ orderDirection: desc,
+ # Subgraph network filtering is typically done via the endpoint URL or a field in the 'where' clause
+ # Assuming the schema allows filtering by protocol network name:
+ # where: { protocol_: { network: $network }, ...$where } # Adjust if schema differs
+ # If filtering by network isn't directly supported in 'where', it might need to be handled post-fetch or by endpoint selection
+ ) {
...SubgraphMarketFields
}
}
diff --git a/src/hooks/useUserPosition.ts b/src/hooks/useUserPosition.ts
index a92de336..6572fa6a 100644
--- a/src/hooks/useUserPosition.ts
+++ b/src/hooks/useUserPosition.ts
@@ -55,8 +55,6 @@ const useUserPositions = (
// Read on-chain data
const currentSnapshot = await fetchPositionSnapshot(marketKey, user as Address, chainId, 0);
- console.log('currentSnapshot', currentSnapshot);
-
if (currentSnapshot) {
setPosition({
market: data.data.marketPosition.market,
diff --git a/src/utils/subgraph-types.ts b/src/utils/subgraph-types.ts
index 99be139c..e2313cb3 100644
--- a/src/utils/subgraph-types.ts
+++ b/src/utils/subgraph-types.ts
@@ -79,7 +79,7 @@ export type SubgraphMarketsQueryResponse = {
// Type for a single market response (if we adapt query later)
export type SubgraphMarketQueryResponse = {
data: {
- market: SubgraphMarket | null; // Assuming a query like market(id: ...) might return null
+ market: SubgraphMarket | null;
};
errors?: { message: string }[];
};
From 555b14a9c09ca2a5dd701d7a8a75272acbb14076 Mon Sep 17 00:00:00 2001
From: antoncoding
Date: Fri, 25 Apr 2025 17:20:43 +0800
Subject: [PATCH 05/20] fix: calculation and blacklist
---
src/config/dataSources.ts | 3 +-
src/contexts/MarketsContext.tsx | 5 +++
src/data-sources/subgraph/market.ts | 49 ++++++++++++++++++--------
src/graphql/morpho-subgraph-queries.ts | 5 +--
src/utils/subgraph-types.ts | 46 +++++++++++++-----------
src/utils/tokens.ts | 4 +++
src/utils/types.ts | 4 +--
7 files changed, 74 insertions(+), 42 deletions(-)
diff --git a/src/config/dataSources.ts b/src/config/dataSources.ts
index 6c0935f8..f8e55e8c 100644
--- a/src/config/dataSources.ts
+++ b/src/config/dataSources.ts
@@ -5,7 +5,8 @@ import { SupportedNetworks } from '@/utils/networks';
*/
export const getMarketDataSource = (network: SupportedNetworks): 'morpho' | 'subgraph' => {
switch (network) {
- // case SupportedNetworks.Mainnet:
+ case SupportedNetworks.Mainnet:
+ return 'subgraph';
case SupportedNetworks.Base:
return 'subgraph';
default:
diff --git a/src/contexts/MarketsContext.tsx b/src/contexts/MarketsContext.tsx
index df78d4b9..bb4c17b6 100644
--- a/src/contexts/MarketsContext.tsx
+++ b/src/contexts/MarketsContext.tsx
@@ -79,6 +79,11 @@ export function MarketsProvider({ children }: MarketsProviderProps) {
} else {
console.warn(`No valid data source found for network ${network}`);
}
+
+ if (network === SupportedNetworks.Mainnet) {
+ console.log('networkMarkets', networkMarkets);
+ }
+
combinedMarkets.push(...networkMarkets);
} catch (networkError) {
console.error(`Failed to fetch markets for network ${network}:`, networkError);
diff --git a/src/data-sources/subgraph/market.ts b/src/data-sources/subgraph/market.ts
index 746a31be..567200b0 100644
--- a/src/data-sources/subgraph/market.ts
+++ b/src/data-sources/subgraph/market.ts
@@ -3,6 +3,7 @@ import {
marketQuery as subgraphMarketQuery,
marketsQuery as subgraphMarketsQuery,
} from '@/graphql/morpho-subgraph-queries'; // Assuming query is here
+import { formatBalance } from '@/utils/balance';
import { SupportedNetworks } from '@/utils/networks';
import {
SubgraphMarket,
@@ -11,6 +12,7 @@ import {
SubgraphToken,
} from '@/utils/subgraph-types';
import { getSubgraphUrl } from '@/utils/subgraph-urls';
+import { blacklistTokens } from '@/utils/tokens';
import { WarningWithDetail, MorphoChainlinkOracleData, Market } from '@/utils/types';
import { subgraphGraphqlFetcher } from './fetchers';
@@ -41,7 +43,7 @@ const transformSubgraphMarketToMarket = (
const lltv = subgraphMarket.lltv ?? '0';
const irmAddress = subgraphMarket.irm ?? '0x';
const inputTokenPriceUSD = subgraphMarket.inputTokenPriceUSD ?? '0';
- const totalDepositBalanceUSD = subgraphMarket.totalDepositBalanceUSD ?? '0';
+
const totalBorrowBalanceUSD = subgraphMarket.totalBorrowBalanceUSD ?? '0';
const totalSupplyShares = subgraphMarket.totalSupplyShares ?? '0';
const totalBorrowShares = subgraphMarket.totalBorrowShares ?? '0';
@@ -67,10 +69,12 @@ const transformSubgraphMarketToMarket = (
const chainId = network;
- const borrowAssets = subgraphMarket.totalBorrow ?? '0';
- const supplyAssets = subgraphMarket.totalSupply ?? '0';
- const collateralAssets = subgraphMarket.inputTokenBalance ?? '0';
- const collateralAssetsUsd = safeParseFloat(subgraphMarket.totalValueLockedUSD);
+ // @todo: might update due to input token being used here
+ const supplyAssets = subgraphMarket.totalSupply ?? subgraphMarket.inputTokenBalance ?? '0';
+ const borrowAssets =
+ subgraphMarket.totalBorrow ?? subgraphMarket.variableBorrowedTokenBalance ?? '0';
+ const collateralAssets = subgraphMarket.totalCollateral ?? '0';
+
const timestamp = safeParseInt(subgraphMarket.lastUpdate);
const totalSupplyNum = safeParseFloat(supplyAssets);
@@ -80,9 +84,20 @@ const transformSubgraphMarketToMarket = (
const supplyApy = Number(subgraphMarket.rates?.find((r) => r.side === 'LENDER')?.rate ?? 0);
const borrowApy = Number(subgraphMarket.rates?.find((r) => r.side === 'BORROWER')?.rate ?? 0);
+ // only borrowBalanceUSD is available in subgraph, we need to calculate supplyAssetsUsd, liquidityAssetsUsd, collateralAssetsUsd
+ const borrowAssetsUsd = safeParseFloat(totalBorrowBalanceUSD);
+
+ // get the prices
+ const loanAssetPrice = safeParseFloat(subgraphMarket.borrowedToken?.lastPriceUSD ?? '0');
+ const collateralAssetPrice = safeParseFloat(subgraphMarket.inputToken?.lastPriceUSD ?? '0');
+
+ const supplyAssetsUsd = formatBalance(supplyAssets, loanAsset.decimals) * loanAssetPrice;
+
const liquidityAssets = (BigInt(supplyAssets) - BigInt(borrowAssets)).toString();
- const liquidityAssetsUsd =
- safeParseFloat(totalDepositBalanceUSD) - safeParseFloat(totalBorrowBalanceUSD);
+ const liquidityAssetsUsd = formatBalance(liquidityAssets, loanAsset.decimals) * loanAssetPrice;
+
+ const collateralAssetsUsd =
+ formatBalance(collateralAssets, collateralAsset.decimals) * collateralAssetPrice;
const warningsWithDetail: WarningWithDetail[] = []; // Subgraph doesn't provide warnings directly
@@ -95,16 +110,20 @@ const transformSubgraphMarketToMarket = (
loanAsset: loanAsset,
collateralAsset: collateralAsset,
state: {
+ // assets
borrowAssets: borrowAssets,
supplyAssets: supplyAssets,
- borrowAssetsUsd: totalBorrowBalanceUSD,
- supplyAssetsUsd: totalDepositBalanceUSD,
+ liquidityAssets: liquidityAssets,
+ collateralAssets: collateralAssets,
+ // shares
borrowShares: totalBorrowShares,
supplyShares: totalSupplyShares,
- liquidityAssets: liquidityAssets,
+ // usd
+ borrowAssetsUsd: borrowAssetsUsd,
+ supplyAssetsUsd: supplyAssetsUsd,
liquidityAssetsUsd: liquidityAssetsUsd,
- collateralAssets: collateralAssets,
collateralAssetsUsd: collateralAssetsUsd,
+
utilization: utilization,
supplyApy: supplyApy,
borrowApy: borrowApy,
@@ -176,13 +195,13 @@ export const fetchSubgraphMarkets = async (
throw new Error(`Subgraph URL for network ${network} is not defined.`);
}
- // Construct variables for the query
+ // Construct variables for the query, adding blacklistTokens
const variables: { first: number; where?: Record; network?: string } = {
first: 1000, // Max limit
// If filtering is needed and supported by the schema, add it here
- // where: filter,
- // Pass network if the query uses it for filtering
- // network: network === SupportedNetworks.Base ? 'BASE' : 'MAINNET', // Example mapping
+ where: {
+ inputToken_not_in: blacklistTokens,
+ },
};
// Use the new marketsQuery
diff --git a/src/graphql/morpho-subgraph-queries.ts b/src/graphql/morpho-subgraph-queries.ts
index fd62e944..3a96768f 100644
--- a/src/graphql/morpho-subgraph-queries.ts
+++ b/src/graphql/morpho-subgraph-queries.ts
@@ -36,6 +36,7 @@ export const marketFragment = `
totalBorrowShares
totalSupply
totalBorrow
+ totalCollateral
fee
name
@@ -79,10 +80,6 @@ export const marketsQuery = `
where: $where,
orderBy: totalValueLockedUSD,
orderDirection: desc,
- # Subgraph network filtering is typically done via the endpoint URL or a field in the 'where' clause
- # Assuming the schema allows filtering by protocol network name:
- # where: { protocol_: { network: $network }, ...$where } # Adjust if schema differs
- # If filtering by network isn't directly supported in 'where', it might need to be handled post-fetch or by endpoint selection
) {
...SubgraphMarketFields
}
diff --git a/src/utils/subgraph-types.ts b/src/utils/subgraph-types.ts
index e2313cb3..c6cb129b 100644
--- a/src/utils/subgraph-types.ts
+++ b/src/utils/subgraph-types.ts
@@ -40,29 +40,35 @@ export type SubgraphMarket = {
isActive: boolean;
canBorrowFrom: boolean;
canUseAsCollateral: boolean;
- maximumLTV: string; // BigDecimal
- liquidationThreshold: string; // BigDecimal
- liquidationPenalty: string; // BigDecimal
- createdTimestamp: string; // BigInt
- createdBlockNumber: string; // BigInt
- lltv: string; // BigInt
- irm: Address; // irmAddress
- inputToken: SubgraphToken; // collateralAsset
- inputTokenBalance: string; // BigInt (native collateral amount)
+ maximumLTV: string;
+ liquidationThreshold: string;
+ liquidationPenalty: string;
+ createdTimestamp: string;
+ createdBlockNumber: string;
+ lltv: string;
+ irm: Address;
+ inputToken: SubgraphToken;
inputTokenPriceUSD: string; // BigDecimal (collateralPrice)
borrowedToken: SubgraphToken; // loanAsset
- variableBorrowedTokenBalance: string | null; // BigInt (native borrow amount)
- totalValueLockedUSD: string; // BigDecimal (collateralAssetsUsd?)
- totalDepositBalanceUSD: string; // BigDecimal (supplyAssetsUsd)
- totalBorrowBalanceUSD: string; // BigDecimal (borrowAssetsUsd)
- totalSupplyShares: string; // BigInt (supplyShares)
+
+ // note: these 2 are weird
+ variableBorrowedTokenBalance: string | null; // updated as total Borrowed
+ inputTokenBalance: string; // updated as total Supply
+
+ totalValueLockedUSD: string;
+ totalDepositBalanceUSD: string;
+ totalBorrowBalanceUSD: string;
+ totalSupplyShares: string;
totalBorrowShares: string; // BigInt (borrowShares)
- totalSupply: string; // BigInt (supplyAssets)
- totalBorrow: string; // BigInt (borrowAssets)
- lastUpdate: string; // BigInt (timestamp)
- reserves: string; // BigDecimal
- reserveFactor: string; // BigDecimal
- fee: string; // BigInt (basis points?)
+
+ totalSupply: string;
+ totalBorrow: string;
+ totalCollateral: string;
+
+ lastUpdate: string;
+ reserves: string;
+ reserveFactor: string;
+ fee: string;
oracle: SubgraphOracle;
rates: SubgraphInterestRate[];
protocol: SubgraphProtocolInfo;
diff --git a/src/utils/tokens.ts b/src/utils/tokens.ts
index b5a9ff83..0aea2f74 100644
--- a/src/utils/tokens.ts
+++ b/src/utils/tokens.ts
@@ -530,6 +530,9 @@ const isWETH = (address: string, chainId: number) => {
return false;
};
+// Scam tokens
+const blacklistTokens = ['0xda1c2c3c8fad503662e41e324fc644dc2c5e0ccd'];
+
export {
supportedTokens,
isWETH,
@@ -541,4 +544,5 @@ export {
MORPHO_TOKEN_MAINNET,
MORPHO_LEGACY,
MORPHO_TOKEN_WRAPPER,
+ blacklistTokens,
};
diff --git a/src/utils/types.ts b/src/utils/types.ts
index 6b272562..e81ece4f 100644
--- a/src/utils/types.ts
+++ b/src/utils/types.ts
@@ -277,8 +277,8 @@ export type Market = {
state: {
borrowAssets: string;
supplyAssets: string;
- borrowAssetsUsd: string;
- supplyAssetsUsd: string;
+ borrowAssetsUsd: number;
+ supplyAssetsUsd: number;
borrowShares: string;
supplyShares: string;
liquidityAssets: string;
From 96f8e7dc6d23ccec10236d3b717be46d9c9f8b1f Mon Sep 17 00:00:00 2001
From: antoncoding
Date: Sat, 26 Apr 2025 18:28:55 +0800
Subject: [PATCH 06/20] feat: useLiquidations hooks
---
src/data-sources/morpho-api/liquidations.ts | 117 +++++++++++++++
src/data-sources/subgraph/liquidations.ts | 72 +++++++++
src/graphql/morpho-subgraph-queries.ts | 23 +++
src/hooks/useLiquidations.ts | 154 +++++++-------------
4 files changed, 267 insertions(+), 99 deletions(-)
create mode 100644 src/data-sources/morpho-api/liquidations.ts
create mode 100644 src/data-sources/subgraph/liquidations.ts
diff --git a/src/data-sources/morpho-api/liquidations.ts b/src/data-sources/morpho-api/liquidations.ts
new file mode 100644
index 00000000..7ea42db9
--- /dev/null
+++ b/src/data-sources/morpho-api/liquidations.ts
@@ -0,0 +1,117 @@
+import { URLS } from '@/utils/urls';
+import { SupportedNetworks } from '@/utils/networks';
+
+// Re-use the query structure from the original hook
+const liquidationsQuery = `
+ query getLiquidations($first: Int, $skip: Int, $chainId: Int) {
+ transactions(
+ where: { type_in: [MarketLiquidation], chainId_in: [$chainId] } # Filter by chainId
+ first: $first
+ skip: $skip
+ ) {
+ items {
+ data {
+ ... on MarketLiquidationTransactionData {
+ market {
+ uniqueKey
+ }
+ }
+ }
+ }
+ pageInfo {
+ countTotal
+ count
+ limit
+ skip
+ }
+ }
+ }
+`;
+
+type LiquidationTransactionItem = {
+ data: {
+ market?: {
+ uniqueKey: string;
+ };
+ };
+};
+
+type PageInfo = {
+ countTotal: number;
+ count: number;
+ limit: number;
+ skip: number;
+};
+
+type QueryResult = {
+ data: {
+ transactions: {
+ items: LiquidationTransactionItem[];
+ pageInfo: PageInfo;
+ };
+ };
+ errors?: any[]; // Add optional errors field
+};
+
+export const fetchMorphoApiLiquidatedMarketKeys = async (
+ network: SupportedNetworks,
+): Promise> => {
+ const liquidatedKeys = new Set();
+ let skip = 0;
+ const pageSize = 1000;
+ let totalCount = 0;
+
+ try {
+ do {
+ const response = await fetch(URLS.MORPHO_BLUE_API, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ query: liquidationsQuery,
+ variables: { first: pageSize, skip, chainId: network }, // Pass chainId
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`Morpho API request failed with status ${response.status}`);
+ }
+
+ const result = (await response.json()) as QueryResult;
+
+ // Check for GraphQL errors
+ if (result.errors) {
+ console.error('GraphQL errors:', result.errors);
+ throw new Error(`GraphQL error fetching liquidations for network ${network}`);
+ }
+
+ if (!result.data?.transactions) {
+ console.warn(`No transactions data found for network ${network} at skip ${skip}`);
+ break; // Exit loop if data structure is unexpected
+ }
+
+ const liquidations = result.data.transactions.items;
+ const pageInfo = result.data.transactions.pageInfo;
+
+ liquidations.forEach((tx) => {
+ if (tx.data?.market?.uniqueKey) {
+ liquidatedKeys.add(tx.data.market.uniqueKey);
+ }
+ });
+
+ totalCount = pageInfo.countTotal;
+ skip += pageInfo.count;
+
+ // Safety break if pageInfo.count is 0 to prevent infinite loop
+ if (pageInfo.count === 0 && skip < totalCount) {
+ console.warn('Received 0 items in a page, but not yet at total count. Breaking loop.');
+ break;
+ }
+ } while (skip < totalCount);
+ } catch (error) {
+ console.error(`Error fetching liquidations via Morpho API for network ${network}:`, error);
+ throw error; // Re-throw the error to be handled by the calling hook
+ }
+
+ console.log(`Fetched ${liquidatedKeys.size} liquidated market keys via Morpho API for ${network}.`);
+ return liquidatedKeys;
+};
\ No newline at end of file
diff --git a/src/data-sources/subgraph/liquidations.ts b/src/data-sources/subgraph/liquidations.ts
new file mode 100644
index 00000000..5e7977d5
--- /dev/null
+++ b/src/data-sources/subgraph/liquidations.ts
@@ -0,0 +1,72 @@
+import { subgraphMarketsWithLiquidationCheckQuery } from '@/graphql/morpho-subgraph-queries';
+import { SupportedNetworks } from '@/utils/networks';
+import { getSubgraphUrl } from '@/utils/subgraph-urls';
+import { blacklistTokens } from '@/utils/tokens';
+import { subgraphGraphqlFetcher } from './fetchers';
+
+// Define the expected structure of the response for the liquidation check query
+type SubgraphMarketLiquidationCheck = {
+ id: string; // Market unique key
+ liquidates: { id: string }[]; // Array will be non-empty if liquidations exist
+};
+
+type SubgraphMarketsLiquidationCheckResponse = {
+ data: {
+ markets: SubgraphMarketLiquidationCheck[];
+ };
+ errors?: any[];
+};
+
+export const fetchSubgraphLiquidatedMarketKeys = async (
+ network: SupportedNetworks,
+): Promise> => {
+ const subgraphApiUrl = getSubgraphUrl(network);
+ if (!subgraphApiUrl) {
+ console.error(`Subgraph URL for network ${network} is not defined.`);
+ throw new Error(`Subgraph URL for network ${network} is not defined.`);
+ }
+
+ const liquidatedKeys = new Set();
+
+ // Apply the same base filters as fetchSubgraphMarkets
+ const variables = {
+ first: 1000, // Fetch in batches if necessary, though unlikely needed just for IDs
+ where: {
+ inputToken_not_in: blacklistTokens,
+ },
+ };
+
+ try {
+ // Subgraph might paginate; handle if necessary, but 1000 limit is often sufficient for just IDs
+ const response = await subgraphGraphqlFetcher(
+ subgraphApiUrl,
+ subgraphMarketsWithLiquidationCheckQuery,
+ variables,
+ );
+
+ if (response.errors) {
+ console.error('GraphQL errors:', response.errors);
+ throw new Error(`GraphQL error fetching liquidated market keys for network ${network}`);
+ }
+
+ const markets = response.data?.markets;
+
+ if (!markets) {
+ console.warn(`No market data returned for liquidation check on network ${network}.`);
+ return liquidatedKeys; // Return empty set
+ }
+
+ markets.forEach((market) => {
+ // If the liquidates array has items, this market has had liquidations
+ if (market.liquidates && market.liquidates.length > 0) {
+ liquidatedKeys.add(market.id);
+ }
+ });
+ } catch (error) {
+ console.error(`Error fetching liquidated market keys via Subgraph for network ${network}:`, error);
+ throw error; // Re-throw
+ }
+
+ console.log(`Fetched ${liquidatedKeys.size} liquidated market keys via Subgraph for ${network}.`);
+ return liquidatedKeys;
+};
\ No newline at end of file
diff --git a/src/graphql/morpho-subgraph-queries.ts b/src/graphql/morpho-subgraph-queries.ts
index 3a96768f..7bfb7456 100644
--- a/src/graphql/morpho-subgraph-queries.ts
+++ b/src/graphql/morpho-subgraph-queries.ts
@@ -248,3 +248,26 @@ export const marketLiquidationsAndBadDebtQuery = `
}
`;
// --- End Query ---
+
+// --- Query to check which markets have had at least one liquidation ---
+export const subgraphMarketsWithLiquidationCheckQuery = `
+ query getSubgraphMarketsWithLiquidationCheck(
+ $first: Int,
+ $where: Market_filter,
+ ) {
+ markets(
+ first: $first,
+ where: $where,
+ orderBy: totalValueLockedUSD, # Keep ordering consistent if needed, though less relevant here
+ orderDirection: desc,
+ ) {
+ id # Market ID (uniqueKey)
+ liquidates(first: 1) { # Fetch only one liquidation event to check existence
+ id # Need any field to confirm presence
+ }
+ # Include fields needed for filtering if the 'where' clause doesn't cover everything
+ # Example: inputToken { id } if filtering by inputToken needs to happen client-side (though 'where' is better)
+ }
+ }
+`;
+// --- End Query ---
diff --git a/src/hooks/useLiquidations.ts b/src/hooks/useLiquidations.ts
index bd758048..50a04c4c 100644
--- a/src/hooks/useLiquidations.ts
+++ b/src/hooks/useLiquidations.ts
@@ -1,71 +1,8 @@
import { useState, useEffect, useCallback } from 'react';
-import { URLS } from '@/utils/urls';
-
-const liquidationsQuery = `
- query getLiquidations($first: Int, $skip: Int) {
- transactions(
- where: { type_in: [MarketLiquidation] }
- first: $first
- skip: $skip
- ) {
- items {
- id
- type
- data {
- ... on MarketLiquidationTransactionData {
- market {
- id
- uniqueKey
- }
- repaidAssets
- }
- }
- hash
- chain {
- id
- }
- }
- pageInfo {
- countTotal
- count
- limit
- skip
- }
- }
- }
-`;
-
-export type LiquidationTransaction = {
- id: string;
- type: string;
- data: {
- market: {
- id: string;
- uniqueKey: string;
- };
- repaidAssets: string;
- };
- hash: string;
- chain: {
- id: number;
- };
-};
-
-type PageInfo = {
- countTotal: number;
- count: number;
- limit: number;
- skip: number;
-};
-
-type QueryResult = {
- data: {
- transactions: {
- items: LiquidationTransaction[];
- pageInfo: PageInfo;
- };
- };
-};
+import { getMarketDataSource } from '@/config/dataSources';
+import { fetchMorphoApiLiquidatedMarketKeys } from '@/data-sources/morpho-api/liquidations';
+import { fetchSubgraphLiquidatedMarketKeys } from '@/data-sources/subgraph/liquidations';
+import { SupportedNetworks } from '@/utils/networks';
const useLiquidations = () => {
const [loading, setLoading] = useState(true);
@@ -74,48 +11,67 @@ const useLiquidations = () => {
const [error, setError] = useState(null);
const fetchLiquidations = useCallback(async (isRefetch = false) => {
+ if (isRefetch) {
+ setIsRefetching(true);
+ } else {
+ setLoading(true);
+ }
+ setError(null); // Reset error
+
+ // Define the networks to check for liquidations
+ const networksToCheck: SupportedNetworks[] = [
+ SupportedNetworks.Mainnet,
+ SupportedNetworks.Base,
+ ];
+
+ const combinedLiquidatedKeys = new Set();
+ let fetchErrors: unknown[] = [];
+
try {
- if (isRefetch) {
- setIsRefetching(true);
- } else {
- setLoading(true);
- }
- const liquidatedKeys = new Set();
- let skip = 0;
- const pageSize = 1000;
- let totalCount = 0;
+ await Promise.all(
+ networksToCheck.map(async (network) => {
+ try {
+ const dataSource = getMarketDataSource(network);
+ let networkLiquidatedKeys: Set;
- do {
- const response = await fetch(URLS.MORPHO_BLUE_API, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- query: liquidationsQuery,
- variables: { first: pageSize, skip },
- }),
- });
- const result = (await response.json()) as QueryResult;
- const liquidations = result.data.transactions.items;
- const pageInfo = result.data.transactions.pageInfo;
+ console.log(`Fetching liquidated markets for ${network} via ${dataSource}`);
- liquidations.forEach((tx) => {
- if (tx.data && 'market' in tx.data) {
- liquidatedKeys.add(tx.data.market.uniqueKey);
+ if (dataSource === 'morpho') {
+ networkLiquidatedKeys = await fetchMorphoApiLiquidatedMarketKeys(network);
+ } else if (dataSource === 'subgraph') {
+ networkLiquidatedKeys = await fetchSubgraphLiquidatedMarketKeys(network);
+ } else {
+ console.warn(`No valid data source found for network ${network} for liquidations.`);
+ networkLiquidatedKeys = new Set(); // Assume none if no source
+ }
+
+ // Add keys from this network to the combined set
+ networkLiquidatedKeys.forEach((key) => combinedLiquidatedKeys.add(key));
+ } catch (networkError) {
+ console.error(
+ `Failed to fetch liquidated market keys for network ${network}:`,
+ networkError,
+ );
+ fetchErrors.push(networkError); // Collect errors
}
- });
+ }),
+ );
- totalCount = pageInfo.countTotal;
- skip += pageInfo.count;
- } while (skip < totalCount);
+ setLiquidatedMarketKeys(combinedLiquidatedKeys);
- setLiquidatedMarketKeys(liquidatedKeys);
- } catch (_error) {
- setError(_error);
+ // Set overall error if any network fetch failed
+ if (fetchErrors.length > 0) {
+ setError(fetchErrors[0]); // Or aggregate errors if needed
+ }
+ } catch (err) {
+ // Catch potential errors from Promise.all itself
+ console.error('Overall error fetching liquidations:', err);
+ setError(err);
} finally {
setLoading(false);
setIsRefetching(false);
}
- }, []);
+ }, []); // Dependencies: None needed directly, fetchers are self-contained
useEffect(() => {
fetchLiquidations().catch(console.error);
From f2a6605dfce99b18a934efe147a4b31fc1fe7908 Mon Sep 17 00:00:00 2001
From: antoncoding
Date: Sat, 26 Apr 2025 18:29:54 +0800
Subject: [PATCH 07/20] chore: lint
---
src/data-sources/morpho-api/liquidations.ts | 8 +++++---
src/data-sources/subgraph/liquidations.ts | 7 +++++--
2 files changed, 10 insertions(+), 5 deletions(-)
diff --git a/src/data-sources/morpho-api/liquidations.ts b/src/data-sources/morpho-api/liquidations.ts
index 7ea42db9..64e99ca1 100644
--- a/src/data-sources/morpho-api/liquidations.ts
+++ b/src/data-sources/morpho-api/liquidations.ts
@@ -1,5 +1,5 @@
-import { URLS } from '@/utils/urls';
import { SupportedNetworks } from '@/utils/networks';
+import { URLS } from '@/utils/urls';
// Re-use the query structure from the original hook
const liquidationsQuery = `
@@ -112,6 +112,8 @@ export const fetchMorphoApiLiquidatedMarketKeys = async (
throw error; // Re-throw the error to be handled by the calling hook
}
- console.log(`Fetched ${liquidatedKeys.size} liquidated market keys via Morpho API for ${network}.`);
+ console.log(
+ `Fetched ${liquidatedKeys.size} liquidated market keys via Morpho API for ${network}.`,
+ );
return liquidatedKeys;
-};
\ No newline at end of file
+};
diff --git a/src/data-sources/subgraph/liquidations.ts b/src/data-sources/subgraph/liquidations.ts
index 5e7977d5..3b256473 100644
--- a/src/data-sources/subgraph/liquidations.ts
+++ b/src/data-sources/subgraph/liquidations.ts
@@ -63,10 +63,13 @@ export const fetchSubgraphLiquidatedMarketKeys = async (
}
});
} catch (error) {
- console.error(`Error fetching liquidated market keys via Subgraph for network ${network}:`, error);
+ console.error(
+ `Error fetching liquidated market keys via Subgraph for network ${network}:`,
+ error,
+ );
throw error; // Re-throw
}
console.log(`Fetched ${liquidatedKeys.size} liquidated market keys via Subgraph for ${network}.`);
return liquidatedKeys;
-};
\ No newline at end of file
+};
From bd55c9e2d6943e64f622f138a75d9dd04f768bf9 Mon Sep 17 00:00:00 2001
From: antoncoding
Date: Sat, 26 Apr 2025 19:07:16 +0800
Subject: [PATCH 08/20] feat: price estimation for markets with no USD value
---
src/contexts/MarketsContext.tsx | 4 +-
src/data-sources/morpho-api/market.ts | 3 +
src/data-sources/subgraph/market.ts | 118 +++++++++++++++++++++++---
src/utils/tokens.ts | 50 +++++++++++
src/utils/types.ts | 3 +
5 files changed, 161 insertions(+), 17 deletions(-)
diff --git a/src/contexts/MarketsContext.tsx b/src/contexts/MarketsContext.tsx
index bb4c17b6..e34e1032 100644
--- a/src/contexts/MarketsContext.tsx
+++ b/src/contexts/MarketsContext.tsx
@@ -132,15 +132,13 @@ export function MarketsProvider({ children }: MarketsProviderProps) {
}
}
},
- [liquidatedMarketKeys], // Dependencies: liquidatedMarketKeys is needed for processing
+ [liquidatedMarketKeys],
);
useEffect(() => {
if (!liquidationsLoading && markets.length === 0) {
- // Fetch markets only if liquidations are loaded and markets aren't already populated
fetchMarkets().catch(console.error);
}
- // Dependency on fetchMarkets is correct here, also depends on liquidationsLoading
}, [liquidationsLoading, fetchMarkets, markets.length]);
const refetch = useCallback(
diff --git a/src/data-sources/morpho-api/market.ts b/src/data-sources/morpho-api/market.ts
index 6e85c9ad..bb4473fb 100644
--- a/src/data-sources/morpho-api/market.ts
+++ b/src/data-sources/morpho-api/market.ts
@@ -27,6 +27,9 @@ const processMarketData = (market: Market): Market => {
...market,
warningsWithDetail,
isProtectedByLiquidationBots: false,
+
+ // Standard API always have USD price!
+ hasUSDPrice: true,
};
};
diff --git a/src/data-sources/subgraph/market.ts b/src/data-sources/subgraph/market.ts
index 567200b0..d4333ef7 100644
--- a/src/data-sources/subgraph/market.ts
+++ b/src/data-sources/subgraph/market.ts
@@ -12,10 +12,58 @@ import {
SubgraphToken,
} from '@/utils/subgraph-types';
import { getSubgraphUrl } from '@/utils/subgraph-urls';
-import { blacklistTokens } from '@/utils/tokens';
+import {
+ blacklistTokens,
+ ERC20Token,
+ findToken,
+ UnknownERC20Token,
+ TokenPeg,
+} from '@/utils/tokens';
import { WarningWithDetail, MorphoChainlinkOracleData, Market } from '@/utils/types';
import { subgraphGraphqlFetcher } from './fetchers';
+// Define the structure for the fetched prices locally
+type LocalMajorPrices = {
+ [TokenPeg.BTC]?: number;
+ [TokenPeg.ETH]?: number;
+};
+
+// Define expected type for CoinGecko API response
+type CoinGeckoPriceResponse = {
+ bitcoin?: { usd?: number };
+ ethereum?: { usd?: number };
+}
+
+// CoinGecko API endpoint
+const COINGECKO_API_URL =
+ 'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,ethereum&vs_currencies=usd';
+
+// Fetcher for major prices needed for estimation
+const fetchLocalMajorPrices = async (): Promise => {
+ try {
+ const response = await fetch(COINGECKO_API_URL);
+ if (!response.ok) {
+ throw new Error(`Internal CoinGecko API request failed with status ${response.status}`);
+ }
+ // Type the JSON response
+ const data = (await response.json()) as CoinGeckoPriceResponse;
+ const prices: LocalMajorPrices = {
+ [TokenPeg.BTC]: data.bitcoin?.usd,
+ [TokenPeg.ETH]: data.ethereum?.usd,
+ };
+ // Filter out undefined prices
+ return Object.entries(prices).reduce((acc, [key, value]) => {
+ if (value !== undefined) {
+ acc[key as keyof LocalMajorPrices] = value;
+ }
+ return acc;
+ }, {} as LocalMajorPrices);
+ } catch (err) {
+ console.error('Failed to fetch internal major token prices for subgraph estimation:', err);
+ return {}; // Return empty object on error
+ }
+};
+
// Helper to safely parse BigDecimal/BigInt strings
const safeParseFloat = (value: string | null | undefined): number => {
if (value === null || value === undefined) return 0;
@@ -38,6 +86,7 @@ const safeParseInt = (value: string | null | undefined): number => {
const transformSubgraphMarketToMarket = (
subgraphMarket: Partial,
network: SupportedNetworks,
+ majorPrices: LocalMajorPrices,
): Market => {
const marketId = subgraphMarket.id ?? '';
const lltv = subgraphMarket.lltv ?? '0';
@@ -49,6 +98,20 @@ const transformSubgraphMarketToMarket = (
const totalBorrowShares = subgraphMarket.totalBorrowShares ?? '0';
const fee = subgraphMarket.fee ?? '0';
+ // Define the estimation helper *inside* the transform function
+ // so it has access to majorPrices
+ const getEstimateValue = (token: ERC20Token | UnknownERC20Token): number | undefined => {
+ if (!('peg' in token) || token.peg === undefined) {
+ return undefined;
+ }
+ const peg = token.peg as TokenPeg;
+ if (peg === TokenPeg.USD) {
+ return 1;
+ }
+ // Access majorPrices from the outer function's scope
+ return majorPrices[peg];
+ };
+
const mapToken = (token: Partial | undefined) => ({
id: token?.id ?? '0x',
address: token?.id ?? '0x',
@@ -88,8 +151,23 @@ const transformSubgraphMarketToMarket = (
const borrowAssetsUsd = safeParseFloat(totalBorrowBalanceUSD);
// get the prices
- const loanAssetPrice = safeParseFloat(subgraphMarket.borrowedToken?.lastPriceUSD ?? '0');
- const collateralAssetPrice = safeParseFloat(subgraphMarket.inputToken?.lastPriceUSD ?? '0');
+ let loanAssetPrice = safeParseFloat(subgraphMarket.borrowedToken?.lastPriceUSD ?? '0');
+ let collateralAssetPrice = safeParseFloat(subgraphMarket.inputToken?.lastPriceUSD ?? '0');
+
+ // @todo: might update due to input token being used here
+ const hasUSDPrice = loanAssetPrice > 0 && collateralAssetPrice > 0;
+ if (!hasUSDPrice) {
+ // no price available, try to estimate
+
+ const knownLoadAsset = findToken(loanAsset.address, network);
+ if (knownLoadAsset) {
+ loanAssetPrice = getEstimateValue(knownLoadAsset) ?? 0;
+ }
+ const knownCollateralAsset = findToken(collateralAsset.address, network);
+ if (knownCollateralAsset) {
+ collateralAssetPrice = getEstimateValue(knownCollateralAsset) ?? 0;
+ }
+ }
const supplyAssetsUsd = formatBalance(supplyAssets, loanAsset.decimals) * loanAssetPrice;
@@ -144,6 +222,7 @@ const transformSubgraphMarketToMarket = (
oracle: {
data: defaultOracleData, // Placeholder oracle data
},
+ hasUSDPrice: hasUSDPrice,
isProtectedByLiquidationBots: false, // Not available from subgraph
badDebt: undefined, // Not available from subgraph
realizedBadDebt: undefined, // Not available from subgraph
@@ -179,15 +258,24 @@ export const fetchSubgraphMarket = async (
return null; // Return null if not found, hook can handle this
}
- return transformSubgraphMarketToMarket(marketData, network);
+ // Fetch major prices needed for potential estimation
+ const majorPrices = await fetchLocalMajorPrices();
+
+ return transformSubgraphMarketToMarket(marketData, network, majorPrices);
};
+// Define type for GraphQL variables
+type SubgraphMarketsVariables = {
+ first: number;
+ where?: {
+ inputToken_not_in?: string[];
+ // Add other potential filter fields here if needed
+ };
+ network?: string; // Keep network optional if sometimes omitted
+}
+
// Fetcher for multiple markets from Subgraph
-export const fetchSubgraphMarkets = async (
- network: SupportedNetworks,
- // Optional filter, adjust based on actual subgraph schema capabilities
- // filter?: { [key: string]: any },
-): Promise => {
+export const fetchSubgraphMarkets = async (network: SupportedNetworks): Promise => {
const subgraphApiUrl = getSubgraphUrl(network);
if (!subgraphApiUrl) {
@@ -196,9 +284,8 @@ export const fetchSubgraphMarkets = async (
}
// Construct variables for the query, adding blacklistTokens
- const variables: { first: number; where?: Record; network?: string } = {
+ const variables: SubgraphMarketsVariables = {
first: 1000, // Max limit
- // If filtering is needed and supported by the schema, add it here
where: {
inputToken_not_in: blacklistTokens,
},
@@ -208,7 +295,7 @@ export const fetchSubgraphMarkets = async (
const response = await subgraphGraphqlFetcher( // Use the new response type
subgraphApiUrl,
subgraphMarketsQuery, // Use the new query
- variables,
+ variables as unknown as Record, // Convert via unknown
);
// Assuming the response structure matches the single market query for the list
@@ -219,6 +306,9 @@ export const fetchSubgraphMarkets = async (
return []; // Return empty array if no markets or error
}
- // Transform each market
- return marketsData.map((market) => transformSubgraphMarketToMarket(market, network));
+ // Fetch major prices *once* before transforming all markets
+ const majorPrices = await fetchLocalMajorPrices();
+
+ // Transform each market using the fetched prices
+ return marketsData.map((market) => transformSubgraphMarketToMarket(market, network, majorPrices));
};
diff --git a/src/utils/tokens.ts b/src/utils/tokens.ts
index 0aea2f74..8d89cdf7 100644
--- a/src/utils/tokens.ts
+++ b/src/utils/tokens.ts
@@ -7,6 +7,13 @@ export type SingleChainERC20Basic = {
address: string;
};
+// a token can be "linked" to a pegged asset, we use this to estimate the USD value for markets if it's not presented.
+export enum TokenPeg {
+ USD = 'USD',
+ ETH = 'ETH',
+ BTC = 'BTC',
+}
+
export type ERC20Token = {
symbol: string;
img: string | undefined;
@@ -16,6 +23,9 @@ export type ERC20Token = {
name: string;
};
isFactoryToken?: boolean;
+
+ // this is not a "hard peg", instead only used for market supply / borrow USD value estimation
+ peg?: TokenPeg;
};
export type UnknownERC20Token = {
@@ -42,12 +52,14 @@ const supportedTokens = [
{ chain: mainnet, address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' },
{ chain: base, address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' },
],
+ peg: TokenPeg.USD,
},
{
symbol: 'USDT',
img: require('../imgs/tokens/usdt.webp') as string,
decimals: 6,
networks: [{ chain: mainnet, address: '0xdac17f958d2ee523a2206206994597c13d831ec7' }],
+ peg: TokenPeg.USD,
},
{
symbol: 'eUSD',
@@ -61,18 +73,21 @@ const supportedTokens = [
name: 'Reserve',
isProxy: true,
},
+ peg: TokenPeg.USD,
},
{
symbol: 'USDA',
img: require('../imgs/tokens/usda.png') as string,
decimals: 6,
networks: [{ chain: mainnet, address: '0x0000206329b97DB379d5E1Bf586BbDB969C63274' }],
+ peg: TokenPeg.USD,
},
{
symbol: 'USD0',
img: require('../imgs/tokens/usd0.png') as string,
decimals: 18,
networks: [{ chain: mainnet, address: '0x73A15FeD60Bf67631dC6cd7Bc5B6e8da8190aCF5' }],
+ peg: TokenPeg.USD,
},
{
symbol: 'USD0++',
@@ -83,6 +98,7 @@ const supportedTokens = [
name: 'Usual',
isProxy: true,
},
+ peg: TokenPeg.USD,
},
{
symbol: 'hyUSD',
@@ -93,18 +109,21 @@ const supportedTokens = [
name: 'Resolve',
isProxy: true,
},
+ peg: TokenPeg.USD,
},
{
symbol: 'crvUSD',
img: require('../imgs/tokens/crvusd.png') as string,
decimals: 18,
networks: [{ chain: mainnet, address: '0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E' }],
+ peg: TokenPeg.USD,
},
{
symbol: 'USDe',
img: require('../imgs/tokens/usde.png') as string,
decimals: 18,
networks: [{ chain: mainnet, address: '0x4c9EDD5852cd905f086C759E8383e09bff1E68B3' }],
+ peg: TokenPeg.USD,
},
{
symbol: 'sUSDe',
@@ -117,12 +136,14 @@ const supportedTokens = [
img: require('../imgs/tokens/frax.webp') as string,
decimals: 18,
networks: [{ chain: mainnet, address: '0x853d955acef822db058eb8505911ed77f175b99e' }],
+ peg: TokenPeg.USD,
},
{
symbol: 'PYUSD',
img: require('../imgs/tokens/pyusd.png') as string,
decimals: 6,
networks: [{ chain: mainnet, address: '0x6c3ea9036406852006290770bedfcaba0e23a0e8' }],
+ peg: TokenPeg.USD,
},
{
symbol: 'aUSD',
@@ -133,12 +154,14 @@ const supportedTokens = [
name: 'Agora',
isProxy: true,
},
+ peg: TokenPeg.USD,
},
{
symbol: 'sUSDS',
img: require('../imgs/tokens/susds.svg') as string,
decimals: 18,
networks: [{ chain: base, address: '0x5875eee11cf8398102fdad704c9e96607675467a' }],
+ peg: TokenPeg.USD,
},
{
symbol: 'wUSDM',
@@ -148,6 +171,7 @@ const supportedTokens = [
{ chain: mainnet, address: '0x57F5E098CaD7A3D1Eed53991D4d66C45C9AF7812' },
{ chain: base, address: '0x57F5E098CaD7A3D1Eed53991D4d66C45C9AF7812' },
],
+ peg: TokenPeg.USD,
},
{
symbol: 'EURe',
@@ -172,6 +196,7 @@ const supportedTokens = [
{ chain: mainnet, address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' },
{ chain: base, address: '0x4200000000000000000000000000000000000006' },
],
+ peg: TokenPeg.ETH,
},
{
symbol: 'sDAI',
@@ -187,24 +212,28 @@ const supportedTokens = [
{ chain: mainnet, address: '0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0' },
{ chain: base, address: '0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452' },
],
+ peg: TokenPeg.ETH,
},
{
symbol: 'cbETH',
img: require('../imgs/tokens/cbeth.png') as string,
decimals: 18,
networks: [{ address: '0x2Ae3F1Ec7F1F5012CFEab0185bfc7aa3cf0DEc22', chain: base }],
+ peg: TokenPeg.ETH,
},
{
symbol: 'DAI',
img: require('../imgs/tokens/dai.webp') as string,
decimals: 18,
networks: [{ chain: mainnet, address: '0x6B175474E89094C44Da98b954EedeAC495271d0F' }],
+ peg: TokenPeg.USD,
},
{
symbol: 'gtWETH',
img: undefined,
decimals: 18,
networks: [{ chain: mainnet, address: '0x2371e134e3455e0593363cBF89d3b6cf53740618' }],
+ peg: TokenPeg.ETH,
},
{
symbol: 'XPC',
@@ -217,12 +246,14 @@ const supportedTokens = [
img: require('../imgs/tokens/oseth.png') as string,
decimals: 18,
networks: [{ chain: mainnet, address: '0xf1C9acDc66974dFB6dEcB12aA385b9cD01190E38' }],
+ peg: TokenPeg.ETH,
},
{
symbol: 'WBTC',
img: require('../imgs/tokens/wbtc.png') as string,
decimals: 8,
networks: [{ chain: mainnet, address: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599' }],
+ peg: TokenPeg.BTC,
},
{
symbol: 'cbBTC',
@@ -232,12 +263,14 @@ const supportedTokens = [
{ chain: mainnet, address: '0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf' },
{ chain: base, address: '0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf' },
],
+ peg: TokenPeg.BTC,
},
{
symbol: 'tBTC',
img: require('../imgs/tokens/tbtc.webp') as string,
decimals: 8,
networks: [{ chain: mainnet, address: '0x18084fbA666a33d37592fA2633fD49a74DD93a88' }],
+ peg: TokenPeg.BTC,
},
{
symbol: 'lBTC',
@@ -247,18 +280,21 @@ const supportedTokens = [
{ chain: mainnet, address: '0x8236a87084f8B84306f72007F36F2618A5634494' },
{ chain: base, address: '0xecAc9C5F704e954931349Da37F60E39f515c11c1' },
],
+ peg: TokenPeg.BTC,
},
{
symbol: 'eBTC',
img: require('../imgs/tokens/ebtc.webp') as string,
decimals: 8,
networks: [{ chain: mainnet, address: '0x657e8C867D8B37dCC18fA4Caead9C45EB088C642' }],
+ peg: TokenPeg.BTC,
},
{
symbol: 'rsETH',
img: require('../imgs/tokens/rseth.png') as string,
decimals: 18,
networks: [{ chain: mainnet, address: '0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7' }],
+ peg: TokenPeg.ETH,
},
{
symbol: 'MKR',
@@ -274,12 +310,14 @@ const supportedTokens = [
{ chain: mainnet, address: '0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee' },
{ chain: base, address: '0x04C0599Ae5A44757c0af6F9eC3b93da8976c150A' },
],
+ peg: TokenPeg.ETH,
},
{
symbol: 'apxETH',
img: require('../imgs/tokens/apxeth.png') as string,
decimals: 18,
networks: [{ chain: mainnet, address: '0x9Ba021B0a9b958B5E75cE9f6dff97C7eE52cb3E6' }],
+ peg: TokenPeg.ETH,
},
{
symbol: 'bsdETH',
@@ -290,6 +328,7 @@ const supportedTokens = [
name: 'Reserve',
isProxy: true,
},
+ peg: TokenPeg.ETH,
},
{
symbol: 'ETH+',
@@ -300,6 +339,7 @@ const supportedTokens = [
name: 'Reserve',
isProxy: true,
},
+ peg: TokenPeg.ETH,
},
{
symbol: 'LDO',
@@ -315,6 +355,7 @@ const supportedTokens = [
{ chain: mainnet, address: '0xae78736Cd615f374D3085123A210448E74Fc6393' },
{ chain: base, address: '0xB6fe221Fe9EeF5aBa221c348bA20A1Bf5e73624c' },
],
+ peg: TokenPeg.ETH,
},
{
symbol: 'ezETH',
@@ -327,6 +368,7 @@ const supportedTokens = [
address: '0x2416092f143378750bb29b79eD961ab195CcEea5',
},
],
+ peg: TokenPeg.ETH,
},
{
symbol: 'stEUR',
@@ -339,6 +381,7 @@ const supportedTokens = [
img: require('../imgs/tokens/crv.webp') as string,
decimals: 18,
networks: [{ chain: mainnet, address: '0xD533a949740bb3306d119CC777fa900bA034cd52' }],
+ peg: TokenPeg.USD,
},
{
symbol: 'DEGEN',
@@ -357,6 +400,7 @@ const supportedTokens = [
img: require('../imgs/tokens/usyc.png') as string,
decimals: 18,
networks: [{ chain: mainnet, address: '0x136471a34f6ef19fE571EFFC1CA711fdb8E49f2b' }],
+ peg: TokenPeg.USD,
},
{
symbol: 'USDz',
@@ -366,24 +410,28 @@ const supportedTokens = [
{ chain: mainnet, address: '0xA469B7Ee9ee773642b3e93E842e5D9b5BaA10067' },
{ chain: base, address: '0x04D5ddf5f3a8939889F11E97f8c4BB48317F1938' },
],
+ peg: TokenPeg.USD,
},
{
symbol: 'wUSDL',
img: require('../imgs/tokens/wusdl.webp') as string,
decimals: 18,
networks: [{ chain: mainnet, address: '0x7751E2F4b8ae93EF6B79d86419d42FE3295A4559' }],
+ peg: TokenPeg.USD,
},
{
symbol: 'pufETH',
img: require('../imgs/tokens/pufETH.webp') as string,
decimals: 18,
networks: [{ chain: mainnet, address: '0xD9A442856C234a39a81a089C06451EBAa4306a72' }],
+ peg: TokenPeg.ETH,
},
{
symbol: 'rswETH',
img: require('../imgs/tokens/rsweth.webp') as string,
decimals: 18,
networks: [{ chain: mainnet, address: '0xFAe103DC9cf190eD75350761e95403b7b8aFa6c0' }],
+ peg: TokenPeg.ETH,
},
{
symbol: 'UNI',
@@ -413,6 +461,7 @@ const supportedTokens = [
{ chain: mainnet, address: '0x66a1E37c9b0eAddca17d3662D6c05F4DECf3e110' },
{ chain: base, address: '0x35E5dB674D8e93a03d814FA0ADa70731efe8a4b9' },
],
+ peg: TokenPeg.USD,
},
{
symbol: 'EIGEN',
@@ -425,6 +474,7 @@ const supportedTokens = [
img: require('../imgs/tokens/wsuperOETHb.png') as string,
decimals: 18,
networks: [{ chain: base, address: '0x7FcD174E80f264448ebeE8c88a7C4476AAF58Ea6' }],
+ peg: TokenPeg.ETH,
},
{
symbol: 'uSOL',
diff --git a/src/utils/types.ts b/src/utils/types.ts
index e81ece4f..07ad0012 100644
--- a/src/utils/types.ts
+++ b/src/utils/types.ts
@@ -292,6 +292,9 @@ export type Market = {
timestamp: number;
rateAtUTarget: number;
};
+
+ // whether we have USD price such has supplyUSD, borrowUSD, collateralUSD, etc. If not, use estimationP
+ hasUSDPrice: boolean;
warnings: MarketWarning[];
badDebt?: {
underlying: number;
From 5c539b4b056079aad976bad0c96e265d4927b3f2 Mon Sep 17 00:00:00 2001
From: antoncoding
Date: Sat, 26 Apr 2025 20:34:39 +0800
Subject: [PATCH 09/20] feat: history
---
src/contexts/MarketsContext.tsx | 4 -
src/data-sources/morpho-api/transactions.ts | 75 +++++++
src/data-sources/subgraph/market.ts | 4 +-
src/data-sources/subgraph/queries.ts | 0
src/data-sources/subgraph/transactions.ts | 212 ++++++++++++++++++++
src/data-sources/subgraph/types.ts | 65 ++++++
src/graphql/morpho-subgraph-queries.ts | 149 +++++++++++---
src/hooks/useMarketData.ts | 17 +-
src/hooks/useUserTransactions.ts | 194 +++++++++++++-----
9 files changed, 625 insertions(+), 95 deletions(-)
create mode 100644 src/data-sources/morpho-api/transactions.ts
create mode 100644 src/data-sources/subgraph/queries.ts
create mode 100644 src/data-sources/subgraph/transactions.ts
create mode 100644 src/data-sources/subgraph/types.ts
diff --git a/src/contexts/MarketsContext.tsx b/src/contexts/MarketsContext.tsx
index e34e1032..56ab9ad2 100644
--- a/src/contexts/MarketsContext.tsx
+++ b/src/contexts/MarketsContext.tsx
@@ -80,10 +80,6 @@ export function MarketsProvider({ children }: MarketsProviderProps) {
console.warn(`No valid data source found for network ${network}`);
}
- if (network === SupportedNetworks.Mainnet) {
- console.log('networkMarkets', networkMarkets);
- }
-
combinedMarkets.push(...networkMarkets);
} catch (networkError) {
console.error(`Failed to fetch markets for network ${network}:`, networkError);
diff --git a/src/data-sources/morpho-api/transactions.ts b/src/data-sources/morpho-api/transactions.ts
new file mode 100644
index 00000000..a755c422
--- /dev/null
+++ b/src/data-sources/morpho-api/transactions.ts
@@ -0,0 +1,75 @@
+import { userTransactionsQuery } from '@/graphql/morpho-api-queries';
+import { TransactionFilters, TransactionResponse } from '@/hooks/useUserTransactions';
+import { SupportedNetworks } from '@/utils/networks';
+import { URLS } from '@/utils/urls';
+
+export const fetchMorphoTransactions = async (
+ filters: TransactionFilters,
+): Promise => {
+ // Conditionally construct the 'where' object
+ const whereClause: Record = {
+ userAddress_in: filters.userAddress, // Assuming this is always required
+ // Default chainIds if none are provided in filters for Morpho API call context
+ chainId_in: filters.chainIds ?? [SupportedNetworks.Base, SupportedNetworks.Mainnet],
+ };
+
+ if (filters.marketUniqueKeys && filters.marketUniqueKeys.length > 0) {
+ whereClause.marketUniqueKey_in = filters.marketUniqueKeys;
+ }
+ if (filters.timestampGte !== undefined && filters.timestampGte !== null) {
+ whereClause.timestamp_gte = filters.timestampGte;
+ }
+ if (filters.timestampLte !== undefined && filters.timestampLte !== null) {
+ whereClause.timestamp_lte = filters.timestampLte;
+ }
+ if (filters.hash) {
+ whereClause.hash = filters.hash;
+ }
+ if (filters.assetIds && filters.assetIds.length > 0) {
+ whereClause.assetId_in = filters.assetIds;
+ }
+
+ try {
+ const response = await fetch(URLS.MORPHO_BLUE_API, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ query: userTransactionsQuery,
+ variables: {
+ where: whereClause, // Use the conditionally built 'where' clause
+ first: filters.first ?? 1000,
+ skip: filters.skip ?? 0,
+ },
+ }),
+ });
+
+ const result = (await response.json()) as {
+ data?: { transactions?: TransactionResponse };
+ errors?: { message: string }[];
+ };
+
+ if (result.errors) {
+ throw new Error(result.errors.map((e) => e.message).join(', '));
+ }
+
+ const transactions = result.data?.transactions;
+ if (!transactions) {
+ return {
+ items: [],
+ pageInfo: { count: 0, countTotal: 0 },
+ error: 'No transaction data received from Morpho API',
+ };
+ }
+
+ return transactions;
+ } catch (err) {
+ console.error('Error fetching Morpho API transactions:', err);
+ return {
+ items: [],
+ pageInfo: { count: 0, countTotal: 0 },
+ error: err instanceof Error ? err.message : 'Unknown Morpho API error occurred',
+ };
+ }
+};
diff --git a/src/data-sources/subgraph/market.ts b/src/data-sources/subgraph/market.ts
index d4333ef7..90686e9e 100644
--- a/src/data-sources/subgraph/market.ts
+++ b/src/data-sources/subgraph/market.ts
@@ -32,7 +32,7 @@ type LocalMajorPrices = {
type CoinGeckoPriceResponse = {
bitcoin?: { usd?: number };
ethereum?: { usd?: number };
-}
+};
// CoinGecko API endpoint
const COINGECKO_API_URL =
@@ -272,7 +272,7 @@ type SubgraphMarketsVariables = {
// Add other potential filter fields here if needed
};
network?: string; // Keep network optional if sometimes omitted
-}
+};
// Fetcher for multiple markets from Subgraph
export const fetchSubgraphMarkets = async (network: SupportedNetworks): Promise => {
diff --git a/src/data-sources/subgraph/queries.ts b/src/data-sources/subgraph/queries.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/data-sources/subgraph/transactions.ts b/src/data-sources/subgraph/transactions.ts
new file mode 100644
index 00000000..979e4f25
--- /dev/null
+++ b/src/data-sources/subgraph/transactions.ts
@@ -0,0 +1,212 @@
+import { subgraphUserTransactionsQuery } from '@/graphql/morpho-subgraph-queries';
+import { TransactionFilters, TransactionResponse } from '@/hooks/useUserTransactions';
+import { SupportedNetworks } from '@/utils/networks';
+import { getSubgraphUrl } from '@/utils/subgraph-urls';
+import { UserTransaction, UserTxTypes } from '@/utils/types';
+import {
+ SubgraphAccountData,
+ SubgraphBorrowTx,
+ SubgraphDepositTx,
+ SubgraphLiquidationTx,
+ SubgraphRepayTx,
+ SubgraphTransactionResponse,
+ SubgraphWithdrawTx,
+} from './types';
+
+const transformSubgraphTransactions = (
+ subgraphData: SubgraphAccountData,
+ filters: TransactionFilters,
+): TransactionResponse => {
+ const allTransactions: UserTransaction[] = [];
+
+ subgraphData.deposits.forEach((tx: SubgraphDepositTx) => {
+ const type = tx.isCollateral ? UserTxTypes.MarketSupplyCollateral : UserTxTypes.MarketSupply;
+ allTransactions.push({
+ hash: tx.hash,
+ timestamp: parseInt(tx.timestamp, 10),
+ type: type,
+ data: {
+ __typename: type,
+ shares: tx.shares,
+ assets: tx.amount,
+ market: {
+ uniqueKey: tx.market.id,
+ },
+ },
+ });
+ });
+
+ subgraphData.withdraws.forEach((tx: SubgraphWithdrawTx) => {
+ const type = tx.isCollateral
+ ? UserTxTypes.MarketWithdrawCollateral
+ : UserTxTypes.MarketWithdraw;
+ allTransactions.push({
+ hash: tx.hash,
+ timestamp: parseInt(tx.timestamp, 10),
+ type: type,
+ data: {
+ __typename: type,
+ shares: tx.shares,
+ assets: tx.amount,
+ market: {
+ uniqueKey: tx.market.id,
+ },
+ },
+ });
+ });
+
+ subgraphData.borrows.forEach((tx: SubgraphBorrowTx) => {
+ allTransactions.push({
+ hash: tx.hash,
+ timestamp: parseInt(tx.timestamp, 10),
+ type: UserTxTypes.MarketBorrow,
+ data: {
+ __typename: UserTxTypes.MarketBorrow,
+ shares: tx.shares,
+ assets: tx.amount,
+ market: {
+ uniqueKey: tx.market.id,
+ },
+ },
+ });
+ });
+
+ subgraphData.repays.forEach((tx: SubgraphRepayTx) => {
+ allTransactions.push({
+ hash: tx.hash,
+ timestamp: parseInt(tx.timestamp, 10),
+ type: UserTxTypes.MarketRepay,
+ data: {
+ __typename: UserTxTypes.MarketRepay,
+ shares: tx.shares,
+ assets: tx.amount,
+ market: {
+ uniqueKey: tx.market.id,
+ },
+ },
+ });
+ });
+
+ subgraphData.liquidations.forEach((tx: SubgraphLiquidationTx) => {
+ allTransactions.push({
+ hash: tx.hash,
+ timestamp: parseInt(tx.timestamp, 10),
+ type: UserTxTypes.MarketLiquidation,
+ data: {
+ __typename: UserTxTypes.MarketLiquidation,
+ shares: '0',
+ assets: tx.repaid,
+ market: {
+ uniqueKey: tx.market.id,
+ },
+ },
+ });
+ });
+
+ allTransactions.sort((a, b) => b.timestamp - a.timestamp);
+
+ const filteredTransactions = filters.marketUniqueKeys
+ ? allTransactions.filter((tx) => filters.marketUniqueKeys?.includes(tx.data.market.uniqueKey))
+ : allTransactions;
+
+ const count = filteredTransactions.length;
+ const countTotal = count;
+
+ return {
+ items: filteredTransactions,
+ pageInfo: {
+ count: count,
+ countTotal: countTotal,
+ },
+ error: null,
+ };
+};
+
+export const fetchSubgraphTransactions = async (
+ filters: TransactionFilters,
+ network: SupportedNetworks,
+): Promise => {
+ if (filters.userAddress.length !== 1) {
+ console.warn('Subgraph fetcher currently supports only one user address.');
+ return {
+ items: [],
+ pageInfo: { count: 0, countTotal: 0 },
+ error: null,
+ };
+ }
+
+ const subgraphUrl = getSubgraphUrl(network);
+
+ if (!subgraphUrl) {
+ const errorMsg = `Subgraph URL not found for network ${network}. Check API key and configuration.`;
+ console.error(errorMsg);
+ return {
+ items: [],
+ pageInfo: { count: 0, countTotal: 0 },
+ error: errorMsg,
+ };
+ }
+
+ const userAddress = filters.userAddress[0].toLowerCase();
+
+ // Always calculate current timestamp (seconds)
+ const currentTimestamp = Math.floor(Date.now() / 1000);
+
+ // Construct variables with mandatory timestamp filters
+ const variables: Record = {
+ userId: userAddress,
+ first: filters.first ?? 1000,
+ skip: filters.skip ?? 0,
+ timestamp_gt: 0, // Always start from time 0
+ timestamp_lt: currentTimestamp, // Always end at current time
+ };
+
+ if (filters.timestampGte !== undefined && filters.timestampGte !== null) {
+ variables.timestamp_gte = filters.timestampGte;
+ }
+ if (filters.timestampLte !== undefined && filters.timestampLte !== null) {
+ variables.timestamp_lte = filters.timestampLte;
+ }
+
+ const requestBody = {
+ query: subgraphUserTransactionsQuery,
+ variables: variables,
+ };
+
+ // Log the URL and body before sending
+ console.log('Subgraph Request URL:', subgraphUrl);
+ console.log('Subgraph Request Body:', JSON.stringify(requestBody));
+
+ try {
+ const response = await fetch(subgraphUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(requestBody),
+ });
+
+ const result = (await response.json()) as SubgraphTransactionResponse;
+
+ if (result.errors) {
+ throw new Error(result.errors.map((e) => e.message).join(', '));
+ }
+
+ if (!result.data?.account) {
+ return {
+ items: [],
+ pageInfo: { count: 0, countTotal: 0 },
+ error: null,
+ };
+ }
+
+ return transformSubgraphTransactions(result.data.account, filters);
+ } catch (err) {
+ console.error(`Error fetching Subgraph transactions from ${subgraphUrl}:`, err);
+ return {
+ items: [],
+ pageInfo: { count: 0, countTotal: 0 },
+ error: err instanceof Error ? err.message : 'Unknown Subgraph error occurred',
+ };
+ }
+};
diff --git a/src/data-sources/subgraph/types.ts b/src/data-sources/subgraph/types.ts
new file mode 100644
index 00000000..e7448ec5
--- /dev/null
+++ b/src/data-sources/subgraph/types.ts
@@ -0,0 +1,65 @@
+import { Address } from 'viem';
+
+type SubgraphAsset = {
+ id: string; // Asset address
+ symbol?: string; // Optional symbol
+ decimals?: number; // Optional decimals
+};
+
+type SubgraphMarketReference = {
+ id: string; // Market unique key
+};
+
+type SubgraphAccountReference = {
+ id: Address;
+};
+
+type SubgraphBaseTx = {
+ id: string; // Transaction ID (e.g., hash + log index)
+ hash: string; // Transaction hash
+ timestamp: string; // Timestamp string (needs conversion to number)
+ market: SubgraphMarketReference; // Reference to the market
+ asset: SubgraphAsset; // Reference to the asset involved
+ amount: string; // Amount of the asset (loan/collateral)
+ shares: string; // Amount in shares
+ accountActor?: SubgraphAccountReference; // Optional: msg.sender for deposits etc.
+};
+
+export type SubgraphDepositTx = SubgraphBaseTx & {
+ isCollateral: boolean; // True for SupplyCollateral, False for Supply
+};
+
+export type SubgraphWithdrawTx = SubgraphBaseTx & {
+ isCollateral: boolean; // True for WithdrawCollateral, False for Withdraw
+};
+
+export type SubgraphBorrowTx = SubgraphBaseTx;
+
+export type SubgraphRepayTx = SubgraphBaseTx;
+
+export type SubgraphLiquidationTx = {
+ id: string;
+ hash: string;
+ timestamp: string;
+ market: SubgraphMarketReference;
+ liquidator: SubgraphAccountReference; // The account calling liquidate
+ amount: string; // Collateral seized amount (string)
+ repaid: string; // Debt repaid amount (string)
+};
+
+// Structure based on the example query { account(id: ...) { ... } }
+export type SubgraphAccountData = {
+ deposits: SubgraphDepositTx[];
+ withdraws: SubgraphWithdrawTx[];
+ borrows: SubgraphBorrowTx[];
+ repays: SubgraphRepayTx[];
+ liquidations: SubgraphLiquidationTx[]; // Assuming liquidations where user was liquidated
+};
+
+// The full response structure from the subgraph query
+export type SubgraphTransactionResponse = {
+ data: {
+ account: SubgraphAccountData | null;
+ };
+ errors?: { message: string }[];
+};
diff --git a/src/graphql/morpho-subgraph-queries.ts b/src/graphql/morpho-subgraph-queries.ts
index 7bfb7456..2eb91b91 100644
--- a/src/graphql/morpho-subgraph-queries.ts
+++ b/src/graphql/morpho-subgraph-queries.ts
@@ -162,9 +162,7 @@ export const marketDepositsWithdrawsQuery = `
where: { market: $marketId, asset: $loanAssetId }
) {
amount
- account {
- id
- }
+ account { id }
timestamp
hash
}
@@ -175,9 +173,7 @@ export const marketDepositsWithdrawsQuery = `
where: { market: $marketId, asset: $loanAssetId }
) {
amount
- account {
- id
- }
+ account { id }
timestamp
hash
}
@@ -195,9 +191,7 @@ export const marketBorrowsRepaysQuery = `
where: { market: $marketId, asset: $loanAssetId }
) {
amount
- account {
- id
- }
+ account { id }
timestamp
hash
}
@@ -208,9 +202,7 @@ export const marketBorrowsRepaysQuery = `
where: { market: $marketId, asset: $loanAssetId }
) {
amount
- account {
- id
- }
+ account { id }
timestamp
hash
}
@@ -227,23 +219,19 @@ export const marketLiquidationsAndBadDebtQuery = `
orderBy: timestamp,
orderDirection: desc
) {
- id # ID of the liquidate event itself
+ id
hash
timestamp
- repaid # Amount of loan asset repaid
- amount # Amount of collateral seized
- liquidator {
- id
- }
+ repaid
+ amount
+ liquidator { id }
}
badDebtRealizations(
first: 1000,
where: { market: $marketId }
) {
badDebt
- liquidation {
- id
- }
+ liquidation { id }
}
}
`;
@@ -258,16 +246,123 @@ export const subgraphMarketsWithLiquidationCheckQuery = `
markets(
first: $first,
where: $where,
- orderBy: totalValueLockedUSD, # Keep ordering consistent if needed, though less relevant here
+ orderBy: totalValueLockedUSD,
orderDirection: desc,
) {
id # Market ID (uniqueKey)
- liquidates(first: 1) { # Fetch only one liquidation event to check existence
- id # Need any field to confirm presence
+ liquidates(first: 1) { # Fetch only one to check existence
+ id
+ }
+ }
+ }
+`;
+
+// Note: The exact field names might need adjustment based on the specific Subgraph schema.
+export const subgraphUserTransactionsQuery = `
+ query GetUserTransactions(
+ $userId: ID!
+ $first: Int!
+ $skip: Int!
+ $timestamp_gt: BigInt! # Always filter from timestamp 0
+ $timestamp_lt: BigInt! # Always filter up to current time
+ ) {
+ account(id: $userId) {
+ deposits(
+ first: $first
+ skip: $skip
+ orderBy: timestamp
+ orderDirection: desc
+ where: {
+ timestamp_gt: $timestamp_gt
+ timestamp_lt: $timestamp_lt
+ }
+ ) {
+ id
+ hash
+ timestamp
+ isCollateral
+ market { id }
+ asset { id }
+ amount
+ shares
+ accountActor { id }
+ }
+ withdraws(
+ first: $first
+ skip: $skip
+ orderBy: timestamp
+ orderDirection: desc
+ where: {
+ timestamp_gt: $timestamp_gt
+ timestamp_lt: $timestamp_lt
+ }
+ ) {
+ id
+ hash
+ timestamp
+ isCollateral
+ market { id }
+ asset { id }
+ amount
+ shares
+ accountActor { id }
+ }
+ borrows(
+ first: $first
+ skip: $skip
+ orderBy: timestamp
+ orderDirection: desc
+ where: {
+ timestamp_gt: $timestamp_gt
+ timestamp_lt: $timestamp_lt
+ }
+ ) {
+ id
+ hash
+ timestamp
+ market { id }
+ asset { id }
+ amount
+ shares
+ accountActor { id }
+ }
+ repays(
+ first: $first
+ skip: $skip
+ orderBy: timestamp
+ orderDirection: desc
+ where: {
+ timestamp_gt: $timestamp_gt
+ timestamp_lt: $timestamp_lt
+ }
+ ) {
+ id
+ hash
+ timestamp
+ market { id }
+ asset { id }
+ amount
+ shares
+ accountActor { id }
+ }
+ liquidations(
+ first: $first
+ skip: $skip
+ orderBy: timestamp
+ orderDirection: desc
+ where: {
+ timestamp_gt: $timestamp_gt
+ timestamp_lt: $timestamp_lt
+ }
+ ) {
+ id
+ hash
+ timestamp
+ market { id }
+ liquidator { id }
+ amount # Collateral seized
+ repaid # Debt repaid
}
- # Include fields needed for filtering if the 'where' clause doesn't cover everything
- # Example: inputToken { id } if filtering by inputToken needs to happen client-side (though 'where' is better)
}
}
`;
-// --- End Query ---
diff --git a/src/hooks/useMarketData.ts b/src/hooks/useMarketData.ts
index 0668422a..869aca61 100644
--- a/src/hooks/useMarketData.ts
+++ b/src/hooks/useMarketData.ts
@@ -11,42 +11,35 @@ export const useMarketData = (
) => {
const queryKey = ['marketData', uniqueKey, network];
- // Determine the data source
const dataSource = network ? getMarketDataSource(network) : null;
const { data, isLoading, error, refetch } = useQuery({
- // Allow null return
queryKey: queryKey,
queryFn: async (): Promise => {
- // Guard clauses
if (!uniqueKey || !network || !dataSource) {
- return null; // Return null if prerequisites aren't met
+ return null;
}
console.log(`Fetching market data for ${uniqueKey} on ${network} via ${dataSource}`);
- // Fetch based on the determined data source
try {
if (dataSource === 'morpho') {
return await fetchMorphoMarket(uniqueKey, network);
} else if (dataSource === 'subgraph') {
- // fetchSubgraphMarket already handles potential null return
return await fetchSubgraphMarket(uniqueKey, network);
}
} catch (fetchError) {
console.error(`Failed to fetch market data via ${dataSource}:`, fetchError);
- return null; // Return null on fetch error
+ return null;
}
- // Fallback if dataSource logic is somehow incorrect
console.warn('Unknown market data source determined');
return null;
},
- // Enable query only if all parameters are present AND a valid data source exists
enabled: !!uniqueKey && !!network && !!dataSource,
- staleTime: 1000 * 60 * 5, // 5 minutes
+ staleTime: 1000 * 60 * 5,
placeholderData: (previousData) => previousData ?? null,
- retry: 1, // Optional: retry once on failure
+ retry: 1,
});
return {
@@ -54,6 +47,6 @@ export const useMarketData = (
isLoading: isLoading,
error: error,
refetch: refetch,
- dataSource: dataSource, // Expose the determined data source
+ dataSource: dataSource,
};
};
diff --git a/src/hooks/useUserTransactions.ts b/src/hooks/useUserTransactions.ts
index 88ee588a..74689167 100644
--- a/src/hooks/useUserTransactions.ts
+++ b/src/hooks/useUserTransactions.ts
@@ -1,13 +1,14 @@
import { useState, useCallback } from 'react';
-import { userTransactionsQuery } from '@/graphql/morpho-api-queries';
-import { SupportedNetworks } from '@/utils/networks';
+import { getMarketDataSource } from '@/config/dataSources';
+import { fetchMorphoTransactions } from '@/data-sources/morpho-api/transactions';
+import { fetchSubgraphTransactions } from '@/data-sources/subgraph/transactions';
+import { SupportedNetworks, isSupportedChain } from '@/utils/networks';
import { UserTransaction } from '@/utils/types';
-import { URLS } from '@/utils/urls';
export type TransactionFilters = {
- userAddress: string[];
+ userAddress: string[]; // Expecting only one for subgraph compatibility
marketUniqueKeys?: string[];
- chainIds?: number[];
+ chainIds?: number[]; // Optional: If provided, fetch only from these chains
timestampGte?: number;
timestampLte?: number;
skip?: number;
@@ -19,67 +20,160 @@ export type TransactionFilters = {
export type TransactionResponse = {
items: UserTransaction[];
pageInfo: {
- count: number;
- countTotal: number;
+ count: number; // Count of items *in the current page* after client-side pagination
+ countTotal: number; // Estimated total count across all sources
};
error: string | null;
};
+// Define a default limit for fetching from each source when combining
+const MAX_ITEMS_PER_SOURCE = 1000;
+
const useUserTransactions = () => {
const [loading, setLoading] = useState(false);
- const [error, setError] = useState(null);
+ const [error, setError] = useState(null);
const fetchTransactions = useCallback(
async (filters: TransactionFilters): Promise => {
- try {
- setLoading(true);
- setError(null);
-
- const response = await fetch(URLS.MORPHO_BLUE_API, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- query: userTransactionsQuery,
- variables: {
- where: {
- userAddress_in: filters.userAddress,
- marketUniqueKey_in: filters.marketUniqueKeys ?? null,
- chainId_in: filters.chainIds ?? [SupportedNetworks.Base, SupportedNetworks.Mainnet],
- timestamp_gte: filters.timestampGte ?? null,
- timestamp_lte: filters.timestampLte ?? null,
- hash: filters.hash ?? null,
- assetId_in: filters.assetIds ?? null,
- },
- first: filters.first ?? 1000,
- skip: filters.skip ?? 0,
- },
- }),
- });
-
- const result = (await response.json()) as {
- data?: { transactions?: TransactionResponse };
- errors?: { message: string }[];
- };
+ setLoading(true);
+ setError(null);
- if (result.errors) {
- throw new Error(result.errors[0].message);
- }
+ // 1. Determine target networks (numeric enum values)
+ let targetNetworks: SupportedNetworks[];
+
+ if (filters.chainIds && filters.chainIds.length > 0) {
+ // Filter provided chainIds to only include valid, supported numeric values
+ targetNetworks = filters.chainIds.filter(isSupportedChain) as SupportedNetworks[];
+ } else {
+ // Default to all supported networks (get only numeric values from enum)
+ targetNetworks = Object.values(SupportedNetworks).filter(
+ (value) => typeof value === 'number',
+ ) as SupportedNetworks[];
+ }
+
+ if (targetNetworks.length === 0) {
+ console.warn('No valid target networks determined.');
+ setLoading(false);
+ return { items: [], pageInfo: { count: 0, countTotal: 0 }, error: null };
+ }
- const transactions = result.data?.transactions as TransactionResponse;
- return transactions;
- } catch (err) {
- console.error('Error fetching transactions:', err);
- setError(err);
+ // Check for subgraph user address limitation
+ const usesSubgraph = targetNetworks.some(
+ (network) => getMarketDataSource(network) === 'subgraph',
+ );
+ if (usesSubgraph && filters.userAddress.length !== 1) {
+ console.error('Subgraph requires exactly one user address.');
+ setError('Subgraph data source requires exactly one user address.');
+ setLoading(false);
return {
items: [],
pageInfo: { count: 0, countTotal: 0 },
- error: err instanceof Error ? err.message : 'Unknown error occurred',
+ error: 'Subgraph data source requires exactly one user address.',
};
- } finally {
- setLoading(false);
}
+
+ // 2. Categorize networks by data source (numeric enum values)
+ const morphoNetworks: SupportedNetworks[] = [];
+ const subgraphNetworks: SupportedNetworks[] = [];
+
+ targetNetworks.forEach((network) => {
+ // network is now guaranteed to be a numeric enum value (e.g., 1, 8453)
+ if (getMarketDataSource(network) === 'subgraph') {
+ subgraphNetworks.push(network);
+ } else {
+ morphoNetworks.push(network);
+ }
+ });
+
+ // 3. Create fetch promises
+ const fetchPromises: Promise[] = [];
+
+ console.log('morphoNetworks', morphoNetworks);
+
+ // Morpho API Fetch
+ if (morphoNetworks.length > 0) {
+ // morphoNetworks directly contains the numeric chain IDs (e.g., [1, ...])
+ console.log(`Queueing fetch from Morpho API for chain IDs: ${morphoNetworks.join(', ')}`);
+ const morphoFilters = {
+ ...filters,
+ chainIds: morphoNetworks, // Pass the numeric IDs directly
+ first: MAX_ITEMS_PER_SOURCE,
+ skip: 0,
+ };
+ fetchPromises.push(fetchMorphoTransactions(morphoFilters));
+ }
+
+ // Subgraph Fetches
+ subgraphNetworks.forEach((network) => {
+ // network is the numeric enum value (e.g., 8453)
+ console.log(`Queueing fetch from Subgraph for network ID: ${network}`);
+ const subgraphFilters = {
+ ...filters,
+ chainIds: [network], // Pass the single numeric ID for context
+ first: MAX_ITEMS_PER_SOURCE,
+ skip: 0,
+ };
+ // Pass the enum value (which is the number) to fetchSubgraphTransactions
+ fetchPromises.push(fetchSubgraphTransactions(subgraphFilters, network));
+ });
+
+ // 4. Execute promises in parallel
+ const results = await Promise.allSettled(fetchPromises);
+
+ // 5. Combine results
+ let combinedItems: UserTransaction[] = [];
+ let combinedTotalCount = 0;
+ const errors: string[] = [];
+
+ results.forEach((result, index) => {
+ const networkDescription =
+ index < (morphoNetworks.length > 0 ? 1 : 0)
+ ? `Morpho API (${morphoNetworks.join(', ')})`
+ : `Subgraph (${subgraphNetworks[index - (morphoNetworks.length > 0 ? 1 : 0)]})`; // Adjust index for subgraph networks
+
+ if (result.status === 'fulfilled') {
+ const response = result.value;
+ if (response.error) {
+ console.warn(`Error from ${networkDescription}: ${response.error}`);
+ errors.push(`Error from ${networkDescription}: ${response.error}`);
+ } else {
+ combinedItems = combinedItems.concat(response.items);
+ combinedTotalCount += response.pageInfo.countTotal; // Aggregate total count
+ console.log(`Received ${response.items.length} items from ${networkDescription}`);
+ }
+ } else {
+ console.error(`Failed to fetch from ${networkDescription}:`, result.reason);
+ errors.push(
+ `Failed to fetch from ${networkDescription}: ${
+ result.reason?.message || 'Unknown error'
+ }`,
+ );
+ }
+ });
+
+ // 6. Sort combined results by timestamp
+ combinedItems.sort((a, b) => b.timestamp - a.timestamp);
+
+ // 7. Apply client-side pagination
+ const skip = filters.skip ?? 0;
+ const first = filters.first ?? combinedItems.length; // Default to all items if 'first' is not provided
+ const paginatedItems = combinedItems.slice(skip, skip + first);
+
+ const finalError = errors.length > 0 ? errors.join('; ') : null;
+ if (finalError) {
+ setError(finalError);
+ }
+
+ setLoading(false);
+
+ return {
+ items: paginatedItems,
+ pageInfo: {
+ count: paginatedItems.length,
+ countTotal: combinedTotalCount, // Note: This is an estimated total
+ },
+ error: finalError,
+ };
},
[],
);
From dc7d04734b0c61f14471d3de1713c24b27177d64 Mon Sep 17 00:00:00 2001
From: antoncoding
Date: Sat, 26 Apr 2025 20:37:48 +0800
Subject: [PATCH 10/20] chore: fix useLiquidations
---
src/config/dataSources.ts | 8 ++++----
src/data-sources/morpho-api/liquidations.ts | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/config/dataSources.ts b/src/config/dataSources.ts
index f8e55e8c..e27d8b67 100644
--- a/src/config/dataSources.ts
+++ b/src/config/dataSources.ts
@@ -5,10 +5,10 @@ import { SupportedNetworks } from '@/utils/networks';
*/
export const getMarketDataSource = (network: SupportedNetworks): 'morpho' | 'subgraph' => {
switch (network) {
- case SupportedNetworks.Mainnet:
- return 'subgraph';
- case SupportedNetworks.Base:
- return 'subgraph';
+ // case SupportedNetworks.Mainnet:
+ // return 'subgraph';
+ // case SupportedNetworks.Base:
+ // return 'subgraph';
default:
return 'morpho'; // Default to Morpho API
}
diff --git a/src/data-sources/morpho-api/liquidations.ts b/src/data-sources/morpho-api/liquidations.ts
index 64e99ca1..71301982 100644
--- a/src/data-sources/morpho-api/liquidations.ts
+++ b/src/data-sources/morpho-api/liquidations.ts
@@ -3,7 +3,7 @@ import { URLS } from '@/utils/urls';
// Re-use the query structure from the original hook
const liquidationsQuery = `
- query getLiquidations($first: Int, $skip: Int, $chainId: Int) {
+ query getLiquidations($first: Int, $skip: Int, $chainId: Int!) {
transactions(
where: { type_in: [MarketLiquidation], chainId_in: [$chainId] } # Filter by chainId
first: $first
From 12b600cca5c80b2fa422bdf9e5204de3b6e431d1 Mon Sep 17 00:00:00 2001
From: antoncoding
Date: Sat, 26 Apr 2025 23:33:21 +0800
Subject: [PATCH 11/20] feat: get positions
---
src/contexts/MarketsContext.tsx | 3 +-
src/data-sources/morpho-api/positions.ts | 76 ++++++
src/data-sources/subgraph/positions.ts | 66 +++++
src/data-sources/subgraph/transactions.ts | 4 -
src/graphql/morpho-subgraph-queries.ts | 14 +
src/hooks/useUserPositions.ts | 318 +++++++++++-----------
src/hooks/useUserPositionsSummaryData.ts | 3 -
7 files changed, 315 insertions(+), 169 deletions(-)
create mode 100644 src/data-sources/morpho-api/positions.ts
create mode 100644 src/data-sources/subgraph/positions.ts
diff --git a/src/contexts/MarketsContext.tsx b/src/contexts/MarketsContext.tsx
index 56ab9ad2..db1bbbed 100644
--- a/src/contexts/MarketsContext.tsx
+++ b/src/contexts/MarketsContext.tsx
@@ -17,7 +17,8 @@ import { isSupportedChain, SupportedNetworks } from '@/utils/networks';
import { Market } from '@/utils/types';
import { getMarketWarningsWithDetail } from '@/utils/warnings';
-type MarketsContextType = {
+// Export the type definition
+export type MarketsContextType = {
markets: Market[];
loading: boolean;
isRefetching: boolean;
diff --git a/src/data-sources/morpho-api/positions.ts b/src/data-sources/morpho-api/positions.ts
new file mode 100644
index 00000000..bc032ce3
--- /dev/null
+++ b/src/data-sources/morpho-api/positions.ts
@@ -0,0 +1,76 @@
+import { userPositionsQuery } from '@/graphql/morpho-api-queries';
+import { SupportedNetworks } from '@/utils/networks';
+import { MarketPosition } from '@/utils/types';
+import { URLS } from '@/utils/urls';
+
+// Type for the raw response from the Morpho API userPositionsQuery
+type MorphoUserPositionsApiResponse = {
+ data?: {
+ userByAddress?: {
+ marketPositions?: MarketPosition[];
+ };
+ };
+ errors?: { message: string }[];
+};
+
+// Type for a valid position with required fields
+type ValidMarketPosition = MarketPosition & {
+ market: {
+ uniqueKey: string;
+ morphoBlue: { chain: { id: number } };
+ };
+};
+
+/**
+ * Fetches the unique keys of markets where a user has a position from the Morpho API.
+ */
+export const fetchMorphoUserPositionMarkets = async (
+ userAddress: string,
+ network: SupportedNetworks,
+): Promise<{ marketUniqueKey: string; chainId: number }[]> => {
+ try {
+ const response = await fetch(URLS.MORPHO_BLUE_API, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ query: userPositionsQuery,
+ variables: {
+ address: userAddress.toLowerCase(),
+ chainId: network,
+ },
+ }),
+ });
+
+ const result = (await response.json()) as MorphoUserPositionsApiResponse;
+
+ if (result.errors) {
+ console.error(
+ `Morpho API error fetching position markets for ${userAddress} on ${network}:`,
+ result.errors,
+ );
+ throw new Error(result.errors.map((e) => e.message).join('; '));
+ }
+
+ const marketPositions = result.data?.userByAddress?.marketPositions ?? [];
+
+ // Filter for valid positions and extract market key and chain ID
+ const positionMarkets = marketPositions
+ .filter(
+ (position): position is ValidMarketPosition =>
+ position.market?.uniqueKey !== undefined &&
+ position.market?.morphoBlue?.chain?.id !== undefined,
+ )
+ .map((position) => ({
+ marketUniqueKey: position.market.uniqueKey,
+ chainId: position.market.morphoBlue.chain.id,
+ }));
+
+ return positionMarkets;
+ } catch (error) {
+ console.error(
+ `Failed to fetch position markets from Morpho API for ${userAddress} on ${network}:`,
+ error,
+ );
+ return []; // Return empty array on error
+ }
+};
diff --git a/src/data-sources/subgraph/positions.ts b/src/data-sources/subgraph/positions.ts
new file mode 100644
index 00000000..a795534e
--- /dev/null
+++ b/src/data-sources/subgraph/positions.ts
@@ -0,0 +1,66 @@
+import { subgraphUserPositionMarketsQuery } from '@/graphql/morpho-subgraph-queries';
+import { SupportedNetworks } from '@/utils/networks';
+import { getSubgraphUrl } from '@/utils/subgraph-urls';
+
+type SubgraphPositionMarketResponse = {
+ data?: {
+ account?: {
+ positions?: {
+ market: {
+ id: string;
+ };
+ }[];
+ };
+ };
+ errors?: { message: string }[];
+};
+
+/**
+ * Fetches the unique keys of markets where a user has a position from the Subgraph.
+ */
+export const fetchSubgraphUserPositionMarkets = async (
+ userAddress: string,
+ network: SupportedNetworks,
+): Promise<{ marketUniqueKey: string; chainId: number }[]> => {
+ const endpoint = getSubgraphUrl(network);
+ if (!endpoint) {
+ console.warn(`No subgraph endpoint found for network ${network}`);
+ return [];
+ }
+
+ try {
+ const response = await fetch(endpoint, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ query: subgraphUserPositionMarketsQuery,
+ variables: {
+ userId: userAddress.toLowerCase(),
+ },
+ }),
+ });
+
+ const result = (await response.json()) as SubgraphPositionMarketResponse;
+
+ if (result.errors) {
+ console.error(
+ `Subgraph error fetching position markets for ${userAddress} on ${network}:`,
+ result.errors,
+ );
+ throw new Error(result.errors.map((e) => e.message).join('; '));
+ }
+
+ const positions = result.data?.account?.positions ?? [];
+
+ return positions.map((pos) => ({
+ marketUniqueKey: pos.market.id,
+ chainId: network, // The network ID is passed in
+ }));
+ } catch (error) {
+ console.error(
+ `Failed to fetch position markets from subgraph for ${userAddress} on ${network}:`,
+ error,
+ );
+ return []; // Return empty array on error
+ }
+};
diff --git a/src/data-sources/subgraph/transactions.ts b/src/data-sources/subgraph/transactions.ts
index 979e4f25..7443ee97 100644
--- a/src/data-sources/subgraph/transactions.ts
+++ b/src/data-sources/subgraph/transactions.ts
@@ -173,10 +173,6 @@ export const fetchSubgraphTransactions = async (
variables: variables,
};
- // Log the URL and body before sending
- console.log('Subgraph Request URL:', subgraphUrl);
- console.log('Subgraph Request Body:', JSON.stringify(requestBody));
-
try {
const response = await fetch(subgraphUrl, {
method: 'POST',
diff --git a/src/graphql/morpho-subgraph-queries.ts b/src/graphql/morpho-subgraph-queries.ts
index 2eb91b91..b733ae8a 100644
--- a/src/graphql/morpho-subgraph-queries.ts
+++ b/src/graphql/morpho-subgraph-queries.ts
@@ -257,6 +257,20 @@ export const subgraphMarketsWithLiquidationCheckQuery = `
}
`;
+// --- Query for User Position Market IDs ---
+export const subgraphUserPositionMarketsQuery = `
+ query GetUserPositionMarkets($userId: ID!) {
+ account(id: $userId) {
+ positions(first: 1000) { # Assuming a user won't have > 1000 positions
+ market {
+ id # Market unique key
+ }
+ }
+ }
+ }
+`;
+// --- End Query ---
+
// Note: The exact field names might need adjustment based on the specific Subgraph schema.
export const subgraphUserTransactionsQuery = `
query GetUserTransactions(
diff --git a/src/hooks/useUserPositions.ts b/src/hooks/useUserPositions.ts
index 64b746b2..c3c1ceb0 100644
--- a/src/hooks/useUserPositions.ts
+++ b/src/hooks/useUserPositions.ts
@@ -1,222 +1,213 @@
-/* eslint-disable @typescript-eslint/no-unsafe-assignment */
-
import { useCallback } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { Address } from 'viem';
-import { userPositionsQuery } from '@/graphql/morpho-api-queries';
+import { getMarketDataSource } from '@/config/dataSources';
+import { fetchMorphoUserPositionMarkets } from '@/data-sources/morpho-api/positions';
+import { fetchSubgraphUserPositionMarkets } from '@/data-sources/subgraph/positions';
import { SupportedNetworks } from '@/utils/networks';
import { fetchPositionSnapshot, type PositionSnapshot } from '@/utils/positions';
-import { MarketPosition, Market } from '@/utils/types';
-import { URLS } from '@/utils/urls';
+import { Market } from '@/utils/types';
import { getMarketWarningsWithDetail } from '@/utils/warnings';
import { useUserMarketsCache } from '../hooks/useUserMarketsCache';
import { useMarkets } from './useMarkets';
-type UserPositionsResponse = {
- marketPositions: MarketPosition[];
- usedMarkets: {
- marketUniqueKey: string;
- chainId: number;
- }[];
+// Type for market key and chain identifier
+type PositionMarket = {
+ marketUniqueKey: string;
+ chainId: number;
+};
+
+// Type returned by the first query
+type InitialDataResponse = {
+ finalMarketKeys: PositionMarket[];
};
+// Type for object used to fetch snapshot details
type MarketToFetch = {
marketKey: string;
chainId: number;
market: Market;
- existingState: PositionSnapshot | null;
};
+// Type for the final processed position data
type EnhancedMarketPosition = {
state: PositionSnapshot;
market: Market & { warningsWithDetail: ReturnType };
};
+// Type for the result of a single snapshot fetch
type SnapshotResult = {
market: Market;
state: PositionSnapshot | null;
} | null;
-type ValidMarketPosition = MarketPosition & {
- market: Market & {
- uniqueKey: string;
- morphoBlue: { chain: { id: number } };
- };
-};
-
-// Query keys for caching
+// --- Query Keys (adjusted for two-step process) ---
export const positionKeys = {
all: ['positions'] as const,
- user: (address: string) => [...positionKeys.all, address] as const,
+ // Key for the initial fetch of relevant market keys
+ initialData: (user: string) => [...positionKeys.all, 'initialData', user] as const,
+ // Key for fetching the on-chain snapshot state for a specific market (used internally by queryClient)
snapshot: (marketKey: string, userAddress: string, chainId: number) =>
[...positionKeys.all, 'snapshot', marketKey, userAddress, chainId] as const,
- enhanced: (user: string | undefined, data: UserPositionsResponse | undefined) =>
- ['enhanced-positions', user, data] as const,
+ // Key for the final enhanced position data, dependent on initialData result
+ enhanced: (user: string | undefined, initialData: InitialDataResponse | undefined) =>
+ ['enhanced-positions', user, initialData] as const,
};
-const fetchUserPositions = async (
- user: string,
- getUserMarkets: () => { marketUniqueKey: string; chainId: number }[],
-): Promise => {
- console.log('🔄 Fetching user positions for:', user);
+// --- Helper Fetch Function --- //
+
+// Fetches market keys ONLY from API/Subgraph sources
+const fetchSourceMarketKeys = async (user: string): Promise => {
+ const allSupportedNetworks = Object.values(SupportedNetworks).filter(
+ (value) => typeof value === 'number',
+ ) as SupportedNetworks[];
- const [responseMainnet, responseBase] = await Promise.all([
- fetch(URLS.MORPHO_BLUE_API, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- query: userPositionsQuery,
- variables: {
- address: user.toLowerCase(),
- chainId: SupportedNetworks.Mainnet,
- },
- }),
- }),
- fetch(URLS.MORPHO_BLUE_API, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- query: userPositionsQuery,
- variables: {
- address: user.toLowerCase(),
- chainId: SupportedNetworks.Base,
- },
- }),
- }),
- ]);
+ const morphoNetworks: SupportedNetworks[] = [];
+ const subgraphNetworks: SupportedNetworks[] = [];
- const [result1, result2] = await Promise.all([responseMainnet.json(), responseBase.json()]);
+ allSupportedNetworks.forEach((network: SupportedNetworks) => {
+ const source = getMarketDataSource(network);
+ if (source === 'subgraph') {
+ subgraphNetworks.push(network);
+ } else {
+ morphoNetworks.push(network);
+ }
+ });
- console.log('📊 Received positions data from both networks');
+ const fetchPromises: Promise[] = [];
- const usedMarkets = getUserMarkets();
- const marketPositions: MarketPosition[] = [];
+ morphoNetworks.forEach((network) => {
+ fetchPromises.push(fetchMorphoUserPositionMarkets(user, network));
+ });
+ subgraphNetworks.forEach((network) => {
+ fetchPromises.push(fetchSubgraphUserPositionMarkets(user, network));
+ });
- // Collect positions
- for (const result of [result1, result2]) {
- if (result.data?.userByAddress?.marketPositions) {
- marketPositions.push(...(result.data.userByAddress.marketPositions as MarketPosition[]));
+ const results = await Promise.allSettled(fetchPromises);
+
+ let sourcePositionMarkets: PositionMarket[] = [];
+ results.forEach((result, index) => {
+ if (result.status === 'fulfilled') {
+ sourcePositionMarkets = sourcePositionMarkets.concat(result.value);
+ } else {
+ const network = [...morphoNetworks, ...subgraphNetworks][index];
+ const source = getMarketDataSource(network);
+ console.error(
+ `[Positions] Failed to fetch from ${source} for network ${network}:`,
+ result.reason,
+ );
}
- }
-
- return { marketPositions, usedMarkets };
+ });
+ // console.log(`[Positions] Fetched ${sourcePositionMarkets.length} keys from sources.`);
+ return sourcePositionMarkets;
};
+// --- Main Hook --- //
+
const useUserPositions = (user: string | undefined, showEmpty = false) => {
const queryClient = useQueryClient();
- const { markets } = useMarkets();
+ const { markets } = useMarkets(); // Get markets list (loading state not directly used for enabling 2nd query)
const { getUserMarkets, batchAddUserMarkets } = useUserMarketsCache(user);
- // Main query for user positions
+ // 1. Query for initial data: Fetch keys from sources, combine with cache, deduplicate
const {
- data: positionsData,
- isLoading: isLoadingPositions,
- isRefetching: isRefetchingPositions,
- error: positionsError,
- refetch: refetchPositions,
- } = useQuery({
- queryKey: positionKeys.user(user ?? ''),
+ data: initialData,
+ isLoading: isLoadingInitialData, // Primary loading state
+ isRefetching: isRefetchingInitialData,
+ error: initialError,
+ refetch: refetchInitialData,
+ } = useQuery({
+ // Note: Removed MarketsContextType type assertion
+ queryKey: positionKeys.initialData(user ?? ''),
queryFn: async () => {
- if (!user) throw new Error('Missing user address');
- return fetchUserPositions(user, getUserMarkets);
+ // User is guaranteed non-null here due to the 'enabled' flag
+ if (!user) throw new Error('Assertion failed: User should be defined here.');
+
+ // Fetch keys from API/Subgraph
+ const sourceMarketKeys = await fetchSourceMarketKeys(user);
+ // Get keys from cache
+ const usedMarkets = getUserMarkets();
+ // Combine and deduplicate
+ const combinedMarkets = [...sourceMarketKeys, ...usedMarkets];
+ const uniqueMarketsMap = new Map();
+ combinedMarkets.forEach((market) => {
+ const key = `${market.marketUniqueKey.toLowerCase()}-${market.chainId}`;
+ if (!uniqueMarketsMap.has(key)) {
+ uniqueMarketsMap.set(key, market);
+ }
+ });
+ const finalMarketKeys = Array.from(uniqueMarketsMap.values());
+ // console.log(`[Positions] Query 1: Final unique keys count: ${finalMarketKeys.length}`);
+ return { finalMarketKeys };
},
- enabled: !!user,
- staleTime: 30000, // Consider data fresh for 30 seconds
- gcTime: 5 * 60 * 1000, // Keep in cache for 5 minutes
+ enabled: !!user && markets.length > 0,
+ staleTime: 30000,
+ gcTime: 5 * 60 * 1000,
});
- // Query for position snapshots
+ // 2. Query for enhanced position data (snapshots), dependent on initialData
const { data: enhancedPositions, isRefetching: isRefetchingEnhanced } = useQuery<
EnhancedMarketPosition[]
>({
- queryKey: positionKeys.enhanced(user, positionsData),
+ queryKey: positionKeys.enhanced(user, initialData),
queryFn: async () => {
- if (!positionsData || !user) return [];
-
- console.log('🔄 Fetching position snapshots');
-
- const { marketPositions, usedMarkets } = positionsData;
-
- // We need to fetch snapshots for ALL markets - both from API and used ones
- const knownMarkets = marketPositions
- .filter(
- (position): position is ValidMarketPosition =>
- position.market?.uniqueKey !== undefined &&
- position.market?.morphoBlue?.chain?.id !== undefined,
- )
- .map(
- (position): MarketToFetch => ({
- marketKey: position.market.uniqueKey,
- chainId: position.market.morphoBlue.chain.id,
- market: position.market,
- existingState: position.state,
- }),
- );
-
- const marketsToRescan = usedMarkets
- .filter((market) => {
- return !marketPositions.find(
- (position) =>
- position.market?.uniqueKey?.toLowerCase() === market.marketUniqueKey.toLowerCase() &&
- position.market?.morphoBlue?.chain?.id === market.chainId,
- );
- })
- .map((market) => {
- const marketWithDetails = markets.find(
- (m) =>
- m.uniqueKey?.toLowerCase() === market.marketUniqueKey.toLowerCase() &&
- m.morphoBlue?.chain?.id === market.chainId,
+ // initialData and user are guaranteed non-null here due to the 'enabled' flag
+ if (!initialData || !user)
+ throw new Error('Assertion failed: initialData/user should be defined here.');
+
+ const { finalMarketKeys } = initialData;
+ // console.log(`[Positions] Query 2: Processing ${finalMarketKeys.length} keys for snapshots.`);
+
+ // Find market details using the main `markets` list from context
+ const allMarketsToFetch: MarketToFetch[] = finalMarketKeys
+ .map((marketInfo) => {
+ const marketDetails = markets.find(
+ (m: Market) =>
+ m.uniqueKey?.toLowerCase() === marketInfo.marketUniqueKey.toLowerCase() &&
+ m.morphoBlue?.chain?.id === marketInfo.chainId,
);
- if (
- !marketWithDetails ||
- !marketWithDetails.uniqueKey ||
- !marketWithDetails.morphoBlue?.chain?.id
- ) {
+ if (!marketDetails) {
+ console.warn(
+ `[Positions] Market details not found for ${marketInfo.marketUniqueKey} on chain ${marketInfo.chainId}. Skipping snapshot fetch.`,
+ );
return null;
}
return {
- marketKey: market.marketUniqueKey,
- chainId: market.chainId,
- market: marketWithDetails,
- existingState: null,
- } as MarketToFetch;
+ marketKey: marketInfo.marketUniqueKey,
+ chainId: marketInfo.chainId,
+ market: marketDetails,
+ };
})
.filter((item): item is MarketToFetch => item !== null);
- const allMarketsToFetch: MarketToFetch[] = [...knownMarkets, ...marketsToRescan];
-
- console.log(`🔄 Fetching snapshots for ${allMarketsToFetch.length} markets`);
+ // console.log(`[Positions] Query 2: Fetching snapshots for ${allMarketsToFetch.length} markets.`);
- // Fetch snapshots in parallel using React Query's built-in caching
+ // Fetch snapshots in parallel
const snapshots = await Promise.all(
- allMarketsToFetch.map(
- async ({ marketKey, chainId, market, existingState }): Promise => {
- const snapshot = await queryClient.fetchQuery({
- queryKey: positionKeys.snapshot(marketKey, user, chainId),
- queryFn: async () => fetchPositionSnapshot(marketKey, user as Address, chainId, 0),
- staleTime: 30000,
- gcTime: 5 * 60 * 1000,
- });
-
- if (!snapshot && !existingState) return null;
-
- return {
- market,
- state: snapshot ?? existingState,
- };
- },
- ),
+ allMarketsToFetch.map(async ({ marketKey, chainId, market }): Promise => {
+ const snapshot = await queryClient.fetchQuery({
+ queryKey: positionKeys.snapshot(marketKey, user, chainId),
+ queryFn: async () => fetchPositionSnapshot(marketKey, user as Address, chainId, 0),
+ staleTime: 30000, // Use same staleTime as main queries
+ gcTime: 5 * 60 * 1000,
+ });
+ // No fallback to existingState here, unlike original logic
+ return snapshot ? { market, state: snapshot } : null;
+ }),
);
- console.log('📊 Received position snapshots');
-
- // Filter out null results and process positions
+ // Process valid snapshots
const validPositions = snapshots
.filter(
(item): item is NonNullable & { state: NonNullable } =>
item !== null && item.state !== null,
)
- .filter((position) => showEmpty || position.state.supplyShares.toString() !== '0')
+ .filter((position) => {
+ const hasSupply = position.state.supplyShares.toString() !== '0';
+ const hasBorrow = position.state.borrowShares.toString() !== '0';
+ const hasCollateral = position.state.collateral.toString() !== '0';
+ return showEmpty || hasSupply || hasBorrow || hasCollateral;
+ })
.map((position) => ({
state: position.state,
market: {
@@ -225,7 +216,7 @@ const useUserPositions = (user: string | undefined, showEmpty = false) => {
},
}));
- // Update market cache with all valid positions
+ // Update market cache
const marketsToCache = validPositions
.filter((position) => position.market?.uniqueKey && position.market?.morphoBlue?.chain?.id)
.map((position) => ({
@@ -237,33 +228,38 @@ const useUserPositions = (user: string | undefined, showEmpty = false) => {
batchAddUserMarkets(marketsToCache);
}
+ // console.log(`[Positions] Query 2: Processed ${validPositions.length} valid positions.`);
return validPositions;
},
- enabled: !!positionsData && !!user,
+ // Enable this query only when the first query has successfully run
+ enabled: !!initialData && !!user,
+ // This query represents derived data, stale/gc time might not be strictly needed
+ // but keeping consistent for simplicity
+ staleTime: 30000,
+ gcTime: 5 * 60 * 1000,
});
+ // Refetch function targets the initial data query
const refetch = useCallback(
async (onSuccess?: () => void) => {
try {
- await refetchPositions();
- if (onSuccess) {
- onSuccess();
- }
+ await refetchInitialData();
+ onSuccess?.();
} catch (error) {
- console.error('Error refetching positions:', error);
+ console.error('[Positions] Error during manual refetch:', error);
}
},
- [refetchPositions],
+ [refetchInitialData],
);
- // Consider refetching true if either query is refetching
- const isRefetching = isRefetchingPositions || isRefetchingEnhanced;
+ // Combine refetching states
+ const isRefetching = isRefetchingInitialData || isRefetchingEnhanced;
return {
data: enhancedPositions ?? [],
- loading: isLoadingPositions,
+ loading: isLoadingInitialData, // Loading is determined by the first query
isRefetching,
- positionsError,
+ positionsError: initialError, // Error is determined by the first query
refetch,
};
};
diff --git a/src/hooks/useUserPositionsSummaryData.ts b/src/hooks/useUserPositionsSummaryData.ts
index 7e1ee3af..2a8eaac0 100644
--- a/src/hooks/useUserPositionsSummaryData.ts
+++ b/src/hooks/useUserPositionsSummaryData.ts
@@ -83,9 +83,6 @@ const useUserPositionsSummaryData = (user: string | undefined) => {
refetch: refetchPositions,
} = useUserPositions(user, true);
- console.log('positionsLoading', positionsLoading);
- console.log('hasInitialData', hasInitialData);
-
const { fetchTransactions } = useUserTransactions();
// Query for block numbers - this runs once and is cached
From 5510997cdfd2c5ef77b030c44f01aa26de61a875 Mon Sep 17 00:00:00 2001
From: antoncoding
Date: Sun, 27 Apr 2025 00:10:03 +0800
Subject: [PATCH 12/20] feat: useUserPosition hook
---
src/config/dataSources.ts | 8 +-
src/data-sources/morpho-api/positions.ts | 79 +++++++---
src/data-sources/morpho-api/transactions.ts | 38 ++---
src/data-sources/subgraph/positions.ts | 164 ++++++++++++++++++++
src/graphql/morpho-subgraph-queries.ts | 18 +++
src/hooks/useUserPosition.ts | 164 ++++++++++++--------
6 files changed, 354 insertions(+), 117 deletions(-)
diff --git a/src/config/dataSources.ts b/src/config/dataSources.ts
index e27d8b67..f8e55e8c 100644
--- a/src/config/dataSources.ts
+++ b/src/config/dataSources.ts
@@ -5,10 +5,10 @@ import { SupportedNetworks } from '@/utils/networks';
*/
export const getMarketDataSource = (network: SupportedNetworks): 'morpho' | 'subgraph' => {
switch (network) {
- // case SupportedNetworks.Mainnet:
- // return 'subgraph';
- // case SupportedNetworks.Base:
- // return 'subgraph';
+ case SupportedNetworks.Mainnet:
+ return 'subgraph';
+ case SupportedNetworks.Base:
+ return 'subgraph';
default:
return 'morpho'; // Default to Morpho API
}
diff --git a/src/data-sources/morpho-api/positions.ts b/src/data-sources/morpho-api/positions.ts
index bc032ce3..e6f62432 100644
--- a/src/data-sources/morpho-api/positions.ts
+++ b/src/data-sources/morpho-api/positions.ts
@@ -1,7 +1,8 @@
-import { userPositionsQuery } from '@/graphql/morpho-api-queries';
+import { userPositionsQuery, userPositionForMarketQuery } from '@/graphql/morpho-api-queries';
import { SupportedNetworks } from '@/utils/networks';
import { MarketPosition } from '@/utils/types';
import { URLS } from '@/utils/urls';
+import { morphoGraphqlFetcher } from './fetchers';
// Type for the raw response from the Morpho API userPositionsQuery
type MorphoUserPositionsApiResponse = {
@@ -13,6 +14,14 @@ type MorphoUserPositionsApiResponse = {
errors?: { message: string }[];
};
+// Type for the raw response from the Morpho API userPositionForMarketQuery
+type MorphoUserMarketPositionApiResponse = {
+ data?: {
+ marketPosition?: MarketPosition;
+ };
+ errors?: { message: string }[];
+};
+
// Type for a valid position with required fields
type ValidMarketPosition = MarketPosition & {
market: {
@@ -29,27 +38,13 @@ export const fetchMorphoUserPositionMarkets = async (
network: SupportedNetworks,
): Promise<{ marketUniqueKey: string; chainId: number }[]> => {
try {
- const response = await fetch(URLS.MORPHO_BLUE_API, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- query: userPositionsQuery,
- variables: {
- address: userAddress.toLowerCase(),
- chainId: network,
- },
- }),
- });
-
- const result = (await response.json()) as MorphoUserPositionsApiResponse;
-
- if (result.errors) {
- console.error(
- `Morpho API error fetching position markets for ${userAddress} on ${network}:`,
- result.errors,
- );
- throw new Error(result.errors.map((e) => e.message).join('; '));
- }
+ const result = await morphoGraphqlFetcher(
+ userPositionsQuery,
+ {
+ address: userAddress.toLowerCase(),
+ chainId: network,
+ },
+ );
const marketPositions = result.data?.userByAddress?.marketPositions ?? [];
@@ -74,3 +69,43 @@ export const fetchMorphoUserPositionMarkets = async (
return []; // Return empty array on error
}
};
+
+/**
+ * Fetches a user's position for a specific market directly from the Morpho API.
+ */
+export const fetchMorphoUserPositionForMarket = async (
+ marketUniqueKey: string,
+ userAddress: string,
+ network: SupportedNetworks,
+): Promise => {
+ try {
+ const result = await morphoGraphqlFetcher(
+ userPositionForMarketQuery,
+ {
+ address: userAddress.toLowerCase(),
+ chainId: network,
+ marketKey: marketUniqueKey,
+ },
+ );
+
+ const marketPosition = result.data?.marketPosition;
+
+ // Check if the position state has zero balances - API might return structure even with no actual position
+ if (
+ marketPosition &&
+ marketPosition.state.supplyAssets === '0' &&
+ marketPosition.state.borrowAssets === '0' &&
+ marketPosition.state.collateral === '0'
+ ) {
+ return null; // Treat zero balance position as null
+ }
+
+ return marketPosition ?? null;
+ } catch (error) {
+ console.error(
+ `Failed to fetch position for market ${marketUniqueKey} from Morpho API for ${userAddress} on ${network}:`,
+ error,
+ );
+ return null; // Return null on error
+ }
+};
diff --git a/src/data-sources/morpho-api/transactions.ts b/src/data-sources/morpho-api/transactions.ts
index a755c422..90068380 100644
--- a/src/data-sources/morpho-api/transactions.ts
+++ b/src/data-sources/morpho-api/transactions.ts
@@ -2,6 +2,15 @@ import { userTransactionsQuery } from '@/graphql/morpho-api-queries';
import { TransactionFilters, TransactionResponse } from '@/hooks/useUserTransactions';
import { SupportedNetworks } from '@/utils/networks';
import { URLS } from '@/utils/urls';
+import { morphoGraphqlFetcher } from './fetchers';
+
+// Define the expected shape of the GraphQL response for transactions
+type MorphoTransactionsApiResponse = {
+ data?: {
+ transactions?: TransactionResponse;
+ };
+ // errors are handled by the fetcher
+};
export const fetchMorphoTransactions = async (
filters: TransactionFilters,
@@ -30,29 +39,14 @@ export const fetchMorphoTransactions = async (
}
try {
- const response = await fetch(URLS.MORPHO_BLUE_API, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
+ const result = await morphoGraphqlFetcher(
+ userTransactionsQuery,
+ {
+ where: whereClause,
+ first: filters.first ?? 1000,
+ skip: filters.skip ?? 0,
},
- body: JSON.stringify({
- query: userTransactionsQuery,
- variables: {
- where: whereClause, // Use the conditionally built 'where' clause
- first: filters.first ?? 1000,
- skip: filters.skip ?? 0,
- },
- }),
- });
-
- const result = (await response.json()) as {
- data?: { transactions?: TransactionResponse };
- errors?: { message: string }[];
- };
-
- if (result.errors) {
- throw new Error(result.errors.map((e) => e.message).join(', '));
- }
+ );
const transactions = result.data?.transactions;
if (!transactions) {
diff --git a/src/data-sources/subgraph/positions.ts b/src/data-sources/subgraph/positions.ts
index a795534e..a2164c83 100644
--- a/src/data-sources/subgraph/positions.ts
+++ b/src/data-sources/subgraph/positions.ts
@@ -1,6 +1,19 @@
+import { request } from 'graphql-request';
+import { fetchSubgraphMarket } from '@/data-sources/subgraph/market'; // Need market data too
import { subgraphUserPositionMarketsQuery } from '@/graphql/morpho-subgraph-queries';
+import { subgraphUserMarketPositionQuery } from '@/graphql/morpho-subgraph-queries';
import { SupportedNetworks } from '@/utils/networks';
import { getSubgraphUrl } from '@/utils/subgraph-urls';
+import { MarketPosition } from '@/utils/types';
+
+// The type expected by MarketPosition.state
+type MarketPositionState = {
+ supplyShares: string;
+ supplyAssets: string;
+ borrowShares: string;
+ borrowAssets: string;
+ collateral: string; // This is collateral assets
+};
type SubgraphPositionMarketResponse = {
data?: {
@@ -15,6 +28,20 @@ type SubgraphPositionMarketResponse = {
errors?: { message: string }[];
};
+type SubgraphPosition = {
+ id: string;
+ asset: {
+ id: string; // Token address
+ };
+ isCollateral: boolean | null;
+ balance: string; // BigInt string
+ side: 'SUPPLIER' | 'COLLATERAL' | 'BORROWER';
+};
+
+type SubgraphPositionResponse = {
+ positions?: SubgraphPosition[];
+};
+
/**
* Fetches the unique keys of markets where a user has a position from the Subgraph.
*/
@@ -64,3 +91,140 @@ export const fetchSubgraphUserPositionMarkets = async (
return []; // Return empty array on error
}
};
+
+/**
+ * Fetches and reconstructs a user's position for a specific market from the Subgraph.
+ * Combines position data with market data.
+ */
+export const fetchSubgraphUserPositionForMarket = async (
+ marketUniqueKey: string,
+ userAddress: string,
+ network: SupportedNetworks,
+): Promise => {
+ const subgraphUrl = getSubgraphUrl(network);
+ if (!subgraphUrl) {
+ console.error(`Subgraph URL not configured for network ${network}.`);
+ return null;
+ }
+
+ try {
+ // 1. Fetch the market details first (needed for context)
+ const market = await fetchSubgraphMarket(marketUniqueKey, network);
+ if (!market) {
+ console.warn(
+ `Market ${marketUniqueKey} not found via subgraph on ${network} while fetching user position.`,
+ );
+ return null; // Cannot proceed without market details
+ }
+
+ // 2. Fetch the user's positions within that market
+ const response = await request(
+ subgraphUrl,
+ subgraphUserMarketPositionQuery,
+ {
+ marketId: marketUniqueKey.toLowerCase(), // Ensure lowercase for subgraph ID matching
+ userId: userAddress.toLowerCase(),
+ },
+ );
+
+ const positions = response.positions ?? [];
+
+ // 3. Reconstruct the MarketPosition.state object
+ let supplyShares = '0';
+ let supplyAssets = '0';
+ let borrowShares = '0';
+ let borrowAssets = '0';
+ let collateralAssets = '0';
+
+ positions.forEach((pos) => {
+ const balanceStr = pos.balance;
+ if (!balanceStr || balanceStr === '0') return; // Ignore zero/empty balances
+
+ switch (pos.side) {
+ case 'SUPPLIER':
+ // Assuming the SUPPLIER asset is always the loan asset
+ if (pos.asset.id.toLowerCase() === market.loanAsset.address.toLowerCase()) {
+ // Subgraph returns shares for SUPPLIER side in `balance`
+ supplyShares = balanceStr;
+ // We also need supplyAssets. Subgraph might not directly provide this for the position.
+ // We might need to calculate it using market.state conversion rates, or rely on fetchPositionSnapshot.
+ // For now, let's assume fetchPositionSnapshot is the primary source for accurate assets.
+ // If falling back here, we might lack the direct asset value from subgraph.
+ // Let's set assets based on shares * rate, IF market state has the rates.
+ // This requires market.state.supplyAssets and market.state.supplyShares
+ const marketTotalSupplyAssets = BigInt(market.state.supplyAssets || '0');
+ const marketTotalSupplyShares = BigInt(market.state.supplyShares || '1'); // Avoid div by zero
+ supplyAssets =
+ marketTotalSupplyShares > 0n
+ ? (
+ (BigInt(supplyShares) * marketTotalSupplyAssets) /
+ marketTotalSupplyShares
+ ).toString()
+ : '0';
+ } else {
+ console.warn(
+ `Subgraph position side 'SUPPLIER' doesn't match loan asset for market ${marketUniqueKey}`,
+ );
+ }
+ break;
+ case 'COLLATERAL':
+ // Assuming the COLLATERAL asset is always the collateral asset
+ if (pos.asset.id.toLowerCase() === market.collateralAsset.address.toLowerCase()) {
+ // Subgraph 'balance' for collateral IS THE ASSET AMOUNT
+ collateralAssets = balanceStr;
+ } else {
+ console.warn(
+ `Subgraph position side 'COLLATERAL' doesn't match collateral asset for market ${marketUniqueKey}`,
+ );
+ }
+ break;
+ case 'BORROWER':
+ // Assuming the BORROWER asset is always the loan asset
+ if (pos.asset.id.toLowerCase() === market.loanAsset.address.toLowerCase()) {
+ // Subgraph returns shares for BORROWER side in `balance`
+ borrowShares = balanceStr;
+ // Calculate borrowAssets from shares
+ const marketTotalBorrowAssets = BigInt(market.state.borrowAssets || '0');
+ const marketTotalBorrowShares = BigInt(market.state.borrowShares || '1'); // Avoid div by zero
+ borrowAssets =
+ marketTotalBorrowShares > 0n
+ ? (
+ (BigInt(borrowShares) * marketTotalBorrowAssets) /
+ marketTotalBorrowShares
+ ).toString()
+ : '0';
+ } else {
+ console.warn(
+ `Subgraph position side 'BORROWER' doesn't match loan asset for market ${marketUniqueKey}`,
+ );
+ }
+ break;
+ }
+ });
+
+ // Check if the user has any position (check assets)
+ if (supplyAssets === '0' && collateralAssets === '0' && borrowAssets === '0') {
+ // If all balances are zero, treat as no position found for this market
+ return null; // Return null as per MarketPosition type possibility
+ }
+
+ const state: MarketPositionState = {
+ supplyAssets: supplyAssets,
+ supplyShares: supplyShares,
+ collateral: collateralAssets, // Use the direct asset amount
+ borrowAssets: borrowAssets,
+ borrowShares: borrowShares,
+ };
+
+ return {
+ market,
+ state: state,
+ };
+ } catch (error) {
+ console.error(
+ `Failed to fetch user position for market ${marketUniqueKey} from Subgraph on ${network}:`,
+ error,
+ );
+ return null; // Return null on error
+ }
+};
diff --git a/src/graphql/morpho-subgraph-queries.ts b/src/graphql/morpho-subgraph-queries.ts
index b733ae8a..d99e3a75 100644
--- a/src/graphql/morpho-subgraph-queries.ts
+++ b/src/graphql/morpho-subgraph-queries.ts
@@ -271,6 +271,24 @@ export const subgraphUserPositionMarketsQuery = `
`;
// --- End Query ---
+// --- Query for User Position in a Single Market ---
+export const subgraphUserMarketPositionQuery = `
+ query GetUserMarketPosition($marketId: ID!, $userId: ID!) {
+ positions(
+ where: { market: $marketId, account: $userId }
+ ) {
+ id
+ asset {
+ id # Token address
+ }
+ isCollateral
+ balance
+ side # SUPPLIER, BORROWER, COLLATERAL
+ }
+ }
+`;
+// --- End Query ---
+
// Note: The exact field names might need adjustment based on the specific Subgraph schema.
export const subgraphUserTransactionsQuery = `
query GetUserTransactions(
diff --git a/src/hooks/useUserPosition.ts b/src/hooks/useUserPosition.ts
index 6572fa6a..5efb6c47 100644
--- a/src/hooks/useUserPosition.ts
+++ b/src/hooks/useUserPosition.ts
@@ -1,92 +1,118 @@
-import { useState, useEffect, useCallback } from 'react';
+import { useQuery } from '@tanstack/react-query';
import { Address } from 'viem';
-import { userPositionForMarketQuery } from '@/graphql/morpho-api-queries';
+import { getMarketDataSource } from '@/config/dataSources';
+import { fetchMorphoUserPositionForMarket } from '@/data-sources/morpho-api/positions';
+import { fetchSubgraphUserPositionForMarket } from '@/data-sources/subgraph/positions';
import { SupportedNetworks } from '@/utils/networks';
import { fetchPositionSnapshot } from '@/utils/positions';
import { MarketPosition } from '@/utils/types';
-import { URLS } from '@/utils/urls';
-const useUserPositions = (
+/**
+ * Hook to fetch a user's position in a specific market.
+ *
+ * Prioritizes the latest on-chain snapshot via `fetchPositionSnapshot`.
+ * Falls back to the configured data source (Morpho API or Subgraph) if the snapshot is unavailable.
+ *
+ * @param user The user's address.
+ * @param chainId The network ID.
+ * @param marketKey The unique key of the market.
+ * @returns User position data, loading state, error state, and refetch function.
+ */
+const useUserPosition = (
user: string | undefined,
- chainId: SupportedNetworks,
- marketKey: string,
+ chainId: SupportedNetworks | undefined,
+ marketKey: string | undefined,
) => {
- const [loading, setLoading] = useState(true);
- const [isRefetching, setIsRefetching] = useState(false);
- const [position, setPosition] = useState(null);
- const [positionsError, setPositionsError] = useState(null);
+ const queryKey = ['userPosition', user, chainId, marketKey];
- const fetchData = useCallback(
- async (isRefetch = false, onSuccess?: () => void) => {
- if (!user) {
- console.error('Missing user address');
- setLoading(false);
- setIsRefetching(false);
- return;
+ const { data, isLoading, error, refetch, isRefetching } = useQuery<
+ MarketPosition | null,
+ unknown
+ >({
+ queryKey: queryKey,
+ queryFn: async (): Promise => {
+ if (!user || !chainId || !marketKey) {
+ console.log('Missing user, chainId, or marketKey for useUserPosition');
+ return null;
}
- try {
- if (isRefetch) {
- setIsRefetching(true);
- } else {
- setLoading(true);
- }
-
- setPositionsError(null);
+ // 1. Try fetching the on-chain snapshot first
+ console.log(`Attempting fetchPositionSnapshot for ${user} on market ${marketKey}`);
+ const snapshot = await fetchPositionSnapshot(marketKey, user as Address, chainId, 0);
- // Fetch position data from both networks
- const res = await fetch(URLS.MORPHO_BLUE_API, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- query: userPositionForMarketQuery,
- variables: {
- address: user.toLowerCase(),
- chainId: chainId,
- marketKey,
- },
- }),
- });
-
- const data = (await res.json()) as { data: { marketPosition: MarketPosition } };
+ if (snapshot) {
+ // If snapshot has zero balances, treat as null position early
+ if (
+ snapshot.supplyAssets === '0' &&
+ snapshot.borrowAssets === '0' &&
+ snapshot.collateral === '0'
+ ) {
+ console.log(
+ `Snapshot shows zero balance for ${user} on market ${marketKey}, returning null.`,
+ );
+ return null;
+ }
+ }
- // Read on-chain data
- const currentSnapshot = await fetchPositionSnapshot(marketKey, user as Address, chainId, 0);
+ // 2. Determine fallback data source
+ const dataSource = getMarketDataSource(chainId);
+ console.log(`Fallback data source for ${chainId}: ${dataSource}`);
- if (currentSnapshot) {
- setPosition({
- market: data.data.marketPosition.market,
- state: currentSnapshot,
- });
- } else {
- setPosition(data.data.marketPosition);
+ // 3. Fetch from the determined data source
+ let positionData: MarketPosition | null = null;
+ try {
+ if (dataSource === 'morpho') {
+ positionData = await fetchMorphoUserPositionForMarket(marketKey, user, chainId);
+ } else if (dataSource === 'subgraph') {
+ positionData = await fetchSubgraphUserPositionForMarket(marketKey, user, chainId);
}
+ } catch (fetchError) {
+ console.error(
+ `Failed to fetch user position via fallback (${dataSource}) for ${user} on market ${marketKey}:`,
+ fetchError,
+ );
+ return null; // Return null on error during fallback
+ }
- onSuccess?.();
- } catch (err) {
- console.error('Error fetching positions:', err);
- setPositionsError(err);
- } finally {
- setLoading(false);
- setIsRefetching(false);
+ // If we got a snapshot earlier, overwrite the state from the fallback with the fresh snapshot state
+ // Ensure the structure matches MarketPosition.state
+ if (snapshot && positionData) {
+ console.log(`Overwriting fallback state with fresh snapshot state for ${marketKey}`);
+ positionData.state = {
+ supplyAssets: snapshot.supplyAssets.toString(),
+ supplyShares: snapshot.supplyShares.toString(),
+ borrowAssets: snapshot.borrowAssets.toString(),
+ borrowShares: snapshot.borrowShares.toString(),
+ collateral: snapshot.collateral,
+ };
+ } else if (snapshot && !positionData) {
+ // If snapshot exists but fallback failed, we cannot construct MarketPosition
+ console.warn(
+ `Snapshot existed but fallback failed for ${marketKey}, cannot return full MarketPosition.`,
+ );
+ return null;
}
- },
- [user, chainId, marketKey],
- );
- useEffect(() => {
- void fetchData();
- }, [fetchData]);
+ console.log(
+ `Final position data for ${user} on market ${marketKey}:`,
+ positionData ? 'Found' : 'Not Found',
+ );
+ return positionData; // This will be null if neither snapshot nor fallback worked, or if balances were zero
+ },
+ enabled: !!user && !!chainId && !!marketKey,
+ staleTime: 1000 * 60 * 1, // Stale after 1 minute
+ refetchInterval: 1000 * 60 * 5, // Refetch every 5 minutes
+ placeholderData: (previousData) => previousData ?? null,
+ retry: 1, // Retry once on error
+ });
return {
- position,
- loading,
+ position: data,
+ loading: isLoading,
isRefetching,
- positionsError,
- refetch: (onSuccess?: () => void) => void fetchData(true, onSuccess),
+ error,
+ refetch,
};
};
-export default useUserPositions;
+export default useUserPosition;
From 7ae0821d96b9d8bcbdd07902c07d98a103ee3c02 Mon Sep 17 00:00:00 2001
From: antoncoding
Date: Sun, 27 Apr 2025 00:17:47 +0800
Subject: [PATCH 13/20] chore: lint and fix util rates
---
app/market/[chainId]/[marketid]/RateChart.tsx | 2 ++
src/data-sources/morpho-api/positions.ts | 12 ++++--------
src/data-sources/morpho-api/transactions.ts | 1 -
src/data-sources/subgraph/market.ts | 2 +-
src/hooks/useUserPosition.ts | 2 +-
5 files changed, 8 insertions(+), 11 deletions(-)
diff --git a/app/market/[chainId]/[marketid]/RateChart.tsx b/app/market/[chainId]/[marketid]/RateChart.tsx
index ea60009c..0f683cc8 100644
--- a/app/market/[chainId]/[marketid]/RateChart.tsx
+++ b/app/market/[chainId]/[marketid]/RateChart.tsx
@@ -54,6 +54,8 @@ function RateChart({
}));
};
+ console.log('market', market);
+
const formatPercentage = (value: number) => `${(value * 100).toFixed(2)}%`;
const getCurrentApyValue = (type: 'supply' | 'borrow') => {
diff --git a/src/data-sources/morpho-api/positions.ts b/src/data-sources/morpho-api/positions.ts
index e6f62432..61a8fb06 100644
--- a/src/data-sources/morpho-api/positions.ts
+++ b/src/data-sources/morpho-api/positions.ts
@@ -1,7 +1,6 @@
import { userPositionsQuery, userPositionForMarketQuery } from '@/graphql/morpho-api-queries';
import { SupportedNetworks } from '@/utils/networks';
import { MarketPosition } from '@/utils/types';
-import { URLS } from '@/utils/urls';
import { morphoGraphqlFetcher } from './fetchers';
// Type for the raw response from the Morpho API userPositionsQuery
@@ -38,13 +37,10 @@ export const fetchMorphoUserPositionMarkets = async (
network: SupportedNetworks,
): Promise<{ marketUniqueKey: string; chainId: number }[]> => {
try {
- const result = await morphoGraphqlFetcher(
- userPositionsQuery,
- {
- address: userAddress.toLowerCase(),
- chainId: network,
- },
- );
+ const result = await morphoGraphqlFetcher(userPositionsQuery, {
+ address: userAddress.toLowerCase(),
+ chainId: network,
+ });
const marketPositions = result.data?.userByAddress?.marketPositions ?? [];
diff --git a/src/data-sources/morpho-api/transactions.ts b/src/data-sources/morpho-api/transactions.ts
index 90068380..20e3e77a 100644
--- a/src/data-sources/morpho-api/transactions.ts
+++ b/src/data-sources/morpho-api/transactions.ts
@@ -1,7 +1,6 @@
import { userTransactionsQuery } from '@/graphql/morpho-api-queries';
import { TransactionFilters, TransactionResponse } from '@/hooks/useUserTransactions';
import { SupportedNetworks } from '@/utils/networks';
-import { URLS } from '@/utils/urls';
import { morphoGraphqlFetcher } from './fetchers';
// Define the expected shape of the GraphQL response for transactions
diff --git a/src/data-sources/subgraph/market.ts b/src/data-sources/subgraph/market.ts
index 90686e9e..7ff41293 100644
--- a/src/data-sources/subgraph/market.ts
+++ b/src/data-sources/subgraph/market.ts
@@ -142,7 +142,7 @@ const transformSubgraphMarketToMarket = (
const totalSupplyNum = safeParseFloat(supplyAssets);
const totalBorrowNum = safeParseFloat(borrowAssets);
- const utilization = totalSupplyNum > 0 ? (totalBorrowNum / totalSupplyNum) * 100 : 0;
+ const utilization = totalSupplyNum > 0 ? totalBorrowNum / totalSupplyNum : 0;
const supplyApy = Number(subgraphMarket.rates?.find((r) => r.side === 'LENDER')?.rate ?? 0);
const borrowApy = Number(subgraphMarket.rates?.find((r) => r.side === 'BORROWER')?.rate ?? 0);
diff --git a/src/hooks/useUserPosition.ts b/src/hooks/useUserPosition.ts
index 5efb6c47..e3c5c234 100644
--- a/src/hooks/useUserPosition.ts
+++ b/src/hooks/useUserPosition.ts
@@ -107,7 +107,7 @@ const useUserPosition = (
});
return {
- position: data,
+ position: data ?? null,
loading: isLoading,
isRefetching,
error,
From 168408b7551d9eb18c240acea68a20abccf32c2a Mon Sep 17 00:00:00 2001
From: antoncoding
Date: Sun, 27 Apr 2025 00:34:25 +0800
Subject: [PATCH 14/20] fix: empty markets
---
src/data-sources/subgraph/market.ts | 11 ++++++-----
src/data-sources/subgraph/queries.ts | 0
2 files changed, 6 insertions(+), 5 deletions(-)
delete mode 100644 src/data-sources/subgraph/queries.ts
diff --git a/src/data-sources/subgraph/market.ts b/src/data-sources/subgraph/market.ts
index 7ff41293..347c720c 100644
--- a/src/data-sources/subgraph/market.ts
+++ b/src/data-sources/subgraph/market.ts
@@ -93,6 +93,10 @@ const transformSubgraphMarketToMarket = (
const irmAddress = subgraphMarket.irm ?? '0x';
const inputTokenPriceUSD = subgraphMarket.inputTokenPriceUSD ?? '0';
+ if (marketId.toLowerCase() === '0x9103c3b4e834476c9a62ea009ba2c884ee42e94e6e314a26f04d312434191836') {
+ console.log('subgraphMarket', subgraphMarket)
+ }
+
const totalBorrowBalanceUSD = subgraphMarket.totalBorrowBalanceUSD ?? '0';
const totalSupplyShares = subgraphMarket.totalSupplyShares ?? '0';
const totalBorrowShares = subgraphMarket.totalBorrowShares ?? '0';
@@ -147,9 +151,6 @@ const transformSubgraphMarketToMarket = (
const supplyApy = Number(subgraphMarket.rates?.find((r) => r.side === 'LENDER')?.rate ?? 0);
const borrowApy = Number(subgraphMarket.rates?.find((r) => r.side === 'BORROWER')?.rate ?? 0);
- // only borrowBalanceUSD is available in subgraph, we need to calculate supplyAssetsUsd, liquidityAssetsUsd, collateralAssetsUsd
- const borrowAssetsUsd = safeParseFloat(totalBorrowBalanceUSD);
-
// get the prices
let loanAssetPrice = safeParseFloat(subgraphMarket.borrowedToken?.lastPriceUSD ?? '0');
let collateralAssetPrice = safeParseFloat(subgraphMarket.inputToken?.lastPriceUSD ?? '0');
@@ -170,6 +171,7 @@ const transformSubgraphMarketToMarket = (
}
const supplyAssetsUsd = formatBalance(supplyAssets, loanAsset.decimals) * loanAssetPrice;
+ const borrowAssetsUsd = formatBalance(borrowAssets, loanAsset.decimals) * loanAssetPrice;
const liquidityAssets = (BigInt(supplyAssets) - BigInt(borrowAssets)).toString();
const liquidityAssetsUsd = formatBalance(liquidityAssets, loanAsset.decimals) * loanAssetPrice;
@@ -287,7 +289,7 @@ export const fetchSubgraphMarkets = async (network: SupportedNetworks): Promise<
const variables: SubgraphMarketsVariables = {
first: 1000, // Max limit
where: {
- inputToken_not_in: blacklistTokens,
+ inputToken_not_in: [...blacklistTokens, '0x0000000000000000000000000000000000000000'],
},
};
@@ -308,7 +310,6 @@ export const fetchSubgraphMarkets = async (network: SupportedNetworks): Promise<
// Fetch major prices *once* before transforming all markets
const majorPrices = await fetchLocalMajorPrices();
-
// Transform each market using the fetched prices
return marketsData.map((market) => transformSubgraphMarketToMarket(market, network, majorPrices));
};
diff --git a/src/data-sources/subgraph/queries.ts b/src/data-sources/subgraph/queries.ts
deleted file mode 100644
index e69de29b..00000000
From b5daa9efe7db36787f131efc3db4ec7ea771043d Mon Sep 17 00:00:00 2001
From: antoncoding
Date: Sun, 27 Apr 2025 00:41:05 +0800
Subject: [PATCH 15/20] feat: lacking data source for oracles
---
src/config/dataSources.ts | 8 +++----
src/data-sources/subgraph/market.ts | 5 +++--
src/utils/warnings.ts | 33 +++++++----------------------
3 files changed, 15 insertions(+), 31 deletions(-)
diff --git a/src/config/dataSources.ts b/src/config/dataSources.ts
index f8e55e8c..e27d8b67 100644
--- a/src/config/dataSources.ts
+++ b/src/config/dataSources.ts
@@ -5,10 +5,10 @@ import { SupportedNetworks } from '@/utils/networks';
*/
export const getMarketDataSource = (network: SupportedNetworks): 'morpho' | 'subgraph' => {
switch (network) {
- case SupportedNetworks.Mainnet:
- return 'subgraph';
- case SupportedNetworks.Base:
- return 'subgraph';
+ // case SupportedNetworks.Mainnet:
+ // return 'subgraph';
+ // case SupportedNetworks.Base:
+ // return 'subgraph';
default:
return 'morpho'; // Default to Morpho API
}
diff --git a/src/data-sources/subgraph/market.ts b/src/data-sources/subgraph/market.ts
index 347c720c..830feac1 100644
--- a/src/data-sources/subgraph/market.ts
+++ b/src/data-sources/subgraph/market.ts
@@ -21,6 +21,7 @@ import {
} from '@/utils/tokens';
import { WarningWithDetail, MorphoChainlinkOracleData, Market } from '@/utils/types';
import { subgraphGraphqlFetcher } from './fetchers';
+import { getMarketWarningsWithDetail, subgraphDefaultWarnings } from '@/utils/warnings';
// Define the structure for the fetched prices locally
type LocalMajorPrices = {
@@ -179,7 +180,7 @@ const transformSubgraphMarketToMarket = (
const collateralAssetsUsd =
formatBalance(collateralAssets, collateralAsset.decimals) * collateralAssetPrice;
- const warningsWithDetail: WarningWithDetail[] = []; // Subgraph doesn't provide warnings directly
+ const warningsWithDetail = getMarketWarningsWithDetail({warnings:subgraphDefaultWarnings});
const marketDetail: Market = {
id: marketId,
@@ -219,7 +220,7 @@ const transformSubgraphMarketToMarket = (
id: chainId,
},
},
- warnings: [], // Subgraph doesn't provide warnings
+ warnings: subgraphDefaultWarnings,
warningsWithDetail: warningsWithDetail,
oracle: {
data: defaultOracleData, // Placeholder oracle data
diff --git a/src/utils/warnings.ts b/src/utils/warnings.ts
index 1e645d1b..6f7a2438 100644
--- a/src/utils/warnings.ts
+++ b/src/utils/warnings.ts
@@ -1,6 +1,14 @@
import { MarketWarning } from '@/utils/types';
import { WarningCategory, WarningWithDetail } from './types';
+export const subgraphDefaultWarnings: MarketWarning[] = [
+ {
+ type: 'unrecognized_oracle',
+ level: 'alert',
+ __typename: 'OracleWarning_MonarchAttached',
+ },
+];
+
const morphoOfficialWarnings: WarningWithDetail[] = [
{
code: 'hardcoded_oracle',
@@ -102,30 +110,5 @@ export const getMarketWarningsWithDetail = (market: { warnings: MarketWarning[]
result.push(foundWarning);
}
}
-
- // ======================
- // Add Extra warnings
- // ======================
-
- // bad debt warnings
- // if (market.badDebt && market.badDebt.usd > 0) {
- // const warning = morphoOfficialWarnings.find((w) => w.code === 'bad_debt_unrealized');
- // if (warning) {
- // if (Number(market.badDebt.usd) > 0.01 * Number(market.state.supplyAssetsUsd)) {
- // warning.level = 'alert';
- // }
- // result.push(warning);
- // }
- // }
- // if (market.realizedBadDebt && market.realizedBadDebt.usd > 0) {
- // const warning = morphoOfficialWarnings.find((w) => w.code === 'bad_debt_realized');
- // if (warning) {
- // if (Number(market.realizedBadDebt.usd) > 0.01 * Number(market.state.supplyAssetsUsd)) {
- // warning.level = 'alert';
- // }
- // result.push(warning);
- // }
- // }
-
return result;
};
From 15082c1a9fdab372330bc2632bdb97c39c994633 Mon Sep 17 00:00:00 2001
From: antoncoding
Date: Sun, 27 Apr 2025 01:12:13 +0800
Subject: [PATCH 16/20] fix: default oracle to showing unknown instaed of no
oracle
---
src/data-sources/subgraph/market.ts | 51 +++++++++++++++++++------
src/utils/oracle.ts | 1 +
src/utils/warnings.ts | 58 +++++++++++++++++++++++++----
3 files changed, 90 insertions(+), 20 deletions(-)
diff --git a/src/data-sources/subgraph/market.ts b/src/data-sources/subgraph/market.ts
index 830feac1..7fb1012b 100644
--- a/src/data-sources/subgraph/market.ts
+++ b/src/data-sources/subgraph/market.ts
@@ -1,4 +1,4 @@
-import { Address } from 'viem';
+import { Address, zeroAddress } from 'viem';
import {
marketQuery as subgraphMarketQuery,
marketsQuery as subgraphMarketsQuery,
@@ -19,9 +19,15 @@ import {
UnknownERC20Token,
TokenPeg,
} from '@/utils/tokens';
-import { WarningWithDetail, MorphoChainlinkOracleData, Market } from '@/utils/types';
+import { MorphoChainlinkOracleData, Market } from '@/utils/types';
+import {
+ getMarketWarningsWithDetail,
+ SUBGRAPH_NO_ORACLE,
+ SUBGRAPH_NO_PRICE,
+ UNRECOGNIZED_COLLATERAL,
+ UNRECOGNIZED_LOAN,
+} from '@/utils/warnings';
import { subgraphGraphqlFetcher } from './fetchers';
-import { getMarketWarningsWithDetail, subgraphDefaultWarnings } from '@/utils/warnings';
// Define the structure for the fetched prices locally
type LocalMajorPrices = {
@@ -94,11 +100,12 @@ const transformSubgraphMarketToMarket = (
const irmAddress = subgraphMarket.irm ?? '0x';
const inputTokenPriceUSD = subgraphMarket.inputTokenPriceUSD ?? '0';
- if (marketId.toLowerCase() === '0x9103c3b4e834476c9a62ea009ba2c884ee42e94e6e314a26f04d312434191836') {
- console.log('subgraphMarket', subgraphMarket)
+ if (
+ marketId.toLowerCase() === '0x9103c3b4e834476c9a62ea009ba2c884ee42e94e6e314a26f04d312434191836'
+ ) {
+ console.log('subgraphMarket', subgraphMarket);
}
- const totalBorrowBalanceUSD = subgraphMarket.totalBorrowBalanceUSD ?? '0';
const totalSupplyShares = subgraphMarket.totalSupplyShares ?? '0';
const totalBorrowShares = subgraphMarket.totalBorrowShares ?? '0';
const fee = subgraphMarket.fee ?? '0';
@@ -129,7 +136,16 @@ const transformSubgraphMarketToMarket = (
const collateralAsset = mapToken(subgraphMarket.inputToken);
const defaultOracleData: MorphoChainlinkOracleData = {
- baseFeedOne: null,
+ baseFeedOne: {
+ address: zeroAddress,
+ chain: {
+ id: network,
+ },
+ description: null,
+ id: zeroAddress,
+ pair: null,
+ vendor: 'Unknown',
+ },
baseFeedTwo: null,
quoteFeedOne: null,
quoteFeedTwo: null,
@@ -152,23 +168,34 @@ const transformSubgraphMarketToMarket = (
const supplyApy = Number(subgraphMarket.rates?.find((r) => r.side === 'LENDER')?.rate ?? 0);
const borrowApy = Number(subgraphMarket.rates?.find((r) => r.side === 'BORROWER')?.rate ?? 0);
+ const warnings = [SUBGRAPH_NO_ORACLE];
+
// get the prices
let loanAssetPrice = safeParseFloat(subgraphMarket.borrowedToken?.lastPriceUSD ?? '0');
let collateralAssetPrice = safeParseFloat(subgraphMarket.inputToken?.lastPriceUSD ?? '0');
// @todo: might update due to input token being used here
const hasUSDPrice = loanAssetPrice > 0 && collateralAssetPrice > 0;
+
+ const knownLoadAsset = findToken(loanAsset.address, network);
+ const knownCollateralAsset = findToken(collateralAsset.address, network);
+
+ if (!knownLoadAsset) {
+ warnings.push(UNRECOGNIZED_LOAN);
+ }
+ if (!knownCollateralAsset) {
+ warnings.push(UNRECOGNIZED_COLLATERAL);
+ }
+
if (!hasUSDPrice) {
// no price available, try to estimate
-
- const knownLoadAsset = findToken(loanAsset.address, network);
if (knownLoadAsset) {
loanAssetPrice = getEstimateValue(knownLoadAsset) ?? 0;
}
- const knownCollateralAsset = findToken(collateralAsset.address, network);
if (knownCollateralAsset) {
collateralAssetPrice = getEstimateValue(knownCollateralAsset) ?? 0;
}
+ warnings.push(SUBGRAPH_NO_PRICE);
}
const supplyAssetsUsd = formatBalance(supplyAssets, loanAsset.decimals) * loanAssetPrice;
@@ -180,7 +207,7 @@ const transformSubgraphMarketToMarket = (
const collateralAssetsUsd =
formatBalance(collateralAssets, collateralAsset.decimals) * collateralAssetPrice;
- const warningsWithDetail = getMarketWarningsWithDetail({warnings:subgraphDefaultWarnings});
+ const warningsWithDetail = getMarketWarningsWithDetail({ warnings });
const marketDetail: Market = {
id: marketId,
@@ -220,7 +247,7 @@ const transformSubgraphMarketToMarket = (
id: chainId,
},
},
- warnings: subgraphDefaultWarnings,
+ warnings: warnings,
warningsWithDetail: warningsWithDetail,
oracle: {
data: defaultOracleData, // Placeholder oracle data
diff --git a/src/utils/oracle.ts b/src/utils/oracle.ts
index a3dd0496..c1863a8f 100644
--- a/src/utils/oracle.ts
+++ b/src/utils/oracle.ts
@@ -27,6 +27,7 @@ export const OracleVendorIcons: Record = {
export function parseOracleVendors(oracleData: MorphoChainlinkOracleData | null): VendorInfo {
if (!oracleData) return { vendors: [], isUnknown: false };
+
if (
!oracleData.baseFeedOne &&
!oracleData.baseFeedTwo &&
diff --git a/src/utils/warnings.ts b/src/utils/warnings.ts
index 6f7a2438..17181ab6 100644
--- a/src/utils/warnings.ts
+++ b/src/utils/warnings.ts
@@ -1,13 +1,37 @@
import { MarketWarning } from '@/utils/types';
import { WarningCategory, WarningWithDetail } from './types';
-export const subgraphDefaultWarnings: MarketWarning[] = [
- {
- type: 'unrecognized_oracle',
- level: 'alert',
- __typename: 'OracleWarning_MonarchAttached',
- },
-];
+// Subgraph Warnings
+
+// Default subrgaph has no oracle data attached!
+export const SUBGRAPH_NO_ORACLE = {
+ type: 'subgraph_unrecognized_oracle',
+ level: 'alert',
+ __typename: 'OracleWarning_MonarchAttached',
+};
+
+// Most subgraph markets has no price data
+export const SUBGRAPH_NO_PRICE = {
+ type: 'subgraph_no_price',
+ level: 'warning',
+ __typename: 'MarketWarning_SubgraphNoPrice',
+};
+
+export const subgraphDefaultWarnings: MarketWarning[] = [SUBGRAPH_NO_ORACLE];
+
+export const UNRECOGNIZED_LOAN = {
+ type: 'unrecognized_loan_asset',
+ level: 'alert',
+ __typename: 'MarketWarning_UnrecognizedLoanAsset',
+};
+
+export const UNRECOGNIZED_COLLATERAL = {
+ type: 'unrecognized_collateral_asset',
+ level: 'alert',
+ __typename: 'MarketWarning_UnrecognizedCollateralAsset',
+};
+
+// Morpho Official Warnings
const morphoOfficialWarnings: WarningWithDetail[] = [
{
@@ -100,12 +124,30 @@ const morphoOfficialWarnings: WarningWithDetail[] = [
},
];
+const subgraphWarnings: WarningWithDetail[] = [
+ {
+ code: 'subgraph_unrecognized_oracle',
+ level: 'alert',
+ description:
+ 'The underlying data source (subgraph) does not provide any details on this oralce address.',
+ category: WarningCategory.oracle,
+ },
+ {
+ code: 'subgraph_no_price',
+ level: 'warning',
+ description: 'The USD value of the market is estimated with an offchain price source.',
+ category: WarningCategory.general,
+ },
+];
+
export const getMarketWarningsWithDetail = (market: { warnings: MarketWarning[] }) => {
const result = [];
+ const allDetails = [...morphoOfficialWarnings, ...subgraphWarnings];
+
// process official warnings
for (const warning of market.warnings) {
- const foundWarning = morphoOfficialWarnings.find((w) => w.code === warning.type);
+ const foundWarning = allDetails.find((w) => w.code === warning.type);
if (foundWarning) {
result.push(foundWarning);
}
From 846f502c0a1343f37133c43bde10c52b4cc2ee0b Mon Sep 17 00:00:00 2001
From: antoncoding
Date: Sun, 27 Apr 2025 11:05:53 +0800
Subject: [PATCH 17/20] misc: fix type
---
app/markets/components/MarketTableUtils.tsx | 2 +-
app/markets/components/utils.ts | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/app/markets/components/MarketTableUtils.tsx b/app/markets/components/MarketTableUtils.tsx
index 04f296e9..b26609cd 100644
--- a/app/markets/components/MarketTableUtils.tsx
+++ b/app/markets/components/MarketTableUtils.tsx
@@ -79,7 +79,7 @@ export function TDTotalSupplyOrBorrow({
symbol,
}: {
dataLabel: string;
- assetsUSD: string;
+ assetsUSD: number;
assets: string;
decimals: number;
symbol: string;
diff --git a/app/markets/components/utils.ts b/app/markets/components/utils.ts
index aab898b0..97add4ba 100644
--- a/app/markets/components/utils.ts
+++ b/app/markets/components/utils.ts
@@ -109,8 +109,8 @@ export function applyFilterAndSort(
}
// Add USD Filters
- const supplyUsd = parseUsdValue(market.state?.supplyAssetsUsd); // Use optional chaining
- const borrowUsd = parseUsdValue(market.state?.borrowAssetsUsd); // Use optional chaining
+ const supplyUsd = parseUsdValue(market.state?.supplyAssetsUsd.toString()); // Use optional chaining
+ const borrowUsd = parseUsdValue(market.state?.borrowAssetsUsd.toString()); // Use optional chaining
if (minSupplyUsd !== null && (supplyUsd === null || supplyUsd < minSupplyUsd)) {
return false;
From 21d3b52f325bafebb062c9b8df32a3a89db88ea7 Mon Sep 17 00:00:00 2001
From: antoncoding
Date: Sun, 27 Apr 2025 11:12:32 +0800
Subject: [PATCH 18/20] fix: refetch
---
src/data-sources/subgraph/market.ts | 2 +-
src/hooks/useUserPosition.ts | 24 ++++++++++++++++++++----
src/utils/oracle.ts | 2 +-
3 files changed, 22 insertions(+), 6 deletions(-)
diff --git a/src/data-sources/subgraph/market.ts b/src/data-sources/subgraph/market.ts
index 7fb1012b..4ffd9d12 100644
--- a/src/data-sources/subgraph/market.ts
+++ b/src/data-sources/subgraph/market.ts
@@ -136,7 +136,7 @@ const transformSubgraphMarketToMarket = (
const collateralAsset = mapToken(subgraphMarket.inputToken);
const defaultOracleData: MorphoChainlinkOracleData = {
- baseFeedOne: {
+ baseFeedOne: {
address: zeroAddress,
chain: {
id: network,
diff --git a/src/hooks/useUserPosition.ts b/src/hooks/useUserPosition.ts
index e3c5c234..57c2a4f4 100644
--- a/src/hooks/useUserPosition.ts
+++ b/src/hooks/useUserPosition.ts
@@ -25,10 +25,13 @@ const useUserPosition = (
) => {
const queryKey = ['userPosition', user, chainId, marketKey];
- const { data, isLoading, error, refetch, isRefetching } = useQuery<
- MarketPosition | null,
- unknown
- >({
+ const {
+ data,
+ isLoading,
+ error,
+ refetch: refetchQuery,
+ isRefetching,
+ } = useQuery({
queryKey: queryKey,
queryFn: async (): Promise => {
if (!user || !chainId || !marketKey) {
@@ -106,6 +109,19 @@ const useUserPosition = (
retry: 1, // Retry once on error
});
+ // refetch with onsuccess callback
+ const refetch = (onSuccess?: () => void) => {
+ refetchQuery()
+ .then(() => {
+ // Call onSuccess callback if provided after successful refetch
+ onSuccess?.();
+ })
+ .catch((err) => {
+ // Optional: Log error during refetch, but don't trigger onSuccess
+ console.error('Error during refetch triggered by refetch function:', err);
+ });
+ };
+
return {
position: data ?? null,
loading: isLoading,
diff --git a/src/utils/oracle.ts b/src/utils/oracle.ts
index c1863a8f..9caf6232 100644
--- a/src/utils/oracle.ts
+++ b/src/utils/oracle.ts
@@ -27,7 +27,7 @@ export const OracleVendorIcons: Record = {
export function parseOracleVendors(oracleData: MorphoChainlinkOracleData | null): VendorInfo {
if (!oracleData) return { vendors: [], isUnknown: false };
-
+
if (
!oracleData.baseFeedOne &&
!oracleData.baseFeedTwo &&
From ea3f393edc0853249b3d7262ddd5b1a889ac8bcf Mon Sep 17 00:00:00 2001
From: antoncoding
Date: Sun, 27 Apr 2025 11:28:13 +0800
Subject: [PATCH 19/20] chore: fix useUserPosition
---
src/config/dataSources.ts | 8 +-
src/data-sources/subgraph/liquidations.ts | 41 +++----
src/hooks/useUserPosition.ts | 143 ++++++++++++++--------
3 files changed, 117 insertions(+), 75 deletions(-)
diff --git a/src/config/dataSources.ts b/src/config/dataSources.ts
index e27d8b67..f8e55e8c 100644
--- a/src/config/dataSources.ts
+++ b/src/config/dataSources.ts
@@ -5,10 +5,10 @@ import { SupportedNetworks } from '@/utils/networks';
*/
export const getMarketDataSource = (network: SupportedNetworks): 'morpho' | 'subgraph' => {
switch (network) {
- // case SupportedNetworks.Mainnet:
- // return 'subgraph';
- // case SupportedNetworks.Base:
- // return 'subgraph';
+ case SupportedNetworks.Mainnet:
+ return 'subgraph';
+ case SupportedNetworks.Base:
+ return 'subgraph';
default:
return 'morpho'; // Default to Morpho API
}
diff --git a/src/data-sources/subgraph/liquidations.ts b/src/data-sources/subgraph/liquidations.ts
index 3b256473..10bcd987 100644
--- a/src/data-sources/subgraph/liquidations.ts
+++ b/src/data-sources/subgraph/liquidations.ts
@@ -29,31 +29,31 @@ export const fetchSubgraphLiquidatedMarketKeys = async (
const liquidatedKeys = new Set();
// Apply the same base filters as fetchSubgraphMarkets
- const variables = {
- first: 1000, // Fetch in batches if necessary, though unlikely needed just for IDs
- where: {
- inputToken_not_in: blacklistTokens,
- },
- };
-
- try {
- // Subgraph might paginate; handle if necessary, but 1000 limit is often sufficient for just IDs
- const response = await subgraphGraphqlFetcher(
+ // paginate until the API returns < pageSize items
+ const pageSize = 1000;
+ let skip = 0;
+ while (true) {
+ const variables = {
+ first: pageSize,
+ skip,
+ where: { inputToken_not_in: blacklistTokens },
+ };
+ const page = await subgraphGraphqlFetcher(
subgraphApiUrl,
subgraphMarketsWithLiquidationCheckQuery,
variables,
);
- if (response.errors) {
- console.error('GraphQL errors:', response.errors);
+ if (page.errors) {
+ console.error('GraphQL errors:', page.errors);
throw new Error(`GraphQL error fetching liquidated market keys for network ${network}`);
}
- const markets = response.data?.markets;
+ const markets = page.data?.markets;
if (!markets) {
- console.warn(`No market data returned for liquidation check on network ${network}.`);
- return liquidatedKeys; // Return empty set
+ console.warn(`No market data returned for liquidation check on network ${network} at skip ${skip}.`);
+ break; // Exit loop if no markets are returned
}
markets.forEach((market) => {
@@ -62,12 +62,11 @@ export const fetchSubgraphLiquidatedMarketKeys = async (
liquidatedKeys.add(market.id);
}
});
- } catch (error) {
- console.error(
- `Error fetching liquidated market keys via Subgraph for network ${network}:`,
- error,
- );
- throw error; // Re-throw
+
+ if (markets.length < pageSize) {
+ break; // Exit loop if the number of returned markets is less than the page size
+ }
+ skip += pageSize;
}
console.log(`Fetched ${liquidatedKeys.size} liquidated market keys via Subgraph for ${network}.`);
diff --git a/src/hooks/useUserPosition.ts b/src/hooks/useUserPosition.ts
index 57c2a4f4..ba02f8e4 100644
--- a/src/hooks/useUserPosition.ts
+++ b/src/hooks/useUserPosition.ts
@@ -6,6 +6,7 @@ import { fetchSubgraphUserPositionForMarket } from '@/data-sources/subgraph/posi
import { SupportedNetworks } from '@/utils/networks';
import { fetchPositionSnapshot } from '@/utils/positions';
import { MarketPosition } from '@/utils/types';
+import { useMarkets } from './useMarkets';
/**
* Hook to fetch a user's position in a specific market.
@@ -25,6 +26,8 @@ const useUserPosition = (
) => {
const queryKey = ['userPosition', user, chainId, marketKey];
+ const { markets } = useMarkets()
+
const {
data,
isLoading,
@@ -41,66 +44,106 @@ const useUserPosition = (
// 1. Try fetching the on-chain snapshot first
console.log(`Attempting fetchPositionSnapshot for ${user} on market ${marketKey}`);
- const snapshot = await fetchPositionSnapshot(marketKey, user as Address, chainId, 0);
-
- if (snapshot) {
- // If snapshot has zero balances, treat as null position early
- if (
- snapshot.supplyAssets === '0' &&
- snapshot.borrowAssets === '0' &&
- snapshot.collateral === '0'
- ) {
- console.log(
- `Snapshot shows zero balance for ${user} on market ${marketKey}, returning null.`,
- );
- return null;
- }
- }
-
- // 2. Determine fallback data source
- const dataSource = getMarketDataSource(chainId);
- console.log(`Fallback data source for ${chainId}: ${dataSource}`);
-
- // 3. Fetch from the determined data source
- let positionData: MarketPosition | null = null;
+ let snapshot = null;
try {
- if (dataSource === 'morpho') {
- positionData = await fetchMorphoUserPositionForMarket(marketKey, user, chainId);
- } else if (dataSource === 'subgraph') {
- positionData = await fetchSubgraphUserPositionForMarket(marketKey, user, chainId);
- }
- } catch (fetchError) {
+ snapshot = await fetchPositionSnapshot(marketKey, user as Address, chainId, 0);
+ console.log(`Snapshot result for ${marketKey}:`, snapshot ? 'Exists' : 'Null');
+ } catch (snapshotError) {
console.error(
- `Failed to fetch user position via fallback (${dataSource}) for ${user} on market ${marketKey}:`,
- fetchError,
+ `Error fetching position snapshot for ${user} on market ${marketKey}:`,
+ snapshotError,
);
- return null; // Return null on error during fallback
+ // Snapshot fetch failed, will proceed to fallback fetch
}
- // If we got a snapshot earlier, overwrite the state from the fallback with the fresh snapshot state
- // Ensure the structure matches MarketPosition.state
- if (snapshot && positionData) {
- console.log(`Overwriting fallback state with fresh snapshot state for ${marketKey}`);
- positionData.state = {
- supplyAssets: snapshot.supplyAssets.toString(),
- supplyShares: snapshot.supplyShares.toString(),
- borrowAssets: snapshot.borrowAssets.toString(),
- borrowShares: snapshot.borrowShares.toString(),
- collateral: snapshot.collateral,
- };
- } else if (snapshot && !positionData) {
- // If snapshot exists but fallback failed, we cannot construct MarketPosition
- console.warn(
- `Snapshot existed but fallback failed for ${marketKey}, cannot return full MarketPosition.`,
- );
- return null;
+ let finalPosition: MarketPosition | null = null;
+
+ if (snapshot) {
+ // Snapshot succeeded, try to use local market data first
+ const market = markets?.find((m) => m.uniqueKey === marketKey);
+
+ if (market) {
+ // Local market data found, construct position directly
+ console.log(`Found local market data for ${marketKey}, constructing position from snapshot.`);
+ finalPosition = {
+ market: market,
+ state: { // Add state from snapshot
+ supplyAssets: snapshot.supplyAssets.toString(),
+ supplyShares: snapshot.supplyShares.toString(),
+ borrowAssets: snapshot.borrowAssets.toString(),
+ borrowShares: snapshot.borrowShares.toString(),
+ collateral: snapshot.collateral,
+ },
+ };
+ } else {
+ // Local market data NOT found, need to fetch from fallback to get structure
+ console.warn(
+ `Local market data not found for ${marketKey}. Fetching from fallback source to combine with snapshot.`,
+ );
+ const dataSource = getMarketDataSource(chainId);
+ let fallbackPosition: MarketPosition | null = null;
+ try {
+ if (dataSource === 'morpho') {
+ fallbackPosition = await fetchMorphoUserPositionForMarket(marketKey, user, chainId);
+ } else if (dataSource === 'subgraph') {
+ fallbackPosition = await fetchSubgraphUserPositionForMarket(marketKey, user, chainId);
+ }
+ if (fallbackPosition) {
+ // Fallback succeeded, combine with snapshot state
+ finalPosition = {
+ ...fallbackPosition,
+ state: {
+ supplyAssets: snapshot.supplyAssets.toString(),
+ supplyShares: snapshot.supplyShares.toString(),
+ borrowAssets: snapshot.borrowAssets.toString(),
+ borrowShares: snapshot.borrowShares.toString(),
+ collateral: snapshot.collateral,
+ },
+ };
+ } else {
+ // Fallback failed even though snapshot existed
+ console.error(
+ `Snapshot exists for ${marketKey}, but fallback fetch failed. Cannot return full position.`,
+ );
+ finalPosition = null;
+ }
+ } catch (fetchError) {
+ console.error(
+ `Failed to fetch user position via fallback (${dataSource}) for ${user} on market ${marketKey} after snapshot success:`,
+ fetchError,
+ );
+ finalPosition = null;
+ }
+ }
+ } else {
+ // Snapshot failed, rely entirely on the fallback data source
+ console.log(`Snapshot failed for ${marketKey}, fetching from fallback source.`);
+ const dataSource = getMarketDataSource(chainId);
+ try {
+ if (dataSource === 'morpho') {
+ finalPosition = await fetchMorphoUserPositionForMarket(marketKey, user, chainId);
+ } else if (dataSource === 'subgraph') {
+ finalPosition = await fetchSubgraphUserPositionForMarket(marketKey, user, chainId);
+ }
+ console.log(
+ `Fallback fetch result (after snapshot failure) for ${marketKey}:`,
+ finalPosition ? 'Found' : 'Not Found',
+ );
+ } catch (fetchError) {
+ console.error(
+ `Failed to fetch user position via fallback (${dataSource}) for ${user} on market ${marketKey}:`,
+ fetchError,
+ );
+ finalPosition = null; // Ensure null on error
+ }
}
console.log(
`Final position data for ${user} on market ${marketKey}:`,
- positionData ? 'Found' : 'Not Found',
+ finalPosition ? 'Found' : 'Not Found',
);
- return positionData; // This will be null if neither snapshot nor fallback worked, or if balances were zero
+ // If finalPosition has zero balances, it's still a valid position state from the snapshot or fallback
+ return finalPosition;
},
enabled: !!user && !!chainId && !!marketKey,
staleTime: 1000 * 60 * 1, // Stale after 1 minute
From b2e50b2b00cd820a9e36e89fa85f41864b47bbb8 Mon Sep 17 00:00:00 2001
From: antoncoding
Date: Sun, 27 Apr 2025 11:33:11 +0800
Subject: [PATCH 20/20] chore: review fixes
---
src/data-sources/subgraph/liquidations.ts | 4 +++-
src/data-sources/subgraph/market.ts | 13 ++++++++++---
src/hooks/useUserPosition.ts | 9 ++++++---
src/hooks/useUserPositions.ts | 9 ++++++++-
4 files changed, 27 insertions(+), 8 deletions(-)
diff --git a/src/data-sources/subgraph/liquidations.ts b/src/data-sources/subgraph/liquidations.ts
index 10bcd987..44d309fd 100644
--- a/src/data-sources/subgraph/liquidations.ts
+++ b/src/data-sources/subgraph/liquidations.ts
@@ -52,7 +52,9 @@ export const fetchSubgraphLiquidatedMarketKeys = async (
const markets = page.data?.markets;
if (!markets) {
- console.warn(`No market data returned for liquidation check on network ${network} at skip ${skip}.`);
+ console.warn(
+ `No market data returned for liquidation check on network ${network} at skip ${skip}.`,
+ );
break; // Exit loop if no markets are returned
}
diff --git a/src/data-sources/subgraph/market.ts b/src/data-sources/subgraph/market.ts
index 4ffd9d12..141d8fa2 100644
--- a/src/data-sources/subgraph/market.ts
+++ b/src/data-sources/subgraph/market.ts
@@ -19,7 +19,7 @@ import {
UnknownERC20Token,
TokenPeg,
} from '@/utils/tokens';
-import { MorphoChainlinkOracleData, Market } from '@/utils/types';
+import { MorphoChainlinkOracleData, Market, MarketWarning } from '@/utils/types';
import {
getMarketWarningsWithDetail,
SUBGRAPH_NO_ORACLE,
@@ -46,7 +46,11 @@ const COINGECKO_API_URL =
'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,ethereum&vs_currencies=usd';
// Fetcher for major prices needed for estimation
+const priceCache: { data?: LocalMajorPrices; ts?: number } = {};
const fetchLocalMajorPrices = async (): Promise => {
+ if (priceCache.data && Date.now() - (priceCache.ts ?? 0) < 60_000) {
+ return priceCache.data;
+ }
try {
const response = await fetch(COINGECKO_API_URL);
if (!response.ok) {
@@ -59,12 +63,15 @@ const fetchLocalMajorPrices = async (): Promise => {
[TokenPeg.ETH]: data.ethereum?.usd,
};
// Filter out undefined prices
- return Object.entries(prices).reduce((acc, [key, value]) => {
+ const result = Object.entries(prices).reduce((acc, [key, value]) => {
if (value !== undefined) {
acc[key as keyof LocalMajorPrices] = value;
}
return acc;
}, {} as LocalMajorPrices);
+ priceCache.data = result;
+ priceCache.ts = Date.now();
+ return result;
} catch (err) {
console.error('Failed to fetch internal major token prices for subgraph estimation:', err);
return {}; // Return empty object on error
@@ -168,7 +175,7 @@ const transformSubgraphMarketToMarket = (
const supplyApy = Number(subgraphMarket.rates?.find((r) => r.side === 'LENDER')?.rate ?? 0);
const borrowApy = Number(subgraphMarket.rates?.find((r) => r.side === 'BORROWER')?.rate ?? 0);
- const warnings = [SUBGRAPH_NO_ORACLE];
+ const warnings: MarketWarning[] = [SUBGRAPH_NO_ORACLE];
// get the prices
let loanAssetPrice = safeParseFloat(subgraphMarket.borrowedToken?.lastPriceUSD ?? '0');
diff --git a/src/hooks/useUserPosition.ts b/src/hooks/useUserPosition.ts
index ba02f8e4..2bc30332 100644
--- a/src/hooks/useUserPosition.ts
+++ b/src/hooks/useUserPosition.ts
@@ -26,7 +26,7 @@ const useUserPosition = (
) => {
const queryKey = ['userPosition', user, chainId, marketKey];
- const { markets } = useMarkets()
+ const { markets } = useMarkets();
const {
data,
@@ -64,10 +64,13 @@ const useUserPosition = (
if (market) {
// Local market data found, construct position directly
- console.log(`Found local market data for ${marketKey}, constructing position from snapshot.`);
+ console.log(
+ `Found local market data for ${marketKey}, constructing position from snapshot.`,
+ );
finalPosition = {
market: market,
- state: { // Add state from snapshot
+ state: {
+ // Add state from snapshot
supplyAssets: snapshot.supplyAssets.toString(),
supplyShares: snapshot.supplyShares.toString(),
borrowAssets: snapshot.borrowAssets.toString(),
diff --git a/src/hooks/useUserPositions.ts b/src/hooks/useUserPositions.ts
index c3c1ceb0..cdd5733d 100644
--- a/src/hooks/useUserPositions.ts
+++ b/src/hooks/useUserPositions.ts
@@ -51,7 +51,14 @@ export const positionKeys = {
[...positionKeys.all, 'snapshot', marketKey, userAddress, chainId] as const,
// Key for the final enhanced position data, dependent on initialData result
enhanced: (user: string | undefined, initialData: InitialDataResponse | undefined) =>
- ['enhanced-positions', user, initialData] as const,
+ [
+ 'enhanced-positions',
+ user,
+ initialData?.finalMarketKeys
+ .map((k) => `${k.marketUniqueKey.toLowerCase()}-${k.chainId}`)
+ .sort()
+ .join(','),
+ ] as const,
};
// --- Helper Fetch Function --- //