From f2b1f72943a87cf166ce01fea4788755b3db0269 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sat, 19 Apr 2025 16:53:25 +0800 Subject: [PATCH 01/11] misc: remove unused fields on market type --- src/contexts/MarketsContext.tsx | 6 +++--- src/graphql/queries.ts | 11 ----------- src/hooks/useLiquidations.ts | 10 +++++----- src/utils/types.ts | 10 +--------- 4 files changed, 9 insertions(+), 28 deletions(-) diff --git a/src/contexts/MarketsContext.tsx b/src/contexts/MarketsContext.tsx index fcc2ba30..d8b60c1b 100644 --- a/src/contexts/MarketsContext.tsx +++ b/src/contexts/MarketsContext.tsx @@ -46,7 +46,7 @@ export function MarketsProvider({ children }: MarketsProviderProps) { const { loading: liquidationsLoading, - liquidatedMarketIds, + liquidatedMarketKeys, error: liquidationsError, refetch: refetchLiquidations, } = useLiquidations(); @@ -81,7 +81,7 @@ export function MarketsProvider({ children }: MarketsProviderProps) { const processedMarkets = filtered.map((market) => { const warningsWithDetail = getMarketWarningsWithDetail(market); - const isProtectedByLiquidationBots = liquidatedMarketIds.has(market.id); + const isProtectedByLiquidationBots = liquidatedMarketKeys.has(market.uniqueKey); return { ...market, @@ -101,7 +101,7 @@ export function MarketsProvider({ children }: MarketsProviderProps) { } } }, - [liquidatedMarketIds], + [liquidatedMarketKeys], ); useEffect(() => { diff --git a/src/graphql/queries.ts b/src/graphql/queries.ts index 6e533dce..5e829d9b 100644 --- a/src/graphql/queries.ts +++ b/src/graphql/queries.ts @@ -13,7 +13,6 @@ export const feedFieldsFragment = ` export const marketFragment = ` fragment MarketFields on Market { - id lltv uniqueKey irmAddress @@ -27,16 +26,12 @@ export const marketFragment = ` id } } - oracleInfo { - type - } loanAsset { id address symbol name decimals - priceUsd } collateralAsset { id @@ -44,7 +39,6 @@ export const marketFragment = ` symbol name decimals - priceUsd } state { borrowAssets @@ -67,7 +61,6 @@ export const marketFragment = ` yearlySupplyTokens asset { address - priceUsd spotPriceEth } amountPerSuppliedToken @@ -80,10 +73,6 @@ export const marketFragment = ` weeklySupplyApy weeklyBorrowApy } - dailyApys { - netSupplyApy - netBorrowApy - } warnings { type level diff --git a/src/hooks/useLiquidations.ts b/src/hooks/useLiquidations.ts index 77bb01fe..bd758048 100644 --- a/src/hooks/useLiquidations.ts +++ b/src/hooks/useLiquidations.ts @@ -70,7 +70,7 @@ type QueryResult = { const useLiquidations = () => { const [loading, setLoading] = useState(true); const [isRefetching, setIsRefetching] = useState(false); - const [liquidatedMarketIds, setLiquidatedMarketIds] = useState>(new Set()); + const [liquidatedMarketKeys, setLiquidatedMarketKeys] = useState>(new Set()); const [error, setError] = useState(null); const fetchLiquidations = useCallback(async (isRefetch = false) => { @@ -80,7 +80,7 @@ const useLiquidations = () => { } else { setLoading(true); } - const liquidatedIds = new Set(); + const liquidatedKeys = new Set(); let skip = 0; const pageSize = 1000; let totalCount = 0; @@ -100,7 +100,7 @@ const useLiquidations = () => { liquidations.forEach((tx) => { if (tx.data && 'market' in tx.data) { - liquidatedIds.add(tx.data.market.id); + liquidatedKeys.add(tx.data.market.uniqueKey); } }); @@ -108,7 +108,7 @@ const useLiquidations = () => { skip += pageInfo.count; } while (skip < totalCount); - setLiquidatedMarketIds(liquidatedIds); + setLiquidatedMarketKeys(liquidatedKeys); } catch (_error) { setError(_error); } finally { @@ -125,7 +125,7 @@ const useLiquidations = () => { fetchLiquidations(true).catch(console.error); }, [fetchLiquidations]); - return { loading, isRefetching, liquidatedMarketIds, error, refetch }; + return { loading, isRefetching, liquidatedMarketKeys, error, refetch }; }; export default useLiquidations; diff --git a/src/utils/types.ts b/src/utils/types.ts index f5a5017b..0372d4f1 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -97,7 +97,6 @@ export type TokenInfo = { symbol: string; name: string; decimals: number; - priceUsd: number; }; // Common types @@ -273,9 +272,6 @@ export type Market = { id: number; }; }; - oracleInfo: { - type: string; - }; loanAsset: TokenInfo; collateralAsset: TokenInfo; state: { @@ -299,7 +295,6 @@ export type Market = { yearlySupplyTokens: string; asset: { address: string; - priceUsd: string | null; spotPriceEth: string | null; }; amountPerSuppliedToken: string; @@ -321,14 +316,11 @@ export type Market = { underlying: number; usd: number; }; - dailyApys: { - netSupplyApy: number; - netBorrowApy: number; - }; // appended by us warningsWithDetail: WarningWithDetail[]; isProtectedByLiquidationBots: boolean; + oracle: { data: MorphoChainlinkOracleData; }; From 4f740e06e8bff20ad307f67a5b4317abdb958950 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sat, 19 Apr 2025 16:56:43 +0800 Subject: [PATCH 02/11] misc: remove unused fields on market type --- src/graphql/queries.ts | 15 --------------- src/utils/morpho.ts | 17 ----------------- src/utils/types.ts | 15 --------------- 3 files changed, 47 deletions(-) diff --git a/src/graphql/queries.ts b/src/graphql/queries.ts index 5e829d9b..787bb16d 100644 --- a/src/graphql/queries.ts +++ b/src/graphql/queries.ts @@ -57,21 +57,6 @@ export const marketFragment = ` fee timestamp rateAtUTarget - rewards { - yearlySupplyTokens - asset { - address - spotPriceEth - } - amountPerSuppliedToken - amountPerBorrowedToken - } - monthlySupplyApy - monthlyBorrowApy - dailySupplyApy - dailyBorrowApy - weeklySupplyApy - weeklyBorrowApy } warnings { type diff --git a/src/utils/morpho.ts b/src/utils/morpho.ts index f4376b25..40e80643 100644 --- a/src/utils/morpho.ts +++ b/src/utils/morpho.ts @@ -1,4 +1,3 @@ -import { formatBalance } from './balance'; import { SupportedNetworks } from './networks'; import { UserTxTypes } from './types'; @@ -17,22 +16,6 @@ export const getBundlerV2 = (chain: SupportedNetworks) => { return '0x4095F064B8d3c3548A3bebfd0Bbfd04750E30077'; }; -export const getRewardPer1000USD = (yearlySupplyTokens: string, marketSupplyAssetUSD: number) => { - return ((formatBalance(yearlySupplyTokens, 18) / marketSupplyAssetUSD) * 1000).toString(); -}; - -export const getUserRewardPerYear = ( - yearlySupplyTokens: string | null, - marketSupplyAssetUSD: number, - userSuppliedUSD: number, -) => { - if (!yearlySupplyTokens) return '0'; - return ( - (formatBalance(yearlySupplyTokens, 18) * Number(userSuppliedUSD)) / - marketSupplyAssetUSD - ).toFixed(2); -}; - export const getIRMTitle = (address: string) => { switch (address.toLowerCase()) { case '0x870ac11d48b15db9a138cf899d20f13f79ba00bc': diff --git a/src/utils/types.ts b/src/utils/types.ts index 0372d4f1..cab8bbea 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -291,21 +291,6 @@ export type Market = { fee: number; timestamp: number; rateAtUTarget: number; - rewards: { - yearlySupplyTokens: string; - asset: { - address: string; - spotPriceEth: string | null; - }; - amountPerSuppliedToken: string; - amountPerBorrowedToken: string; - }[]; - monthlySupplyApy: number; - monthlyBorrowApy: number; - dailySupplyApy: number; - dailyBorrowApy: number; - weeklySupplyApy: number; - weeklyBorrowApy: number; }; warnings: MarketWarning[]; badDebt?: { From fe3b7e4f2b282c680fde1b54bd617f537d1dce22 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sat, 19 Apr 2025 16:59:49 +0800 Subject: [PATCH 03/11] misc: rename query file --- src/contexts/MarketsContext.tsx | 2 +- src/graphql/{queries.ts => morpho-api-queries.ts} | 2 ++ src/hooks/useMarket.ts | 2 +- src/hooks/useMarketBorrows.ts | 2 +- src/hooks/useMarketLiquidations.ts | 2 +- src/hooks/useMarketSupplies.ts | 2 +- src/hooks/useUserPosition.ts | 2 +- src/hooks/useUserPositions.ts | 2 +- src/hooks/useUserRebalancerInfo.ts | 2 +- src/hooks/useUserTransactions.ts | 2 +- 10 files changed, 11 insertions(+), 9 deletions(-) rename src/graphql/{queries.ts => morpho-api-queries.ts} (99%) diff --git a/src/contexts/MarketsContext.tsx b/src/contexts/MarketsContext.tsx index d8b60c1b..ca90ee33 100644 --- a/src/contexts/MarketsContext.tsx +++ b/src/contexts/MarketsContext.tsx @@ -9,7 +9,7 @@ import { useState, useMemo, } from 'react'; -import { marketsQuery } from '@/graphql/queries'; +import { marketsQuery } from '@/graphql/morpho-api-queries'; import useLiquidations from '@/hooks/useLiquidations'; import { isSupportedChain } from '@/utils/networks'; import { Market } from '@/utils/types'; diff --git a/src/graphql/queries.ts b/src/graphql/morpho-api-queries.ts similarity index 99% rename from src/graphql/queries.ts rename to src/graphql/morpho-api-queries.ts index 787bb16d..f797e837 100644 --- a/src/graphql/queries.ts +++ b/src/graphql/morpho-api-queries.ts @@ -1,3 +1,5 @@ +// Queries for Morpho Officail API + export const feedFieldsFragment = ` fragment FeedFields on OracleFeed { address diff --git a/src/hooks/useMarket.ts b/src/hooks/useMarket.ts index 1c4d6a8e..65516c83 100644 --- a/src/hooks/useMarket.ts +++ b/src/hooks/useMarket.ts @@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query'; import { SupportedNetworks } from '@/utils/networks'; import { URLS } from '@/utils/urls'; import { getMarketWarningsWithDetail } from '@/utils/warnings'; -import { marketDetailQuery, marketHistoricalDataQuery } from '../graphql/queries'; +import { marketDetailQuery, marketHistoricalDataQuery } from '../graphql/morpho-api-queries'; import { MarketDetail, TimeseriesOptions, Market } from '../utils/types'; type GraphQLResponse = { diff --git a/src/hooks/useMarketBorrows.ts b/src/hooks/useMarketBorrows.ts index 98e63b00..f45279f0 100644 --- a/src/hooks/useMarketBorrows.ts +++ b/src/hooks/useMarketBorrows.ts @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from 'react'; -import { marketBorrowsQuery } from '@/graphql/queries'; +import { marketBorrowsQuery } from '@/graphql/morpho-api-queries'; import { URLS } from '@/utils/urls'; export type MarketBorrowTransaction = { diff --git a/src/hooks/useMarketLiquidations.ts b/src/hooks/useMarketLiquidations.ts index 20216174..2fd6cfe6 100644 --- a/src/hooks/useMarketLiquidations.ts +++ b/src/hooks/useMarketLiquidations.ts @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from 'react'; -import { marketLiquidationsQuery } from '@/graphql/queries'; +import { marketLiquidationsQuery } from '@/graphql/morpho-api-queries'; import { URLS } from '@/utils/urls'; export type MarketLiquidationTransaction = { diff --git a/src/hooks/useMarketSupplies.ts b/src/hooks/useMarketSupplies.ts index 27997e81..6fb21cd3 100644 --- a/src/hooks/useMarketSupplies.ts +++ b/src/hooks/useMarketSupplies.ts @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from 'react'; -import { marketSuppliesQuery } from '@/graphql/queries'; +import { marketSuppliesQuery } from '@/graphql/morpho-api-queries'; import { URLS } from '@/utils/urls'; export type MarketSupplyTransaction = { diff --git a/src/hooks/useUserPosition.ts b/src/hooks/useUserPosition.ts index 0356a3ff..c18f63bb 100644 --- a/src/hooks/useUserPosition.ts +++ b/src/hooks/useUserPosition.ts @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback } from 'react'; import { Address } from 'viem'; -import { userPositionForMarketQuery } from '@/graphql/queries'; +import { userPositionForMarketQuery } from '@/graphql/morpho-api-queries'; import { SupportedNetworks } from '@/utils/networks'; import { fetchPositionSnapshot } from '@/utils/positions'; import { MarketPosition } from '@/utils/types'; diff --git a/src/hooks/useUserPositions.ts b/src/hooks/useUserPositions.ts index 18ea2174..64b746b2 100644 --- a/src/hooks/useUserPositions.ts +++ b/src/hooks/useUserPositions.ts @@ -3,7 +3,7 @@ import { useCallback } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { Address } from 'viem'; -import { userPositionsQuery } from '@/graphql/queries'; +import { userPositionsQuery } from '@/graphql/morpho-api-queries'; import { SupportedNetworks } from '@/utils/networks'; import { fetchPositionSnapshot, type PositionSnapshot } from '@/utils/positions'; import { MarketPosition, Market } from '@/utils/types'; diff --git a/src/hooks/useUserRebalancerInfo.ts b/src/hooks/useUserRebalancerInfo.ts index c473c21b..2de737ae 100644 --- a/src/hooks/useUserRebalancerInfo.ts +++ b/src/hooks/useUserRebalancerInfo.ts @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from 'react'; -import { userRebalancerInfoQuery } from '@/graphql/queries'; +import { userRebalancerInfoQuery } from '@/graphql/morpho-api-queries'; import { UserRebalancerInfo } from '@/utils/types'; import { URLS } from '@/utils/urls'; diff --git a/src/hooks/useUserTransactions.ts b/src/hooks/useUserTransactions.ts index 58e7215e..88ee588a 100644 --- a/src/hooks/useUserTransactions.ts +++ b/src/hooks/useUserTransactions.ts @@ -1,5 +1,5 @@ import { useState, useCallback } from 'react'; -import { userTransactionsQuery } from '@/graphql/queries'; +import { userTransactionsQuery } from '@/graphql/morpho-api-queries'; import { SupportedNetworks } from '@/utils/networks'; import { UserTransaction } from '@/utils/types'; import { URLS } from '@/utils/urls'; From 677128957f1afc0593f07bef648807b5a210e39a Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sat, 19 Apr 2025 17:00:53 +0800 Subject: [PATCH 04/11] chore: lint --- src/graphql/{statsQueries.ts => monarch-stats-queries.ts} | 0 src/services/statsService.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/graphql/{statsQueries.ts => monarch-stats-queries.ts} (100%) diff --git a/src/graphql/statsQueries.ts b/src/graphql/monarch-stats-queries.ts similarity index 100% rename from src/graphql/statsQueries.ts rename to src/graphql/monarch-stats-queries.ts diff --git a/src/services/statsService.ts b/src/services/statsService.ts index 7689a380..ef02bc7a 100644 --- a/src/services/statsService.ts +++ b/src/services/statsService.ts @@ -1,5 +1,5 @@ import { request, gql } from 'graphql-request'; -import { transactionsByTimeRangeQuery, userGrowthQuery } from '@/graphql/statsQueries'; +import { transactionsByTimeRangeQuery, userGrowthQuery } from '@/graphql/monarch-stats-queries'; import { SupportedNetworks } from '@/utils/networks'; import { processTransactionData } from '@/utils/statsDataProcessing'; import { From 1e21ccb17162240a458364da3cc2e29d40dde0f8 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sat, 19 Apr 2025 17:34:49 +0800 Subject: [PATCH 05/11] feat: basic market stats --- app/market/[chainId]/[marketid]/content.tsx | 15 +- src/graphql/morpho-api-queries.ts | 1 + src/graphql/morpho-subgraph-queries.ts | 97 +++++++++ src/hooks/useSubgraphMarket.ts | 222 ++++++++++++++++++++ src/utils/subgraph-types.ts | 85 ++++++++ 5 files changed, 415 insertions(+), 5 deletions(-) create mode 100644 src/graphql/morpho-subgraph-queries.ts create mode 100644 src/hooks/useSubgraphMarket.ts create mode 100644 src/utils/subgraph-types.ts diff --git a/app/market/[chainId]/[marketid]/content.tsx b/app/market/[chainId]/[marketid]/content.tsx index d04c38f1..298fd21d 100644 --- a/app/market/[chainId]/[marketid]/content.tsx +++ b/app/market/[chainId]/[marketid]/content.tsx @@ -30,6 +30,7 @@ import { PositionStats } from './components/PositionStats'; import { SuppliesTable } from './components/SuppliesTable'; import RateChart from './RateChart'; import VolumeChart from './VolumeChart'; +import { useSubgraphMarket } from '@/hooks/useSubgraphMarket'; const NOW = Math.floor(Date.now() / 1000); const WEEK_IN_SECONDS = 7 * 24 * 60 * 60; @@ -62,11 +63,15 @@ function MarketContent() { }); // 4. Data fetching hooks - const { - data: market, - isLoading: isMarketLoading, - error: marketError, - } = useMarket(marketid as string, network); + // const { + // data: market, + // isLoading: isMarketLoading, + // error: marketError, + // } = useMarket(marketid as string, network); + + const {data: market, isLoading: isMarketLoading, error: marketError} = useSubgraphMarket(marketid as string, network); + + console.log('market', market); const { data: historicalData, diff --git a/src/graphql/morpho-api-queries.ts b/src/graphql/morpho-api-queries.ts index f797e837..b926e702 100644 --- a/src/graphql/morpho-api-queries.ts +++ b/src/graphql/morpho-api-queries.ts @@ -1,4 +1,5 @@ // Queries for Morpho Officail API +// Reference: https://blue-api.morpho.org/graphql export const feedFieldsFragment = ` fragment FeedFields on OracleFeed { diff --git a/src/graphql/morpho-subgraph-queries.ts b/src/graphql/morpho-subgraph-queries.ts new file mode 100644 index 00000000..d4aa76a6 --- /dev/null +++ b/src/graphql/morpho-subgraph-queries.ts @@ -0,0 +1,97 @@ +export const tokenFragment = ` + fragment TokenFields on Token { + id # Maps to address + name + symbol + decimals + lastPriceUSD + } +`; + +export const oracleFragment = ` + fragment OracleFields on Oracle { + id + oracleAddress + oracleSource + isActive + isUSD + } +`; + +export const marketFragment = ` + fragment SubgraphMarketFields on Market { + id # Maps to uniqueKey + lltv + irm # Maps to irmAddress + inputToken { # Maps to collateralAsset + # Fetch full token details now + ...TokenFields + } + inputTokenPriceUSD # Maps to collateralPrice + borrowedToken { # Maps to loanAsset + # Fetch full token details now + ...TokenFields + } + totalDepositBalanceUSD # Maps to state.supplyAssetsUsd + totalBorrowBalanceUSD # Maps to state.borrowAssetsUsd + totalSupplyShares # Maps to state.supplyShares + totalBorrowShares # Maps to state.borrowShares + totalSupply # Add back + totalBorrow # Add back + fee # Maps to state.fee + + # --- Restore previously removed fields --- + name + isActive + canBorrowFrom + canUseAsCollateral + maximumLTV + liquidationThreshold + liquidationPenalty + createdTimestamp + createdBlockNumber + inputTokenBalance # Add back + variableBorrowedTokenBalance # Add back + totalValueLockedUSD # Add back + lastUpdate # Add back + reserves # Add back + reserveFactor # Add back + oracle { # Add back + ...OracleFields + } + rates { # Add back + id + rate # APY + side + type + } + protocol { # Add back + id # Morpho Blue ID? + network # Chain ID? + protocol # Protocol Name (e.g., Morpho Blue) + } + # --- End of restored fields --- + } + ${tokenFragment} # Restore interpolation + ${oracleFragment} # Restore interpolation +`; + +export const marketsQuery = ` + query getSubgraphMarkets($first: Int, $where: Market_filter) { + markets(first: $first, where: $where, orderBy: totalValueLockedUSD, orderDirection: desc) { + ...SubgraphMarketFields + } + } + ${marketFragment} +`; + +// Add other queries as needed, e.g., for user positions based on subgraph schema + +export const marketQuery = ` + query getSubgraphMarket($id: Bytes!) { + market(id: $id) { + ...SubgraphMarketFields + } + } + ${marketFragment} +`; \ No newline at end of file diff --git a/src/hooks/useSubgraphMarket.ts b/src/hooks/useSubgraphMarket.ts new file mode 100644 index 00000000..741425bd --- /dev/null +++ b/src/hooks/useSubgraphMarket.ts @@ -0,0 +1,222 @@ +import { useQuery } from '@tanstack/react-query'; +import { SupportedNetworks } from '@/utils/networks'; +import { URLS } from '@/utils/urls'; +import { getMarketWarningsWithDetail } from '@/utils/warnings'; +import { MarketDetail, TimeseriesOptions, WarningWithDetail, WarningCategory, MorphoChainlinkOracleData } from '../utils/types'; +import { + marketsQuery as subgraphMarketsQuery, // Keep old name for reference if needed + marketQuery as subgraphMarketQuery // Import the new single market query +} from '../graphql/morpho-subgraph-queries'; +import { + SubgraphMarket, + SubgraphMarketsQueryResponse, // Keep for reference if needed + SubgraphMarketQueryResponse, // Use the single market response type + SubgraphToken, + SubgraphInterestRate, + SubgraphOracle +} from '../utils/subgraph-types'; +import { Address, formatUnits } from 'viem'; + +const apiKey = process.env.NEXT_PUBLIC_THEGRAPH_API_KEY + +// Use the existing API URL for now, replace if Subgraph has a dedicated URL +const SUBGRAPH_API_URL = `https://gateway.thegraph.com/api/${apiKey}/subgraphs/id/71ZTy1veF9twER9CLMnPWeLQ7GZcwKsjmygejrgKirqs` + +const subgraphGraphqlFetcher = async ( + query: string, + variables: Record, +): Promise => { + const response = await fetch(SUBGRAPH_API_URL, { // Use subgraph URL + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query, variables }), + }); + + if (!response.ok) { + throw new Error('Network response was not ok'); + } + + const result = (await response.json()) as T; + + // Basic error handling, specific to GraphQL structure + if ('errors' in result && Array.isArray((result as any).errors) && (result as any).errors.length > 0) { + throw new Error((result as any).errors[0].message); + } + + return result; +}; + +// Helper to safely parse BigDecimal/BigInt strings +const safeParseFloat = (value: string | null | undefined): number => { + if (value === null || value === undefined) return 0; + try { + return parseFloat(value); + } catch { + return 0; + } +}; + +const safeParseInt = (value: string | null | undefined): number => { + if (value === null || value === undefined) return 0; + try { + return parseInt(value, 10); + } catch { + return 0; + } +} + +// Transformation function +const transformSubgraphMarketToMarketDetail = (subgraphMarket: Partial): MarketDetail => { + // Use Partial as input type since not all fields are guaranteed + + // --- Handle fields from the simplified response --- + const marketId = subgraphMarket.id ?? ''; // This is the derived market ID/uniqueKey + 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'; + const fee = subgraphMarket.fee ?? '0'; + + // Map token info - provide defaults if tokens are missing + const mapToken = (token: Partial | undefined) => ({ + id: token?.id ?? '0x', // Default to zero address + address: token?.id ?? '0x', // Default to zero address + symbol: token?.symbol ?? 'Unknown', + name: token?.name ?? 'Unknown Token', + decimals: token?.decimals ?? 18, // Default to 18 decimals + }); + + const loanAsset = mapToken(subgraphMarket.borrowedToken); + const collateralAsset = mapToken(subgraphMarket.inputToken); + + // --- Provide defaults for missing fields --- + const defaultOracleData: MorphoChainlinkOracleData = { + baseFeedOne: null, + baseFeedTwo: null, + quoteFeedOne: null, + quoteFeedTwo: null, + }; + + // Placeholder for chain ID mapping + // TODO: Implement mapping from subgraphMarket.protocol?.network string to chain ID number + const chainId = 1; // Default to Mainnet for now + + // Default state values for fields not present in the simplified query + const borrowAssets = subgraphMarket.totalBorrow ?? '0'; + const supplyAssets = subgraphMarket.totalSupply ?? '0'; + const collateralAssets = subgraphMarket.inputTokenBalance ?? '0'; + const collateralAssetsUsd = safeParseFloat(subgraphMarket.totalValueLockedUSD); // Use totalValueLockedUSD if available, else 0 + const timestamp = safeParseInt(subgraphMarket.lastUpdate); // Use lastUpdate if available, else 0 + + // Calculate utilization safely with defaults + const totalSupplyNum = safeParseFloat(supplyAssets); + const totalBorrowNum = safeParseFloat(borrowAssets); + const utilization = totalSupplyNum > 0 ? (totalBorrowNum / totalSupplyNum) * 100 : 0; + + // Default APYs (since rates are not fetched) + const supplyApy = 0; + const borrowApy = 0; + + // Liquidity calculation with defaults + const liquidityAssets = (BigInt(supplyAssets) - BigInt(borrowAssets)).toString(); + const liquidityAssetsUsd = safeParseFloat(totalDepositBalanceUSD) - safeParseFloat(totalBorrowBalanceUSD); + + // Default warnings (isActive is not fetched in simplified query) + const warningsWithDetail: WarningWithDetail[] = []; + + const marketDetail: MarketDetail = { + // Mapped from simplified response + id: marketId, + uniqueKey: marketId, + lltv: lltv, + irmAddress: irmAddress as Address, // Cast to Address + collateralPrice: inputTokenPriceUSD, + loanAsset: loanAsset, + collateralAsset: collateralAsset, + + // State mapped from simplified response + defaults + state: { + borrowAssets: borrowAssets, + supplyAssets: supplyAssets, + borrowAssetsUsd: totalBorrowBalanceUSD, + supplyAssetsUsd: totalDepositBalanceUSD, + borrowShares: totalBorrowShares, + supplyShares: totalSupplyShares, + liquidityAssets: liquidityAssets, + liquidityAssetsUsd: liquidityAssetsUsd, + collateralAssets: collateralAssets, + collateralAssetsUsd: collateralAssetsUsd, + utilization: utilization, + supplyApy: supplyApy, + borrowApy: borrowApy, + // Fee: Assuming conversion from basis points (10000 = 100%) + fee: safeParseFloat(fee) / 100, // Divide by 100 if fee is basis points * 100 + timestamp: timestamp, + rateAtUTarget: 0, // Default + }, + + // Defaulted fields + oracleAddress: subgraphMarket.oracle?.oracleAddress ?? '0x', // Default to zero address + morphoBlue: { + id: subgraphMarket.protocol?.id ?? '0x', // Default + address: subgraphMarket.protocol?.id ?? '0x', // Default + chain: { + id: chainId, + }, + }, + warnings: [], // Default + warningsWithDetail: warningsWithDetail, // Default + oracle: { + data: defaultOracleData, // Default + }, + isProtectedByLiquidationBots: false, // Default + badDebt: undefined, // Default + realizedBadDebt: undefined, // Default + historicalState: { // Default empty historical + supplyApy: [], + borrowApy: [], + supplyAssetsUsd: [], + borrowAssetsUsd: [], + rateAtUTarget: [], + utilization: [], + supplyAssets: [], + borrowAssets: [], + liquidityAssetsUsd: [], + liquidityAssets: [], + }, + }; + + return marketDetail; +}; + +// Hook to fetch a specific market using its ID (uniqueKey) +export const useSubgraphMarket = (uniqueKey: string | undefined, network: SupportedNetworks) => { + return useQuery({ // Allow null if market not found + queryKey: ['subgraphMarket', uniqueKey, network], + queryFn: async () => { + if (!uniqueKey) return null; // Return null if uniqueKey is not provided + + // Use the new query for a single market + const response = await subgraphGraphqlFetcher(subgraphMarketQuery, { + id: uniqueKey.toLowerCase() // Pass the uniqueKey as the id variable + }); + + // Get the market data directly from the response + const marketData = response.data.market; + + if (!marketData) { + console.warn(`Market with key ${uniqueKey} not found in Subgraph response.`); + return null; // Market not found or query returned null + } + + return transformSubgraphMarketToMarketDetail(marketData); + }, + enabled: !!uniqueKey && !!network, // Only run query if uniqueKey and network are available + staleTime: 1000 * 60 * 5, // Cache data for 5 minutes + }); +}; + +// TODO: Add useSubgraphMarketHistoricalData if needed, requires adding historical queries to the subgraph file. \ No newline at end of file diff --git a/src/utils/subgraph-types.ts b/src/utils/subgraph-types.ts new file mode 100644 index 00000000..74f24aa9 --- /dev/null +++ b/src/utils/subgraph-types.ts @@ -0,0 +1,85 @@ +import { Address } from 'viem'; + +// Corresponds to tokenFragment +export type SubgraphToken = { + id: Address; // address + name: string; + symbol: string; + decimals: number; + lastPriceUSD: string | null; // BigDecimal represented as string +}; + +// Corresponds to oracleFragment +export type SubgraphOracle = { + id: string; + oracleAddress: Address; + oracleSource: string | null; + isActive: boolean; + isUSD: boolean; +}; + +// Corresponds to InterestRate type within marketFragment +export type SubgraphInterestRate = { + id: string; + rate: string; // BigDecimal represented as string (APY percentage) + side: 'LENDER' | 'BORROWER'; + type: 'STABLE' | 'VARIABLE' | 'FIXED'; +}; + +// Corresponds to protocol details within marketFragment +export type SubgraphProtocolInfo = { + id: string; + network: string; // e.g., "MAINNET", "BASE" + protocol: string; // e.g., "Morpho Blue" +}; + +// Corresponds to the main marketFragment (SubgraphMarketFields) +export type SubgraphMarket = { + id: Address; // uniqueKey (market address) + name: string; + 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) + 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) + 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?) + oracle: SubgraphOracle; + rates: SubgraphInterestRate[]; + protocol: SubgraphProtocolInfo; +}; + +// Type for the GraphQL response structure using marketsQuery +export type SubgraphMarketsQueryResponse = { + data: { + markets: SubgraphMarket[]; + }; + errors?: { message: string }[]; +}; + +// 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 + }; + errors?: { message: string }[]; +}; \ No newline at end of file From 2e52f4c5886c2468ff1e81c9737b099d061693e7 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sat, 19 Apr 2025 23:46:47 +0800 Subject: [PATCH 06/11] misc: urls --- src/hooks/useSubgraphMarket.ts | 36 ++++++++++++++++++++-------------- src/utils/subgraph-urls.ts | 29 +++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 15 deletions(-) create mode 100644 src/utils/subgraph-urls.ts diff --git a/src/hooks/useSubgraphMarket.ts b/src/hooks/useSubgraphMarket.ts index 741425bd..b47ed8d2 100644 --- a/src/hooks/useSubgraphMarket.ts +++ b/src/hooks/useSubgraphMarket.ts @@ -1,7 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import { SupportedNetworks } from '@/utils/networks'; -import { URLS } from '@/utils/urls'; -import { getMarketWarningsWithDetail } from '@/utils/warnings'; +import { getSubgraphUrl } from '@/utils/subgraph-urls'; // Import the new URL getter import { MarketDetail, TimeseriesOptions, WarningWithDetail, WarningCategory, MorphoChainlinkOracleData } from '../utils/types'; import { marketsQuery as subgraphMarketsQuery, // Keep old name for reference if needed @@ -17,16 +16,12 @@ import { } from '../utils/subgraph-types'; import { Address, formatUnits } from 'viem'; -const apiKey = process.env.NEXT_PUBLIC_THEGRAPH_API_KEY - -// Use the existing API URL for now, replace if Subgraph has a dedicated URL -const SUBGRAPH_API_URL = `https://gateway.thegraph.com/api/${apiKey}/subgraphs/id/71ZTy1veF9twER9CLMnPWeLQ7GZcwKsjmygejrgKirqs` - const subgraphGraphqlFetcher = async ( + apiUrl: string, // Accept URL as a parameter query: string, variables: Record, ): Promise => { - const response = await fetch(SUBGRAPH_API_URL, { // Use subgraph URL + const response = await fetch(apiUrl, { // Use the passed URL method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query, variables }), @@ -66,7 +61,7 @@ const safeParseInt = (value: string | null | undefined): number => { } // Transformation function -const transformSubgraphMarketToMarketDetail = (subgraphMarket: Partial): MarketDetail => { +const transformSubgraphMarketToMarketDetail = (subgraphMarket: Partial, network: SupportedNetworks): MarketDetail => { // Use Partial as input type since not all fields are guaranteed // --- Handle fields from the simplified response --- @@ -102,7 +97,7 @@ const transformSubgraphMarketToMarketDetail = (subgraphMarket: Partial({ // Allow null if market not found queryKey: ['subgraphMarket', uniqueKey, network], queryFn: async () => { - if (!uniqueKey) return null; // Return null if uniqueKey is not provided + if (!uniqueKey || !network) return null; // Also check if network is provided + + 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.`); // Or return null + } // Use the new query for a single market - const response = await subgraphGraphqlFetcher(subgraphMarketQuery, { - id: uniqueKey.toLowerCase() // Pass the uniqueKey as the id variable - }); + const response = await subgraphGraphqlFetcher( + subgraphApiUrl, // Pass the dynamically selected URL + subgraphMarketQuery, + { + id: uniqueKey.toLowerCase() // Pass the uniqueKey as the id variable + } + ); // Get the market data directly from the response const marketData = response.data.market; @@ -212,7 +218,7 @@ export const useSubgraphMarket = (uniqueKey: string | undefined, network: Suppor return null; // Market not found or query returned null } - return transformSubgraphMarketToMarketDetail(marketData); + return transformSubgraphMarketToMarketDetail(marketData, network); }, enabled: !!uniqueKey && !!network, // Only run query if uniqueKey and network are available staleTime: 1000 * 60 * 5, // Cache data for 5 minutes diff --git a/src/utils/subgraph-urls.ts b/src/utils/subgraph-urls.ts new file mode 100644 index 00000000..15a1da58 --- /dev/null +++ b/src/utils/subgraph-urls.ts @@ -0,0 +1,29 @@ +import { SupportedNetworks } from './networks'; + +const apiKey = process.env.NEXT_PUBLIC_THEGRAPH_API_KEY; + +// Ensure the API key is available +if (!apiKey) { + console.error('NEXT_PUBLIC_THEGRAPH_API_KEY is not set in environment variables.'); + // Potentially throw an error or handle this case as needed +} + +const baseSubgraphUrl = apiKey + ? `https://gateway.thegraph.com/api/${apiKey}/subgraphs/id/71ZTy1veF9twER9CLMnPWeLQ7GZcwKsjmygejrgKirqs` + : undefined; + +// TODO: Replace 'YOUR_MAINNET_SUBGRAPH_ID' with the actual Mainnet Subgraph ID +const mainnetSubgraphUrl = apiKey + ? `https://gateway.thegraph.com/api/${apiKey}/subgraphs/id/8Lz789DP5VKLXumTMTgygjU2xtuzx8AhbaacgN5PYCAs` + : undefined; + +// Map network IDs (from SupportedNetworks) to Subgraph URLs +export const SUBGRAPH_URLS: { [key in SupportedNetworks]?: string } = { + [SupportedNetworks.Base]: baseSubgraphUrl, + [SupportedNetworks.Mainnet]: mainnetSubgraphUrl, + // Add other supported networks and their Subgraph URLs here +}; + +export const getSubgraphUrl = (network: SupportedNetworks): string | undefined => { + return SUBGRAPH_URLS[network]; +}; \ No newline at end of file From e738ce24ba1d2faed6d3063bab0a44a6cafe8aa9 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sun, 20 Apr 2025 17:36:37 +0800 Subject: [PATCH 07/11] chore: remove unused historicalState --- app/market/[chainId]/[marketid]/content.tsx | 9 +- src/graphql/morpho-subgraph-queries.ts | 58 ++++----- src/hooks/useMarket.ts | 16 +-- src/hooks/useSubgraphMarket.ts | 132 +++++++------------- src/hooks/useUserPosition.ts | 5 +- src/utils/types.ts | 2 +- 6 files changed, 80 insertions(+), 142 deletions(-) diff --git a/app/market/[chainId]/[marketid]/content.tsx b/app/market/[chainId]/[marketid]/content.tsx index 298fd21d..58457e52 100644 --- a/app/market/[chainId]/[marketid]/content.tsx +++ b/app/market/[chainId]/[marketid]/content.tsx @@ -62,16 +62,9 @@ function MarketContent() { interval: 'HOUR', }); - // 4. Data fetching hooks - // const { - // data: market, - // isLoading: isMarketLoading, - // error: marketError, - // } = useMarket(marketid as string, network); - const {data: market, isLoading: isMarketLoading, error: marketError} = useSubgraphMarket(marketid as string, network); - console.log('market', market); + console.log('market', market?.oracle); const { data: historicalData, diff --git a/src/graphql/morpho-subgraph-queries.ts b/src/graphql/morpho-subgraph-queries.ts index d4aa76a6..d14f2c44 100644 --- a/src/graphql/morpho-subgraph-queries.ts +++ b/src/graphql/morpho-subgraph-queries.ts @@ -1,6 +1,6 @@ export const tokenFragment = ` fragment TokenFields on Token { - id # Maps to address + id # address name symbol decimals @@ -20,27 +20,24 @@ export const oracleFragment = ` export const marketFragment = ` fragment SubgraphMarketFields on Market { - id # Maps to uniqueKey + id # uniqueKey lltv - irm # Maps to irmAddress - inputToken { # Maps to collateralAsset - # Fetch full token details now + irm # irmAddress + inputToken { # collateralAsset ...TokenFields } - inputTokenPriceUSD # Maps to collateralPrice - borrowedToken { # Maps to loanAsset - # Fetch full token details now + inputTokenPriceUSD # collateralPrice + borrowedToken { # loanAsset ...TokenFields } - totalDepositBalanceUSD # Maps to state.supplyAssetsUsd - totalBorrowBalanceUSD # Maps to state.borrowAssetsUsd - totalSupplyShares # Maps to state.supplyShares - totalBorrowShares # Maps to state.borrowShares - totalSupply # Add back - totalBorrow # Add back - fee # Maps to state.fee + totalDepositBalanceUSD # supplyAssetsUsd + totalBorrowBalanceUSD # borrowAssetsUsd + totalSupplyShares # supplyShares + totalBorrowShares # borrowShares + totalSupply # supplyAssets + totalBorrow # borrowAssets + fee # fee - # --- Restore previously removed fields --- name isActive canBorrowFrom @@ -50,30 +47,29 @@ export const marketFragment = ` liquidationPenalty createdTimestamp createdBlockNumber - inputTokenBalance # Add back - variableBorrowedTokenBalance # Add back - totalValueLockedUSD # Add back - lastUpdate # Add back - reserves # Add back - reserveFactor # Add back - oracle { # Add back + inputTokenBalance # collateralAssets + variableBorrowedTokenBalance + totalValueLockedUSD # collateralAssetsUsd? + lastUpdate # timestamp + reserves + reserveFactor + oracle { ...OracleFields } - rates { # Add back + rates { id rate # APY side type } - protocol { # Add back - id # Morpho Blue ID? - network # Chain ID? - protocol # Protocol Name (e.g., Morpho Blue) + protocol { + id # Morpho Blue Address? + network # Chain Name + protocol # Protocol Name } - # --- End of restored fields --- } - ${tokenFragment} # Restore interpolation - ${oracleFragment} # Restore interpolation + ${tokenFragment} + ${oracleFragment} `; export const marketsQuery = ` diff --git a/src/hooks/useMarket.ts b/src/hooks/useMarket.ts index 65516c83..87f6873f 100644 --- a/src/hooks/useMarket.ts +++ b/src/hooks/useMarket.ts @@ -35,30 +35,18 @@ const graphqlFetcher = async ( return result; }; -const processMarketData = (market: Market): MarketDetail => { +const processMarketData = (market: Market): Market => { const warningsWithDetail = getMarketWarningsWithDetail(market); return { ...market, warningsWithDetail, isProtectedByLiquidationBots: false, // NOT needed for now, might implement later - historicalState: { - supplyApy: [], - borrowApy: [], - supplyAssetsUsd: [], - borrowAssetsUsd: [], - rateAtUTarget: [], - utilization: [], - supplyAssets: [], - borrowAssets: [], - liquidityAssetsUsd: [], - liquidityAssets: [], - }, }; }; export const useMarket = (uniqueKey: string, network: SupportedNetworks) => { - return useQuery({ + return useQuery({ queryKey: ['market', uniqueKey, network], queryFn: async () => { const response = await graphqlFetcher(marketDetailQuery, { uniqueKey, chainId: network }); diff --git a/src/hooks/useSubgraphMarket.ts b/src/hooks/useSubgraphMarket.ts index b47ed8d2..06fc3312 100644 --- a/src/hooks/useSubgraphMarket.ts +++ b/src/hooks/useSubgraphMarket.ts @@ -1,27 +1,23 @@ import { useQuery } from '@tanstack/react-query'; import { SupportedNetworks } from '@/utils/networks'; -import { getSubgraphUrl } from '@/utils/subgraph-urls'; // Import the new URL getter -import { MarketDetail, TimeseriesOptions, WarningWithDetail, WarningCategory, MorphoChainlinkOracleData } from '../utils/types'; +import { getSubgraphUrl } from '@/utils/subgraph-urls'; +import { WarningWithDetail, MorphoChainlinkOracleData, Market } from '../utils/types'; import { - marketsQuery as subgraphMarketsQuery, // Keep old name for reference if needed - marketQuery as subgraphMarketQuery // Import the new single market query + marketQuery as subgraphMarketQuery } from '../graphql/morpho-subgraph-queries'; import { SubgraphMarket, - SubgraphMarketsQueryResponse, // Keep for reference if needed - SubgraphMarketQueryResponse, // Use the single market response type + SubgraphMarketQueryResponse, SubgraphToken, - SubgraphInterestRate, - SubgraphOracle } from '../utils/subgraph-types'; -import { Address, formatUnits } from 'viem'; +import { Address } from 'viem'; const subgraphGraphqlFetcher = async ( - apiUrl: string, // Accept URL as a parameter + apiUrl: string, query: string, variables: Record, ): Promise => { - const response = await fetch(apiUrl, { // Use the passed URL + const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query, variables }), @@ -33,7 +29,6 @@ const subgraphGraphqlFetcher = async ( const result = (await response.json()) as T; - // Basic error handling, specific to GraphQL structure if ('errors' in result && Array.isArray((result as any).errors) && (result as any).errors.length > 0) { throw new Error((result as any).errors[0].message); } @@ -60,12 +55,9 @@ const safeParseInt = (value: string | null | undefined): number => { } } -// Transformation function -const transformSubgraphMarketToMarketDetail = (subgraphMarket: Partial, network: SupportedNetworks): MarketDetail => { - // Use Partial as input type since not all fields are guaranteed +const transformSubgraphMarketToMarketDetail = (subgraphMarket: Partial, network: SupportedNetworks): Market => { - // --- Handle fields from the simplified response --- - const marketId = subgraphMarket.id ?? ''; // This is the derived market ID/uniqueKey + const marketId = subgraphMarket.id ?? ''; const lltv = subgraphMarket.lltv ?? '0'; const irmAddress = subgraphMarket.irm ?? '0x'; const inputTokenPriceUSD = subgraphMarket.inputTokenPriceUSD ?? '0'; @@ -75,19 +67,19 @@ const transformSubgraphMarketToMarketDetail = (subgraphMarket: Partial | undefined) => ({ - id: token?.id ?? '0x', // Default to zero address - address: token?.id ?? '0x', // Default to zero address + id: token?.id ?? '0x', + address: token?.id ?? '0x', symbol: token?.symbol ?? 'Unknown', name: token?.name ?? 'Unknown Token', - decimals: token?.decimals ?? 18, // Default to 18 decimals + decimals: token?.decimals ?? 18, }); const loanAsset = mapToken(subgraphMarket.borrowedToken); const collateralAsset = mapToken(subgraphMarket.inputToken); - // --- Provide defaults for missing fields --- const defaultOracleData: MorphoChainlinkOracleData = { baseFeedOne: null, baseFeedTwo: null, @@ -95,93 +87,69 @@ const transformSubgraphMarketToMarketDetail = (subgraphMarket: Partial 0 ? (totalBorrowNum / totalSupplyNum) * 100 : 0; - // Default APYs (since rates are not fetched) - const supplyApy = 0; - const borrowApy = 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); - // Liquidity calculation with defaults const liquidityAssets = (BigInt(supplyAssets) - BigInt(borrowAssets)).toString(); const liquidityAssetsUsd = safeParseFloat(totalDepositBalanceUSD) - safeParseFloat(totalBorrowBalanceUSD); - // Default warnings (isActive is not fetched in simplified query) const warningsWithDetail: WarningWithDetail[] = []; - const marketDetail: MarketDetail = { - // Mapped from simplified response + const marketDetail: Market = { id: marketId, uniqueKey: marketId, lltv: lltv, - irmAddress: irmAddress as Address, // Cast to Address + irmAddress: irmAddress as Address, collateralPrice: inputTokenPriceUSD, loanAsset: loanAsset, collateralAsset: collateralAsset, - - // State mapped from simplified response + defaults state: { - borrowAssets: borrowAssets, - supplyAssets: supplyAssets, - borrowAssetsUsd: totalBorrowBalanceUSD, - supplyAssetsUsd: totalDepositBalanceUSD, - borrowShares: totalBorrowShares, - supplyShares: totalSupplyShares, - liquidityAssets: liquidityAssets, + borrowAssets: borrowAssets, + supplyAssets: supplyAssets, + borrowAssetsUsd: totalBorrowBalanceUSD, + supplyAssetsUsd: totalDepositBalanceUSD, + borrowShares: totalBorrowShares, + supplyShares: totalSupplyShares, + liquidityAssets: liquidityAssets, liquidityAssetsUsd: liquidityAssetsUsd, - collateralAssets: collateralAssets, - collateralAssetsUsd: collateralAssetsUsd, + collateralAssets: collateralAssets, + collateralAssetsUsd: collateralAssetsUsd, utilization: utilization, supplyApy: supplyApy, borrowApy: borrowApy, - // Fee: Assuming conversion from basis points (10000 = 100%) - fee: safeParseFloat(fee) / 100, // Divide by 100 if fee is basis points * 100 + fee: safeParseFloat(fee) / 100, timestamp: timestamp, - rateAtUTarget: 0, // Default + rateAtUTarget: 0, }, - - // Defaulted fields - oracleAddress: subgraphMarket.oracle?.oracleAddress ?? '0x', // Default to zero address + oracleAddress: subgraphMarket.oracle?.oracleAddress ?? '0x', morphoBlue: { - id: subgraphMarket.protocol?.id ?? '0x', // Default - address: subgraphMarket.protocol?.id ?? '0x', // Default + id: subgraphMarket.protocol?.id ?? '0x', + address: subgraphMarket.protocol?.id ?? '0x', chain: { id: chainId, }, }, - warnings: [], // Default - warningsWithDetail: warningsWithDetail, // Default + warnings: [], + warningsWithDetail: warningsWithDetail, oracle: { - data: defaultOracleData, // Default - }, - isProtectedByLiquidationBots: false, // Default - badDebt: undefined, // Default - realizedBadDebt: undefined, // Default - historicalState: { // Default empty historical - supplyApy: [], - borrowApy: [], - supplyAssetsUsd: [], - borrowAssetsUsd: [], - rateAtUTarget: [], - utilization: [], - supplyAssets: [], - borrowAssets: [], - liquidityAssetsUsd: [], - liquidityAssets: [], + data: defaultOracleData, }, + isProtectedByLiquidationBots: false, + badDebt: undefined, + realizedBadDebt: undefined, }; return marketDetail; @@ -189,40 +157,36 @@ const transformSubgraphMarketToMarketDetail = (subgraphMarket: Partial { - return useQuery({ // Allow null if market not found + return useQuery({ queryKey: ['subgraphMarket', uniqueKey, network], queryFn: async () => { - if (!uniqueKey || !network) return null; // Also check if network is provided + if (!uniqueKey || !network) return null; 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.`); // Or return null + throw new Error(`Subgraph URL for network ${network} is not defined.`); } - // Use the new query for a single market const response = await subgraphGraphqlFetcher( - subgraphApiUrl, // Pass the dynamically selected URL + subgraphApiUrl, subgraphMarketQuery, { - id: uniqueKey.toLowerCase() // Pass the uniqueKey as the id variable + id: uniqueKey.toLowerCase() } ); - // Get the market data directly from the response const marketData = response.data.market; if (!marketData) { console.warn(`Market with key ${uniqueKey} not found in Subgraph response.`); - return null; // Market not found or query returned null + return null; } return transformSubgraphMarketToMarketDetail(marketData, network); }, - enabled: !!uniqueKey && !!network, // Only run query if uniqueKey and network are available - staleTime: 1000 * 60 * 5, // Cache data for 5 minutes + enabled: !!uniqueKey && !!network, + staleTime: 1000 * 60 * 5, }); }; - -// TODO: Add useSubgraphMarketHistoricalData if needed, requires adding historical queries to the subgraph file. \ No newline at end of file diff --git a/src/hooks/useUserPosition.ts b/src/hooks/useUserPosition.ts index c18f63bb..a92de336 100644 --- a/src/hooks/useUserPosition.ts +++ b/src/hooks/useUserPosition.ts @@ -60,10 +60,7 @@ const useUserPositions = ( if (currentSnapshot) { setPosition({ market: data.data.marketPosition.market, - state: { - ...currentSnapshot, - collateral: data.data.marketPosition.state.collateral, - }, + state: currentSnapshot, }); } else { setPosition(data.data.marketPosition); diff --git a/src/utils/types.ts b/src/utils/types.ts index cab8bbea..714dc195 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -319,7 +319,7 @@ export type TimeseriesDataPoint = { export type TimeseriesOptions = { startTimestamp: number; endTimestamp: number; - interval: 'MINUTE' | 'HALF_HOUR' | 'HOUR' | 'DAY' | 'WEEK' | 'MONTH' | 'QUARTER' | 'YEAR' | 'ALL'; + interval: 'HOUR' | 'DAY' | 'WEEK' | 'MONTH'; }; type MarketRates = { From 9619c45837c310aeed73e1426b1d74853fccda85 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sun, 20 Apr 2025 22:34:52 +0800 Subject: [PATCH 08/11] chore: subgraph for historical data --- app/market/[chainId]/[marketid]/content.tsx | 16 +- src/graphql/morpho-subgraph-queries.ts | 86 ++++++-- src/hooks/useMarket.ts | 144 ++++++++---- src/hooks/useSubgraphMarketHistoricalData.ts | 220 +++++++++++++++++++ src/utils/types.ts | 2 +- 5 files changed, 402 insertions(+), 66 deletions(-) create mode 100644 src/hooks/useSubgraphMarketHistoricalData.ts diff --git a/app/market/[chainId]/[marketid]/content.tsx b/app/market/[chainId]/[marketid]/content.tsx index 58457e52..27c662c9 100644 --- a/app/market/[chainId]/[marketid]/content.tsx +++ b/app/market/[chainId]/[marketid]/content.tsx @@ -31,6 +31,7 @@ import { SuppliesTable } from './components/SuppliesTable'; import RateChart from './RateChart'; import VolumeChart from './VolumeChart'; import { useSubgraphMarket } from '@/hooks/useSubgraphMarket'; +import { useSubgraphMarketHistoricalData } from '@/hooks/useSubgraphMarketHistoricalData'; const NOW = Math.floor(Date.now() / 1000); const WEEK_IN_SECONDS = 7 * 24 * 60 * 60; @@ -66,11 +67,17 @@ function MarketContent() { console.log('market', market?.oracle); + // const { + // data: historicalData, + // isLoading: isHistoricalLoading, + // refetch: refetchHistoricalData, + // } = useMarketHistoricalData(marketid as string, network, rateTimeRange, volumeTimeRange); + const { data: historicalData, isLoading: isHistoricalLoading, refetch: refetchHistoricalData, - } = useMarketHistoricalData(marketid as string, network, rateTimeRange, volumeTimeRange); + } = useSubgraphMarketHistoricalData(marketid as string, network, rateTimeRange); // 5. Oracle price hook - safely handle undefined market const { price: oraclePrice } = useOraclePrice({ @@ -107,11 +114,10 @@ function MarketContent() { if (type === 'rate') { setRateTimeRange(newTimeRange); - void refetchHistoricalData.rates(); } else { setVolumeTimeRange(newTimeRange); - void refetchHistoricalData.volumes(); } + void refetchHistoricalData(); }, [refetchHistoricalData, setRateTimeRange, setVolumeTimeRange], ); @@ -338,7 +344,7 @@ function MarketContent() { historicalData={historicalData?.volumes} market={market} volumeTimeRange={volumeTimeRange} - isLoading={isHistoricalLoading.volumes} + isLoading={isHistoricalLoading} volumeView={volumeView} volumeTimeframe={volumeTimeframe} setVolumeTimeframe={setVolumeTimeframe} @@ -351,7 +357,7 @@ function MarketContent() { historicalData={historicalData?.rates} market={market} rateTimeRange={rateTimeRange} - isLoading={isHistoricalLoading.rates} + isLoading={isHistoricalLoading} apyTimeframe={apyTimeframe} setApyTimeframe={setApyTimeframe} setTimeRangeAndRefetch={setTimeRangeAndRefetch} diff --git a/src/graphql/morpho-subgraph-queries.ts b/src/graphql/morpho-subgraph-queries.ts index d14f2c44..b358f93c 100644 --- a/src/graphql/morpho-subgraph-queries.ts +++ b/src/graphql/morpho-subgraph-queries.ts @@ -1,6 +1,6 @@ export const tokenFragment = ` fragment TokenFields on Token { - id # address + id name symbol decimals @@ -20,23 +20,23 @@ export const oracleFragment = ` export const marketFragment = ` fragment SubgraphMarketFields on Market { - id # uniqueKey + id lltv - irm # irmAddress + irm inputToken { # collateralAsset ...TokenFields } - inputTokenPriceUSD # collateralPrice + inputTokenPriceUSD borrowedToken { # loanAsset ...TokenFields } - totalDepositBalanceUSD # supplyAssetsUsd - totalBorrowBalanceUSD # borrowAssetsUsd - totalSupplyShares # supplyShares - totalBorrowShares # borrowShares - totalSupply # supplyAssets - totalBorrow # borrowAssets - fee # fee + totalDepositBalanceUSD + totalBorrowBalanceUSD + totalSupplyShares + totalBorrowShares + totalSupply + totalBorrow + fee name isActive @@ -47,10 +47,10 @@ export const marketFragment = ` liquidationPenalty createdTimestamp createdBlockNumber - inputTokenBalance # collateralAssets + inputTokenBalance variableBorrowedTokenBalance - totalValueLockedUSD # collateralAssetsUsd? - lastUpdate # timestamp + totalValueLockedUSD + lastUpdate reserves reserveFactor oracle { @@ -63,7 +63,7 @@ export const marketFragment = ` type } protocol { - id # Morpho Blue Address? + id network # Chain Name protocol # Protocol Name } @@ -90,4 +90,58 @@ export const marketQuery = ` } } ${marketFragment} -`; \ No newline at end of file +`; + +// --- Added for Historical Data --- + +export const marketHourlySnapshotFragment = ` + fragment MarketHourlySnapshotFields on MarketHourlySnapshot { + id + timestamp + market { + id + inputToken { + ...TokenFields + } + borrowedToken { + ...TokenFields + } + } + rates { + id + rate # APY + side + type + } + totalDepositBalanceUSD + totalBorrowBalanceUSD + inputTokenBalance + inputTokenPriceUSD + hourlyDepositUSD + hourlyBorrowUSD + outputTokenSupply + variableBorrowedTokenBalance + # Note: The subgraph schema for snapshots doesn't seem to directly expose + # total native supply/borrow amounts historically, only USD values and hourly deltas. + } +`; + +export const marketHourlySnapshotsQuery = ` + query getMarketHourlySnapshots($marketId: Bytes!, $startTimestamp: BigInt!, $endTimestamp: BigInt!) { + marketHourlySnapshots( + first: 1000, # Subgraph max limit + orderBy: timestamp, + orderDirection: asc, + where: { + market: $marketId, + timestamp_gte: $startTimestamp, + timestamp_lte: $endTimestamp + } + ) { + ...MarketHourlySnapshotFields + } + } + ${marketHourlySnapshotFragment} + ${tokenFragment} # Ensure TokenFields fragment is included +`; +// --- End Added Section --- \ No newline at end of file diff --git a/src/hooks/useMarket.ts b/src/hooks/useMarket.ts index 87f6873f..2a6c4efb 100644 --- a/src/hooks/useMarket.ts +++ b/src/hooks/useMarket.ts @@ -3,19 +3,61 @@ import { SupportedNetworks } from '@/utils/networks'; import { URLS } from '@/utils/urls'; import { getMarketWarningsWithDetail } from '@/utils/warnings'; import { marketDetailQuery, marketHistoricalDataQuery } from '../graphql/morpho-api-queries'; -import { MarketDetail, TimeseriesOptions, Market } from '../utils/types'; +import { HistoricalData, TimeseriesOptions, Market } from '../utils/types'; // Assuming TimeseriesDataPoint is used within MarketRates/Volumes -type GraphQLResponse = { +// Define MarketRates/Volumes locally based on structure in types.ts +// as they are not exported directly +type MarketRates = { + supplyApy: TimeseriesDataPoint[]; + borrowApy: TimeseriesDataPoint[]; + rateAtUTarget: TimeseriesDataPoint[]; + utilization: TimeseriesDataPoint[]; +}; + +type MarketVolumes = { + supplyAssetsUsd: TimeseriesDataPoint[]; + borrowAssetsUsd: TimeseriesDataPoint[]; + liquidityAssetsUsd: TimeseriesDataPoint[]; + supplyAssets: TimeseriesDataPoint[]; + borrowAssets: TimeseriesDataPoint[]; + liquidityAssets: TimeseriesDataPoint[]; +}; +// We need TimeseriesDataPoint too +type TimeseriesDataPoint = { + x: number; + y: number; +}; + + +type MarketGraphQLResponse = { data: { - marketByUniqueKey: MarketDetail; + marketByUniqueKey: Market; }; errors?: { message: string }[]; }; -const graphqlFetcher = async ( +// Specific type for the historical data query response +// It returns a Market object augmented with historicalState +type MarketWithHistoricalState = Market & { + historicalState: { + rates: MarketRates; + volumes: MarketVolumes; + } | null; // Allow null if no data +}; + +type HistoricalDataGraphQLResponse = { + data: { + marketByUniqueKey: MarketWithHistoricalState; + }; + errors?: { message: string }[]; +}; + +// Generic fetcher, the caller needs to handle the specific data structure +// Add constraint to T +const graphqlFetcher = async >( query: string, variables: Record, -): Promise => { +): Promise => { const response = await fetch(URLS.MORPHO_BLUE_API, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -26,9 +68,10 @@ const graphqlFetcher = async ( throw new Error('Network response was not ok'); } - const result = (await response.json()) as GraphQLResponse; + const result = (await response.json()) as T; // Cast to generic T - if (result.errors) { + // Check for errors at the top level + if (result.errors && Array.isArray(result.errors) && result.errors.length > 0) { throw new Error(result.errors[0].message); } @@ -46,56 +89,69 @@ const processMarketData = (market: Market): Market => { }; export const useMarket = (uniqueKey: string, network: SupportedNetworks) => { - return useQuery({ + return useQuery({ // Expects Market type queryKey: ['market', uniqueKey, network], queryFn: async () => { - const response = await graphqlFetcher(marketDetailQuery, { uniqueKey, chainId: network }); + // Fetcher returns MarketGraphQLResponse here + const response = await graphqlFetcher(marketDetailQuery, { uniqueKey, chainId: network }); + if (!response.data || !response.data.marketByUniqueKey) { + throw new Error('Market data not found in response'); + } return processMarketData(response.data.marketByUniqueKey); }, }); }; +// Return type matching the structure within historicalState +export type HistoricalDataResult = { + rates: MarketRates | null; + volumes: MarketVolumes | null; +} | null; // Allow null for loading/error states + export const useMarketHistoricalData = ( - uniqueKey: string, - network: SupportedNetworks, - rateOptions: TimeseriesOptions, - volumeOptions: TimeseriesOptions, + uniqueKey: string | undefined, + network: SupportedNetworks | undefined, + // Rate and Volume options seem unused separately now? + // The query fetches both. Let's simplify to one options object. + options: TimeseriesOptions | undefined, ) => { - const fetchHistoricalData = async (options: TimeseriesOptions) => { - const response = await graphqlFetcher(marketHistoricalDataQuery, { - uniqueKey, - options, - chainId: network, - }); - return response.data.marketByUniqueKey.historicalState; - }; + // This query now returns the historicalState object containing both rates and volumes + const { data, isLoading, error, refetch } = useQuery({ + queryKey: ['marketHistoricalData', uniqueKey, network, options?.startTimestamp, options?.endTimestamp, options?.interval], + queryFn: async (): Promise => { + if (!uniqueKey || !network || !options) return null; - const rateQuery = useQuery({ - queryKey: ['marketHistoricalRates', uniqueKey, network, rateOptions], - queryFn: async () => fetchHistoricalData(rateOptions), - }); + // Use the specific response type for historical data + const response = await graphqlFetcher(marketHistoricalDataQuery, { + uniqueKey, + options, + chainId: network, + }); + + // Access historicalState correctly + const historicalState = response?.data?.marketByUniqueKey?.historicalState; + + if (!historicalState) { + console.warn("Historical state not found in response for", uniqueKey); + return { rates: null, volumes: null }; // Return empty structure + } - const volumeQuery = useQuery({ - queryKey: ['marketHistoricalVolumes', uniqueKey, network, volumeOptions], - queryFn: async () => fetchHistoricalData(volumeOptions), + // The API returns the structure we need directly + return { + rates: historicalState.rates, + volumes: historicalState.volumes + }; + }, + enabled: !!uniqueKey && !!network && !!options, + staleTime: 1000 * 60 * 5, // 5 minutes + placeholderData: (previousData) => previousData ?? null, }); + return { - data: { - rates: rateQuery.data, - volumes: volumeQuery.data, - }, - isLoading: { - rates: rateQuery.isLoading, - volumes: volumeQuery.isLoading, - }, - error: { - rates: rateQuery.error, - volumes: volumeQuery.error, - }, - refetch: { - rates: rateQuery.refetch, - volumes: volumeQuery.refetch, - }, + data: data, // Contains { rates: MarketRates | null, volumes: MarketVolumes | null } | null + isLoading: isLoading, + error: error, + refetch: refetch, // Refetches the combined historical data }; }; diff --git a/src/hooks/useSubgraphMarketHistoricalData.ts b/src/hooks/useSubgraphMarketHistoricalData.ts new file mode 100644 index 00000000..6ca887c8 --- /dev/null +++ b/src/hooks/useSubgraphMarketHistoricalData.ts @@ -0,0 +1,220 @@ +import { useQuery } from '@tanstack/react-query'; +import { SupportedNetworks } from '@/utils/networks'; +import { getSubgraphUrl } from '@/utils/subgraph-urls'; +import { + TimeseriesOptions, + TimeseriesDataPoint, // Assuming this is exported + HistoricalData, // Use HistoricalData which contains MarketRates & MarketVolumes +} from '../utils/types'; +import { marketHourlySnapshotsQuery } from '../graphql/morpho-subgraph-queries'; + +// Define MarketRates and MarketVolumes locally based on their structure in types.ts +// Ideally, these should be exported from src/utils/types.ts +type MarketRates = { + supplyApy: TimeseriesDataPoint[]; + borrowApy: TimeseriesDataPoint[]; + rateAtUTarget: TimeseriesDataPoint[]; + utilization: TimeseriesDataPoint[]; +}; + +type MarketVolumes = { + supplyAssetsUsd: TimeseriesDataPoint[]; + borrowAssetsUsd: TimeseriesDataPoint[]; + liquidityAssetsUsd: TimeseriesDataPoint[]; + supplyAssets: TimeseriesDataPoint[]; + borrowAssets: TimeseriesDataPoint[]; + liquidityAssets: TimeseriesDataPoint[]; +}; + + +// --- Local Type Definitions (Subgraph Specific) --- +interface SubgraphInterestRate { + id: string; + rate: string; + side: 'LENDER' | 'BORROWER'; + type: 'VARIABLE' | 'STABLE' | 'FIXED'; +} + +interface SubgraphMarketHourlySnapshot { + id: string; + timestamp: string; + market: { + id: string; + }; + rates: SubgraphInterestRate[]; + totalDepositBalanceUSD: string; + totalBorrowBalanceUSD: string; + inputTokenBalance: string; + inputTokenPriceUSD: string; + hourlyDepositUSD: string; + hourlyBorrowUSD: string; + outputTokenSupply: string | null; + variableBorrowedTokenBalance: string | null; +} +// --- End Local Type Definitions --- + + +// Helper function +const subgraphGraphqlFetcher = async ( + apiUrl: string, + query: string, + variables: Record, +): Promise => { + const response = await fetch(apiUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query, variables }), + }); + + if (!response.ok) { + throw new Error(`Network response was not ok (status: ${response.status})`); + } + + const result = (await response.json()) as T; + + if (result.errors && Array.isArray(result.errors) && result.errors.length > 0) { + console.error('Subgraph Query Errors:', result.errors); + throw new Error(`Subgraph query failed: ${result.errors[0].message}`); + } + + if (!result.data) { + console.error('Subgraph response missing data field:', result); + throw new Error('Subgraph query response did not contain data.'); + } + + return result; +}; + +// Define response type based on the query structure +interface SubgraphMarketHourlySnapshotQueryResponse { + data: { + marketHourlySnapshots: SubgraphMarketHourlySnapshot[]; + }; +} + +// Define the structure for the hook's return data +export interface TransformedSubgraphHistoricalData { + rates: MarketRates; + volumes: MarketVolumes; +} + +// Helper to create an empty MarketRates/MarketVolumes structure +const createEmptyHistoricalStructure = (): TransformedSubgraphHistoricalData => ({ + rates: { supplyApy: [], borrowApy: [], rateAtUTarget: [], utilization: [] }, + volumes: { + supplyAssetsUsd: [], borrowAssetsUsd: [], liquidityAssetsUsd: [], + supplyAssets: [], borrowAssets: [], liquidityAssets: [], + }, +}); + +const transformSubgraphSnapshots = ( + snapshots: SubgraphMarketHourlySnapshot[] | undefined +): TransformedSubgraphHistoricalData => { + const result = createEmptyHistoricalStructure(); + + if (!snapshots) { + return result; + } + + snapshots.forEach(snapshot => { + const timestamp = parseInt(snapshot.timestamp, 10); + if (isNaN(timestamp)) { + console.warn("Skipping snapshot due to invalid timestamp:", snapshot); + return; + } + + // Process Rates (APY) + const snapshotRates = Array.isArray(snapshot.rates) ? snapshot.rates : []; + const supplyRate = snapshotRates.find((r: SubgraphInterestRate | null | undefined) => r?.side === 'LENDER'); + const borrowRate = snapshotRates.find((r: SubgraphInterestRate | null | undefined) => r?.side === 'BORROWER'); + + const supplyApyValue = supplyRate?.rate ? parseFloat(supplyRate.rate) : 0; + const borrowApyValue = borrowRate?.rate ? parseFloat(borrowRate.rate) : 0; + + result.rates.supplyApy.push({ x: timestamp, y: !isNaN(supplyApyValue) ? supplyApyValue : 0 }); + result.rates.borrowApy.push({ x: timestamp, y: !isNaN(borrowApyValue) ? borrowApyValue : 0 }); + + // Placeholders for data not directly available in subgraph snapshots + result.rates.rateAtUTarget.push({ x: timestamp, y: 0 }); + result.rates.utilization.push({ x: timestamp, y: 0 }); + + // no USD values available in subgraph + const finalSupplyUsd = 0; + const finalBorrowUsd = 0; + const finalLiquidityUsd = 0; + + const supplyNative = BigInt(snapshot.inputTokenBalance ?? '0'); + const borrowNative = BigInt(snapshot.variableBorrowedTokenBalance ?? '0'); + const liquidityNative = supplyNative - borrowNative; + + result.volumes.supplyAssetsUsd.push({ x: timestamp, y: finalSupplyUsd }); + result.volumes.borrowAssetsUsd.push({ x: timestamp, y: finalBorrowUsd }); + result.volumes.liquidityAssetsUsd.push({ x: timestamp, y: finalLiquidityUsd }); + + // Process Native asset amounts + + + // Convert BigInt to number for TimeseriesDataPoint. + // Warning: Potential precision loss for very large numbers. + // Consider formatting units in the UI instead if precision is critical. + result.volumes.supplyAssets.push({ x: timestamp, y: Number(supplyNative) }); + result.volumes.borrowAssets.push({ x: timestamp, y: Number(borrowNative) }); + result.volumes.liquidityAssets.push({ x: timestamp, y: Number(liquidityNative) }); + }); + + // Sort data by timestamp + Object.values(result.rates).forEach((arr: TimeseriesDataPoint[]) => arr.sort((a, b) => a.x - b.x)); + Object.values(result.volumes).forEach((arr: TimeseriesDataPoint[]) => arr.sort((a, b) => a.x - b.x)); + + return result; +}; + + +export const useSubgraphMarketHistoricalData = ( + marketId: string | undefined, + network: SupportedNetworks | undefined, + timeRange: TimeseriesOptions | undefined, +) => { + return useQuery({ + queryKey: ['subgraphMarketHistoricalData', marketId, network, timeRange?.startTimestamp, timeRange?.endTimestamp, timeRange?.interval], + queryFn: async (): Promise => { + if (!marketId || !network || !timeRange || !timeRange.startTimestamp || !timeRange.endTimestamp) { + return createEmptyHistoricalStructure(); + } + + const subgraphApiUrl = getSubgraphUrl(network); + if (!subgraphApiUrl) { + console.error(`Subgraph URL for network ${network} is not defined.`); + return createEmptyHistoricalStructure(); + } + + try { + const variables = { + marketId: marketId.toLowerCase(), + startTimestamp: String(timeRange.startTimestamp), + endTimestamp: String(timeRange.endTimestamp), + }; + + const response = await subgraphGraphqlFetcher( + subgraphApiUrl, + marketHourlySnapshotsQuery, + variables + ); + + if (!response.data || !response.data.marketHourlySnapshots) { + return createEmptyHistoricalStructure(); + } + + return transformSubgraphSnapshots(response.data.marketHourlySnapshots); + + } catch (error) { + console.error("Error fetching or processing subgraph historical data:", error); + return createEmptyHistoricalStructure(); + } + }, + enabled: !!marketId && !!network && !!timeRange && !!timeRange.startTimestamp && !!timeRange.endTimestamp, + staleTime: 1000 * 60 * 5, // 5 minutes + placeholderData: (previousData) => previousData ?? createEmptyHistoricalStructure(), + retry: 1, + }); +}; \ No newline at end of file diff --git a/src/utils/types.ts b/src/utils/types.ts index 714dc195..0f1e2467 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -338,7 +338,7 @@ type MarketVolumes = { liquidityAssets: TimeseriesDataPoint[]; }; -export type MarketDetail = Market & { +export type HistoricalData = { historicalState: MarketRates & MarketVolumes; }; From 4c5fe725609c13d17d18299c73728d3c07a1c3f6 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sun, 20 Apr 2025 23:29:43 +0800 Subject: [PATCH 09/11] refactor: data source architecture --- app/market/[chainId]/[marketid]/content.tsx | 25 +- src/config/dataSources.ts | 29 +++ src/data-sources/morpho-api/fetchers.ts | 28 +++ src/data-sources/morpho-api/historical.ts | 101 ++++++++ src/data-sources/morpho-api/market.ts | 36 +++ src/data-sources/subgraph/fetchers.ts | 30 +++ src/data-sources/subgraph/historical.ts | 146 ++++++++++++ .../subgraph/market.ts} | 105 +++------ src/hooks/useMarket.ts | 157 ------------- src/hooks/useMarketData.ts | 58 +++++ src/hooks/useMarketHistoricalData.ts | 69 ++++++ src/hooks/useSubgraphMarketHistoricalData.ts | 220 ------------------ 12 files changed, 546 insertions(+), 458 deletions(-) create mode 100644 src/config/dataSources.ts create mode 100644 src/data-sources/morpho-api/fetchers.ts create mode 100644 src/data-sources/morpho-api/historical.ts create mode 100644 src/data-sources/morpho-api/market.ts create mode 100644 src/data-sources/subgraph/fetchers.ts create mode 100644 src/data-sources/subgraph/historical.ts rename src/{hooks/useSubgraphMarket.ts => data-sources/subgraph/market.ts} (64%) delete mode 100644 src/hooks/useMarket.ts create mode 100644 src/hooks/useMarketData.ts create mode 100644 src/hooks/useMarketHistoricalData.ts delete mode 100644 src/hooks/useSubgraphMarketHistoricalData.ts diff --git a/app/market/[chainId]/[marketid]/content.tsx b/app/market/[chainId]/[marketid]/content.tsx index 27c662c9..2d865e1a 100644 --- a/app/market/[chainId]/[marketid]/content.tsx +++ b/app/market/[chainId]/[marketid]/content.tsx @@ -16,7 +16,6 @@ import Header from '@/components/layout/header/Header'; import OracleVendorBadge from '@/components/OracleVendorBadge'; import { SupplyModalV2 } from '@/components/SupplyModalV2'; import { TokenIcon } from '@/components/TokenIcon'; -import { useMarket, useMarketHistoricalData } from '@/hooks/useMarket'; import { useOraclePrice } from '@/hooks/useOraclePrice'; import useUserPositions from '@/hooks/useUserPosition'; import MORPHO_LOGO from '@/imgs/tokens/morpho.svg'; @@ -30,8 +29,8 @@ import { PositionStats } from './components/PositionStats'; import { SuppliesTable } from './components/SuppliesTable'; import RateChart from './RateChart'; import VolumeChart from './VolumeChart'; -import { useSubgraphMarket } from '@/hooks/useSubgraphMarket'; -import { useSubgraphMarketHistoricalData } from '@/hooks/useSubgraphMarketHistoricalData'; +import { useMarketData } from '@/hooks/useMarketData'; +import { useMarketHistoricalData } from '@/hooks/useMarketHistoricalData'; const NOW = Math.floor(Date.now() / 1000); const WEEK_IN_SECONDS = 7 * 24 * 60 * 60; @@ -63,21 +62,14 @@ function MarketContent() { interval: 'HOUR', }); - const {data: market, isLoading: isMarketLoading, error: marketError} = useSubgraphMarket(marketid as string, network); + const {data: market, isLoading: isMarketLoading, error: marketError} = useMarketData(marketid as string, network); - console.log('market', market?.oracle); - - // const { - // data: historicalData, - // isLoading: isHistoricalLoading, - // refetch: refetchHistoricalData, - // } = useMarketHistoricalData(marketid as string, network, rateTimeRange, volumeTimeRange); const { data: historicalData, isLoading: isHistoricalLoading, refetch: refetchHistoricalData, - } = useSubgraphMarketHistoricalData(marketid as string, network, rateTimeRange); + } = useMarketHistoricalData(marketid as string, network, rateTimeRange); // 5. Oracle price hook - safely handle undefined market const { price: oraclePrice } = useOraclePrice({ @@ -143,7 +135,14 @@ function MarketContent() { } if (!market) { - return
Market data not available
; + return (<> +
+
+
+ +
+
+ ); } // 8. Derived values that depend on market data diff --git a/src/config/dataSources.ts b/src/config/dataSources.ts new file mode 100644 index 00000000..26c20b20 --- /dev/null +++ b/src/config/dataSources.ts @@ -0,0 +1,29 @@ +import { SupportedNetworks } from '@/utils/networks'; + +/** + * Determines the primary data source for market details based on the network. + */ +export const getMarketDataSource = (network: SupportedNetworks): 'morpho' | 'subgraph' => { + // Use Subgraph for specific networks, Morpho API for others + switch (network) { + // Networks using subgraph + case SupportedNetworks.Mainnet: + case SupportedNetworks.Base: + return 'subgraph'; + // Networks using morpho-api + default: + return 'morpho'; // Default to Morpho API + } +}; + +/** + * Determines the data source for historical market data. + * Assumes only Morpho API provides this, unless explicitly excluded. + */ +export const getHistoricalDataSource = (network: SupportedNetworks): 'morpho' | 'subgraph' => { + // Networks excluded from Morpho API historical data + // Add networks here if they don't support historical data via Morpho API + + // Assume other networks have historical data via Morpho API + return 'morpho'; +}; \ No newline at end of file diff --git a/src/data-sources/morpho-api/fetchers.ts b/src/data-sources/morpho-api/fetchers.ts new file mode 100644 index 00000000..09105c6f --- /dev/null +++ b/src/data-sources/morpho-api/fetchers.ts @@ -0,0 +1,28 @@ +import { URLS } from '@/utils/urls'; + +// Generic fetcher for Morpho API +export const morphoGraphqlFetcher = async >( + query: string, + variables: Record, +): Promise => { + const response = await fetch(URLS.MORPHO_BLUE_API, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query, variables }), + }); + + if (!response.ok) { + throw new Error('Network response was not ok from Morpho API'); + } + + const result = (await response.json()) as T; + + // Check for GraphQL errors + if ('errors' in result && Array.isArray((result as any).errors) && (result as any).errors.length > 0) { + // Log the full error for debugging + console.error('Morpho API GraphQL Error:', (result as any).errors); + throw new Error((result as any).errors[0].message || 'Unknown GraphQL error from Morpho API'); + } + + return result; +}; \ No newline at end of file diff --git a/src/data-sources/morpho-api/historical.ts b/src/data-sources/morpho-api/historical.ts new file mode 100644 index 00000000..66886b27 --- /dev/null +++ b/src/data-sources/morpho-api/historical.ts @@ -0,0 +1,101 @@ +import { SupportedNetworks } from '@/utils/networks'; +import { TimeseriesOptions, Market, TimeseriesDataPoint } from '@/utils/types'; +import { marketHistoricalDataQuery } from '@/graphql/morpho-api-queries'; +import { morphoGraphqlFetcher } from './fetchers'; + +// --- Types related to Historical Data --- +// Re-exported from types.ts for clarity or define locally if not exported +export type { TimeseriesDataPoint, TimeseriesOptions }; + +export type MarketRates = { + supplyApy: TimeseriesDataPoint[]; + borrowApy: TimeseriesDataPoint[]; + rateAtUTarget: TimeseriesDataPoint[]; + utilization: TimeseriesDataPoint[]; +}; + +export type MarketVolumes = { + supplyAssetsUsd: TimeseriesDataPoint[]; + borrowAssetsUsd: TimeseriesDataPoint[]; + liquidityAssetsUsd: TimeseriesDataPoint[]; + supplyAssets: TimeseriesDataPoint[]; + borrowAssets: TimeseriesDataPoint[]; + liquidityAssets: TimeseriesDataPoint[]; +}; + +// Adjust the response structure type: historicalState contains rates/volumes directly +type MarketWithHistoricalState = Market & { + historicalState: (Partial & Partial) | null; +}; + +type HistoricalDataGraphQLResponse = { + data: { + marketByUniqueKey: MarketWithHistoricalState; + }; + errors?: { message: string }[]; +}; + +// Standardized result type for historical data hooks +// Represents the successful return structure, undefined indicates not found/error +export type HistoricalDataSuccessResult = { + rates: MarketRates; + volumes: MarketVolumes; +}; +// --- End Types --- + +// Fetcher for historical market data from Morpho API +export const fetchMorphoMarketHistoricalData = async ( + uniqueKey: string, + network: SupportedNetworks, + options: TimeseriesOptions, +): Promise => { + try { + const response = await morphoGraphqlFetcher(marketHistoricalDataQuery, { + uniqueKey, + options, + chainId: network, + }); + + 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 (!historicalState || Object.keys(historicalState).length === 0 || !historicalState.supplyApy) { // Example check + console.warn("Historical state not found, empty, or missing essential data in Morpho API response for", uniqueKey); + return null; + } + + // Construct the expected nested structure for the hook + // Assume API returns *some* data if historicalState is valid + const rates: MarketRates = { + supplyApy: historicalState.supplyApy ?? [], + borrowApy: historicalState.borrowApy ?? [], + rateAtUTarget: historicalState.rateAtUTarget ?? [], + utilization: historicalState.utilization ?? [], + }; + const volumes: MarketVolumes = { + supplyAssetsUsd: historicalState.supplyAssetsUsd ?? [], + borrowAssetsUsd: historicalState.borrowAssetsUsd ?? [], + liquidityAssetsUsd: historicalState.liquidityAssetsUsd ?? [], + supplyAssets: historicalState.supplyAssets ?? [], + borrowAssets: historicalState.borrowAssets ?? [], + liquidityAssets: historicalState.liquidityAssets ?? [], + }; + + // Sort each timeseries array by timestamp (x-axis) ascending + const sortByTimestamp = (a: TimeseriesDataPoint, b: TimeseriesDataPoint) => a.x - b.x; + Object.values(rates).forEach(arr => arr.sort(sortByTimestamp)); + Object.values(volumes).forEach(arr => arr.sort(sortByTimestamp)); + + return { rates, volumes }; + + } catch (error) { + console.error("Error fetching Morpho historical data:", error); + return null; // Return null on error + } +}; \ No newline at end of file diff --git a/src/data-sources/morpho-api/market.ts b/src/data-sources/morpho-api/market.ts new file mode 100644 index 00000000..303dff0c --- /dev/null +++ b/src/data-sources/morpho-api/market.ts @@ -0,0 +1,36 @@ +import { SupportedNetworks } from '@/utils/networks'; +import { getMarketWarningsWithDetail } from '@/utils/warnings'; +import { marketDetailQuery } from '@/graphql/morpho-api-queries'; +import { Market } from '@/utils/types'; +import { morphoGraphqlFetcher } from './fetchers'; + +// Removed historical types (MarketRates, MarketVolumes, etc.) +// Moved HistoricalDataResult to historical.ts + +type MarketGraphQLResponse = { + data: { + marketByUniqueKey: Market; + }; + errors?: { message: string }[]; +}; + +const processMarketData = (market: Market): Market => { + const warningsWithDetail = getMarketWarningsWithDetail(market); + return { + ...market, + warningsWithDetail, + isProtectedByLiquidationBots: false, + }; +}; + +// Fetcher for market details from Morpho API +export const fetchMorphoMarket = async (uniqueKey: string, network: SupportedNetworks): Promise => { + const response = await morphoGraphqlFetcher(marketDetailQuery, { + uniqueKey, + chainId: network, + }); + if (!response.data || !response.data.marketByUniqueKey) { + throw new Error('Market data not found in Morpho API response'); + } + return processMarketData(response.data.marketByUniqueKey); +}; \ No newline at end of file diff --git a/src/data-sources/subgraph/fetchers.ts b/src/data-sources/subgraph/fetchers.ts new file mode 100644 index 00000000..1228269a --- /dev/null +++ b/src/data-sources/subgraph/fetchers.ts @@ -0,0 +1,30 @@ +import { getSubgraphUrl } from '@/utils/subgraph-urls'; + +// Generic fetcher for Subgraph API +export const subgraphGraphqlFetcher = async ( + apiUrl: string, // Subgraph URL can vary + query: string, + variables: Record, +): Promise => { + const response = await fetch(apiUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query, variables }), + }); + + if (!response.ok) { + console.error('Subgraph network response was not ok', response.status, response.statusText); + throw new Error(`Network response was not ok from Subgraph API: ${apiUrl}`); + } + + const result = (await response.json()) as T; + + // Check for GraphQL errors in the Subgraph response + if ('errors' in result && Array.isArray((result as any).errors) && (result as any).errors.length > 0) { + // Log the full error for debugging + console.error('Subgraph API GraphQL Error:', (result as any).errors); + throw new Error((result as any).errors[0].message || 'Unknown GraphQL error from Subgraph API'); + } + + return result; +}; \ No newline at end of file diff --git a/src/data-sources/subgraph/historical.ts b/src/data-sources/subgraph/historical.ts new file mode 100644 index 00000000..f2116f89 --- /dev/null +++ b/src/data-sources/subgraph/historical.ts @@ -0,0 +1,146 @@ +import { SupportedNetworks } from '@/utils/networks'; +import { getSubgraphUrl } from '@/utils/subgraph-urls'; +import { + TimeseriesOptions, + TimeseriesDataPoint, +} from '@/utils/types'; // Assuming TimeseriesDataPoint is exported +import { marketHourlySnapshotsQuery } from '@/graphql/morpho-subgraph-queries'; +import { subgraphGraphqlFetcher } from './fetchers'; +import { HistoricalDataSuccessResult, MarketRates, MarketVolumes } from '../morpho-api/historical'; // Updated path & added imports + +// --- Subgraph Specific Types (Copied from useSubgraphMarketHistoricalData.ts) --- +interface SubgraphInterestRate { + id: string; + rate: string; + side: 'LENDER' | 'BORROWER'; + type: 'VARIABLE' | 'STABLE' | 'FIXED'; +} + +interface SubgraphMarketHourlySnapshot { + id: string; + timestamp: string; + market: { + id: string; + }; + rates: SubgraphInterestRate[]; + totalDepositBalanceUSD: string; + totalBorrowBalanceUSD: string; + inputTokenBalance: string; + inputTokenPriceUSD: string; + hourlyDepositUSD: string; + hourlyBorrowUSD: string; + outputTokenSupply: string | null; + variableBorrowedTokenBalance: string | null; +} + +interface SubgraphMarketHourlySnapshotQueryResponse { + data: { + marketHourlySnapshots: SubgraphMarketHourlySnapshot[]; + }; +} +// --- End Subgraph Specific Types --- + +// Transformation function (simplified) +const transformSubgraphSnapshotsToHistoricalResult = ( + snapshots: SubgraphMarketHourlySnapshot[] // Expect non-empty array here +): HistoricalDataSuccessResult => { + const rates: MarketRates = { + supplyApy: [] as TimeseriesDataPoint[], + borrowApy: [] as TimeseriesDataPoint[], + rateAtUTarget: [] as TimeseriesDataPoint[], + utilization: [] as TimeseriesDataPoint[], + }; + const volumes: MarketVolumes = { + supplyAssetsUsd: [] as TimeseriesDataPoint[], + borrowAssetsUsd: [] as TimeseriesDataPoint[], + liquidityAssetsUsd: [] as TimeseriesDataPoint[], + supplyAssets: [] as TimeseriesDataPoint[], + borrowAssets: [] as TimeseriesDataPoint[], + liquidityAssets: [] as TimeseriesDataPoint[], + }; + + // No need to check for !snapshots here, handled by caller + snapshots.forEach(snapshot => { + const timestamp = parseInt(snapshot.timestamp, 10); + if (isNaN(timestamp)) { + console.warn("Skipping snapshot due to invalid timestamp:", snapshot); + return; + } + + const snapshotRates = Array.isArray(snapshot.rates) ? snapshot.rates : []; + const supplyRate = snapshotRates.find(r => r?.side === 'LENDER'); + const borrowRate = snapshotRates.find(r => r?.side === 'BORROWER'); + + const supplyApyValue = supplyRate?.rate ? parseFloat(supplyRate.rate) : 0; + const borrowApyValue = borrowRate?.rate ? parseFloat(borrowRate.rate) : 0; + + rates.supplyApy.push({ x: timestamp, y: !isNaN(supplyApyValue) ? supplyApyValue : 0 }); + rates.borrowApy.push({ x: timestamp, y: !isNaN(borrowApyValue) ? borrowApyValue : 0 }); + rates.rateAtUTarget.push({ x: timestamp, y: 0 }); + rates.utilization.push({ x: timestamp, y: 0 }); + + const supplyNative = BigInt(snapshot.inputTokenBalance ?? '0'); + const borrowNative = BigInt(snapshot.variableBorrowedTokenBalance ?? '0'); + const liquidityNative = supplyNative - borrowNative; + + volumes.supplyAssetsUsd.push({ x: timestamp, y: 0 }); + volumes.borrowAssetsUsd.push({ x: timestamp, y: 0 }); + volumes.liquidityAssetsUsd.push({ x: timestamp, y: 0 }); + + volumes.supplyAssets.push({ x: timestamp, y: Number(supplyNative) }); + volumes.borrowAssets.push({ x: timestamp, y: Number(borrowNative) }); + volumes.liquidityAssets.push({ x: timestamp, y: Number(liquidityNative) }); + }); + + // Sort data by timestamp + Object.values(rates).forEach((arr: TimeseriesDataPoint[]) => arr.sort((a: TimeseriesDataPoint, b: TimeseriesDataPoint) => a.x - b.x)); + Object.values(volumes).forEach((arr: TimeseriesDataPoint[]) => arr.sort((a: TimeseriesDataPoint, b: TimeseriesDataPoint) => a.x - b.x)); + + return { rates, volumes }; +}; + +// Fetcher function for Subgraph historical data +export const fetchSubgraphMarketHistoricalData = async ( + marketId: string, + network: SupportedNetworks, + timeRange: TimeseriesOptions, +): Promise => { // Updated return type + + if (!timeRange.startTimestamp || !timeRange.endTimestamp) { + console.warn('Subgraph historical fetch requires start and end timestamps.'); + return null; // Return null + } + + const subgraphApiUrl = getSubgraphUrl(network); + if (!subgraphApiUrl) { + console.error(`Subgraph URL for network ${network} is not defined.`); + return null; // Return null + } + + try { + const variables = { + marketId: marketId.toLowerCase(), + startTimestamp: String(timeRange.startTimestamp), + endTimestamp: String(timeRange.endTimestamp), + }; + + const response = await subgraphGraphqlFetcher( + subgraphApiUrl, + marketHourlySnapshotsQuery, + variables + ); + + // If no data or empty snapshots array, return null + if (!response.data || !response.data.marketHourlySnapshots || response.data.marketHourlySnapshots.length === 0) { + console.warn(`No subgraph historical snapshots found for market ${marketId}`); + return null; + } + + // Pass the guaranteed non-empty array to the transformer + return transformSubgraphSnapshotsToHistoricalResult(response.data.marketHourlySnapshots); + + } catch (error) { + console.error("Error fetching or processing subgraph historical data:", error); + return null; // Return null on error + } +}; \ No newline at end of file diff --git a/src/hooks/useSubgraphMarket.ts b/src/data-sources/subgraph/market.ts similarity index 64% rename from src/hooks/useSubgraphMarket.ts rename to src/data-sources/subgraph/market.ts index 06fc3312..14ef00f0 100644 --- a/src/hooks/useSubgraphMarket.ts +++ b/src/data-sources/subgraph/market.ts @@ -1,40 +1,14 @@ -import { useQuery } from '@tanstack/react-query'; import { SupportedNetworks } from '@/utils/networks'; import { getSubgraphUrl } from '@/utils/subgraph-urls'; -import { WarningWithDetail, MorphoChainlinkOracleData, Market } from '../utils/types'; -import { - marketQuery as subgraphMarketQuery -} from '../graphql/morpho-subgraph-queries'; +import { WarningWithDetail, MorphoChainlinkOracleData, Market } from '@/utils/types'; +import { marketQuery as subgraphMarketQuery } from '@/graphql/morpho-subgraph-queries'; // Assuming query is here import { SubgraphMarket, SubgraphMarketQueryResponse, SubgraphToken, -} from '../utils/subgraph-types'; +} from '@/utils/subgraph-types'; import { Address } from 'viem'; - -const subgraphGraphqlFetcher = async ( - apiUrl: string, - query: string, - variables: Record, -): Promise => { - const response = await fetch(apiUrl, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ query, variables }), - }); - - if (!response.ok) { - throw new Error('Network response was not ok'); - } - - const result = (await response.json()) as T; - - if ('errors' in result && Array.isArray((result as any).errors) && (result as any).errors.length > 0) { - throw new Error((result as any).errors[0].message); - } - - return result; -}; +import { subgraphGraphqlFetcher } from './fetchers'; // Helper to safely parse BigDecimal/BigInt strings const safeParseFloat = (value: string | null | undefined): number => { @@ -55,7 +29,10 @@ const safeParseInt = (value: string | null | undefined): number => { } } -const transformSubgraphMarketToMarketDetail = (subgraphMarket: Partial, network: SupportedNetworks): Market => { +const transformSubgraphMarketToMarket = ( + subgraphMarket: Partial, + network: SupportedNetworks +): Market => { const marketId = subgraphMarket.id ?? ''; const lltv = subgraphMarket.lltv ?? '0'; @@ -67,8 +44,6 @@ const transformSubgraphMarketToMarketDetail = (subgraphMarket: Partial | undefined) => ({ id: token?.id ?? '0x', address: token?.id ?? '0x', @@ -87,7 +62,6 @@ const transformSubgraphMarketToMarketDetail = (subgraphMarket: Partial { - return useQuery({ - queryKey: ['subgraphMarket', uniqueKey, network], - queryFn: async () => { - if (!uniqueKey || !network) return null; +// Fetcher for market details from Subgraph +export const fetchSubgraphMarket = async ( + uniqueKey: string, + network: SupportedNetworks +): Promise => { - const subgraphApiUrl = getSubgraphUrl(network); + const subgraphApiUrl = getSubgraphUrl(network); - if (!subgraphApiUrl) { + if (!subgraphApiUrl) { console.error(`Subgraph URL for network ${network} is not defined.`); throw new Error(`Subgraph URL for network ${network} is not defined.`); - } + } - const response = await subgraphGraphqlFetcher( - subgraphApiUrl, - subgraphMarketQuery, - { - id: uniqueKey.toLowerCase() - } - ); + const response = await subgraphGraphqlFetcher( + subgraphApiUrl, + subgraphMarketQuery, + { + id: uniqueKey.toLowerCase() // Ensure ID is lowercase for subgraph + } + ); - const marketData = response.data.market; + const marketData = response.data.market; - if (!marketData) { + if (!marketData) { console.warn(`Market with key ${uniqueKey} not found in Subgraph response.`); - return null; - } + return null; // Return null if not found, hook can handle this + } - return transformSubgraphMarketToMarketDetail(marketData, network); - }, - enabled: !!uniqueKey && !!network, - staleTime: 1000 * 60 * 5, - }); -}; + return transformSubgraphMarketToMarket(marketData, network); +}; \ No newline at end of file diff --git a/src/hooks/useMarket.ts b/src/hooks/useMarket.ts deleted file mode 100644 index 2a6c4efb..00000000 --- a/src/hooks/useMarket.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { SupportedNetworks } from '@/utils/networks'; -import { URLS } from '@/utils/urls'; -import { getMarketWarningsWithDetail } from '@/utils/warnings'; -import { marketDetailQuery, marketHistoricalDataQuery } from '../graphql/morpho-api-queries'; -import { HistoricalData, TimeseriesOptions, Market } from '../utils/types'; // Assuming TimeseriesDataPoint is used within MarketRates/Volumes - -// Define MarketRates/Volumes locally based on structure in types.ts -// as they are not exported directly -type MarketRates = { - supplyApy: TimeseriesDataPoint[]; - borrowApy: TimeseriesDataPoint[]; - rateAtUTarget: TimeseriesDataPoint[]; - utilization: TimeseriesDataPoint[]; -}; - -type MarketVolumes = { - supplyAssetsUsd: TimeseriesDataPoint[]; - borrowAssetsUsd: TimeseriesDataPoint[]; - liquidityAssetsUsd: TimeseriesDataPoint[]; - supplyAssets: TimeseriesDataPoint[]; - borrowAssets: TimeseriesDataPoint[]; - liquidityAssets: TimeseriesDataPoint[]; -}; -// We need TimeseriesDataPoint too -type TimeseriesDataPoint = { - x: number; - y: number; -}; - - -type MarketGraphQLResponse = { - data: { - marketByUniqueKey: Market; - }; - errors?: { message: string }[]; -}; - -// Specific type for the historical data query response -// It returns a Market object augmented with historicalState -type MarketWithHistoricalState = Market & { - historicalState: { - rates: MarketRates; - volumes: MarketVolumes; - } | null; // Allow null if no data -}; - -type HistoricalDataGraphQLResponse = { - data: { - marketByUniqueKey: MarketWithHistoricalState; - }; - errors?: { message: string }[]; -}; - -// Generic fetcher, the caller needs to handle the specific data structure -// Add constraint to T -const graphqlFetcher = async >( - query: string, - variables: Record, -): Promise => { - const response = await fetch(URLS.MORPHO_BLUE_API, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ query, variables }), - }); - - if (!response.ok) { - throw new Error('Network response was not ok'); - } - - const result = (await response.json()) as T; // Cast to generic T - - // Check for errors at the top level - if (result.errors && Array.isArray(result.errors) && result.errors.length > 0) { - throw new Error(result.errors[0].message); - } - - return result; -}; - -const processMarketData = (market: Market): Market => { - const warningsWithDetail = getMarketWarningsWithDetail(market); - - return { - ...market, - warningsWithDetail, - isProtectedByLiquidationBots: false, // NOT needed for now, might implement later - }; -}; - -export const useMarket = (uniqueKey: string, network: SupportedNetworks) => { - return useQuery({ // Expects Market type - queryKey: ['market', uniqueKey, network], - queryFn: async () => { - // Fetcher returns MarketGraphQLResponse here - const response = await graphqlFetcher(marketDetailQuery, { uniqueKey, chainId: network }); - if (!response.data || !response.data.marketByUniqueKey) { - throw new Error('Market data not found in response'); - } - return processMarketData(response.data.marketByUniqueKey); - }, - }); -}; - -// Return type matching the structure within historicalState -export type HistoricalDataResult = { - rates: MarketRates | null; - volumes: MarketVolumes | null; -} | null; // Allow null for loading/error states - -export const useMarketHistoricalData = ( - uniqueKey: string | undefined, - network: SupportedNetworks | undefined, - // Rate and Volume options seem unused separately now? - // The query fetches both. Let's simplify to one options object. - options: TimeseriesOptions | undefined, -) => { - // This query now returns the historicalState object containing both rates and volumes - const { data, isLoading, error, refetch } = useQuery({ - queryKey: ['marketHistoricalData', uniqueKey, network, options?.startTimestamp, options?.endTimestamp, options?.interval], - queryFn: async (): Promise => { - if (!uniqueKey || !network || !options) return null; - - // Use the specific response type for historical data - const response = await graphqlFetcher(marketHistoricalDataQuery, { - uniqueKey, - options, - chainId: network, - }); - - // Access historicalState correctly - const historicalState = response?.data?.marketByUniqueKey?.historicalState; - - if (!historicalState) { - console.warn("Historical state not found in response for", uniqueKey); - return { rates: null, volumes: null }; // Return empty structure - } - - // The API returns the structure we need directly - return { - rates: historicalState.rates, - volumes: historicalState.volumes - }; - }, - enabled: !!uniqueKey && !!network && !!options, - staleTime: 1000 * 60 * 5, // 5 minutes - placeholderData: (previousData) => previousData ?? null, - }); - - - return { - data: data, // Contains { rates: MarketRates | null, volumes: MarketVolumes | null } | null - isLoading: isLoading, - error: error, - refetch: refetch, // Refetches the combined historical data - }; -}; diff --git a/src/hooks/useMarketData.ts b/src/hooks/useMarketData.ts new file mode 100644 index 00000000..14e5022c --- /dev/null +++ b/src/hooks/useMarketData.ts @@ -0,0 +1,58 @@ +import { useQuery } from '@tanstack/react-query'; +import { SupportedNetworks } from '@/utils/networks'; +import { Market } from '@/utils/types'; +import { getMarketDataSource } from '@/config/dataSources'; +import { fetchMorphoMarket } from '@/data-sources/morpho-api/market'; +import { fetchSubgraphMarket } from '@/data-sources/subgraph/market'; + +export const useMarketData = ( + uniqueKey: string | undefined, + network: SupportedNetworks | undefined, +) => { + 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 + } + + 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 + } + + // 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 + placeholderData: (previousData) => previousData ?? null, + retry: 1, // Optional: retry once on failure + }); + + return { + data: data, + isLoading: isLoading, + error: error, + refetch: refetch, + dataSource: dataSource, // Expose the determined data source + }; +}; \ No newline at end of file diff --git a/src/hooks/useMarketHistoricalData.ts b/src/hooks/useMarketHistoricalData.ts new file mode 100644 index 00000000..23e5558b --- /dev/null +++ b/src/hooks/useMarketHistoricalData.ts @@ -0,0 +1,69 @@ +import { useQuery } from '@tanstack/react-query'; +import { SupportedNetworks } from '@/utils/networks'; +import { TimeseriesOptions } from '@/utils/types'; +import { getHistoricalDataSource } from '@/config/dataSources'; +import { + fetchMorphoMarketHistoricalData, + HistoricalDataSuccessResult, +} from '@/data-sources/morpho-api/historical'; +import { + fetchSubgraphMarketHistoricalData, +} from '@/data-sources/subgraph/historical'; + +export const useMarketHistoricalData = ( + uniqueKey: string | undefined, + network: SupportedNetworks | undefined, + options: TimeseriesOptions | undefined, +) => { + const queryKey = [ + 'marketHistoricalData', + uniqueKey, + network, + options?.startTimestamp, + options?.endTimestamp, + options?.interval, + ]; + + const dataSource = network ? getHistoricalDataSource(network) : null; + + const { data, isLoading, error, refetch } = useQuery< + HistoricalDataSuccessResult | null + >({ + queryKey: queryKey, + queryFn: async (): Promise => { + if (!uniqueKey || !network || !options || !dataSource) { + console.log('Historical data prerequisites not met or source unavailable.', { + uniqueKey, network, options, dataSource + }); + return null; + } + + 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; + } else if (dataSource === 'subgraph') { + const res = await fetchSubgraphMarketHistoricalData(uniqueKey, network, options); + console.log('res', res); + return res; + } + + console.warn("Unknown historical data source determined"); + return null; + }, + enabled: !!uniqueKey && !!network && !!options && !!dataSource, + staleTime: 1000 * 60 * 5, + placeholderData: null, + retry: 1, + }); + + return { + data: data, + isLoading: isLoading, + error: error, + refetch: refetch, + dataSource: dataSource, + }; +}; \ No newline at end of file diff --git a/src/hooks/useSubgraphMarketHistoricalData.ts b/src/hooks/useSubgraphMarketHistoricalData.ts deleted file mode 100644 index 6ca887c8..00000000 --- a/src/hooks/useSubgraphMarketHistoricalData.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { SupportedNetworks } from '@/utils/networks'; -import { getSubgraphUrl } from '@/utils/subgraph-urls'; -import { - TimeseriesOptions, - TimeseriesDataPoint, // Assuming this is exported - HistoricalData, // Use HistoricalData which contains MarketRates & MarketVolumes -} from '../utils/types'; -import { marketHourlySnapshotsQuery } from '../graphql/morpho-subgraph-queries'; - -// Define MarketRates and MarketVolumes locally based on their structure in types.ts -// Ideally, these should be exported from src/utils/types.ts -type MarketRates = { - supplyApy: TimeseriesDataPoint[]; - borrowApy: TimeseriesDataPoint[]; - rateAtUTarget: TimeseriesDataPoint[]; - utilization: TimeseriesDataPoint[]; -}; - -type MarketVolumes = { - supplyAssetsUsd: TimeseriesDataPoint[]; - borrowAssetsUsd: TimeseriesDataPoint[]; - liquidityAssetsUsd: TimeseriesDataPoint[]; - supplyAssets: TimeseriesDataPoint[]; - borrowAssets: TimeseriesDataPoint[]; - liquidityAssets: TimeseriesDataPoint[]; -}; - - -// --- Local Type Definitions (Subgraph Specific) --- -interface SubgraphInterestRate { - id: string; - rate: string; - side: 'LENDER' | 'BORROWER'; - type: 'VARIABLE' | 'STABLE' | 'FIXED'; -} - -interface SubgraphMarketHourlySnapshot { - id: string; - timestamp: string; - market: { - id: string; - }; - rates: SubgraphInterestRate[]; - totalDepositBalanceUSD: string; - totalBorrowBalanceUSD: string; - inputTokenBalance: string; - inputTokenPriceUSD: string; - hourlyDepositUSD: string; - hourlyBorrowUSD: string; - outputTokenSupply: string | null; - variableBorrowedTokenBalance: string | null; -} -// --- End Local Type Definitions --- - - -// Helper function -const subgraphGraphqlFetcher = async ( - apiUrl: string, - query: string, - variables: Record, -): Promise => { - const response = await fetch(apiUrl, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ query, variables }), - }); - - if (!response.ok) { - throw new Error(`Network response was not ok (status: ${response.status})`); - } - - const result = (await response.json()) as T; - - if (result.errors && Array.isArray(result.errors) && result.errors.length > 0) { - console.error('Subgraph Query Errors:', result.errors); - throw new Error(`Subgraph query failed: ${result.errors[0].message}`); - } - - if (!result.data) { - console.error('Subgraph response missing data field:', result); - throw new Error('Subgraph query response did not contain data.'); - } - - return result; -}; - -// Define response type based on the query structure -interface SubgraphMarketHourlySnapshotQueryResponse { - data: { - marketHourlySnapshots: SubgraphMarketHourlySnapshot[]; - }; -} - -// Define the structure for the hook's return data -export interface TransformedSubgraphHistoricalData { - rates: MarketRates; - volumes: MarketVolumes; -} - -// Helper to create an empty MarketRates/MarketVolumes structure -const createEmptyHistoricalStructure = (): TransformedSubgraphHistoricalData => ({ - rates: { supplyApy: [], borrowApy: [], rateAtUTarget: [], utilization: [] }, - volumes: { - supplyAssetsUsd: [], borrowAssetsUsd: [], liquidityAssetsUsd: [], - supplyAssets: [], borrowAssets: [], liquidityAssets: [], - }, -}); - -const transformSubgraphSnapshots = ( - snapshots: SubgraphMarketHourlySnapshot[] | undefined -): TransformedSubgraphHistoricalData => { - const result = createEmptyHistoricalStructure(); - - if (!snapshots) { - return result; - } - - snapshots.forEach(snapshot => { - const timestamp = parseInt(snapshot.timestamp, 10); - if (isNaN(timestamp)) { - console.warn("Skipping snapshot due to invalid timestamp:", snapshot); - return; - } - - // Process Rates (APY) - const snapshotRates = Array.isArray(snapshot.rates) ? snapshot.rates : []; - const supplyRate = snapshotRates.find((r: SubgraphInterestRate | null | undefined) => r?.side === 'LENDER'); - const borrowRate = snapshotRates.find((r: SubgraphInterestRate | null | undefined) => r?.side === 'BORROWER'); - - const supplyApyValue = supplyRate?.rate ? parseFloat(supplyRate.rate) : 0; - const borrowApyValue = borrowRate?.rate ? parseFloat(borrowRate.rate) : 0; - - result.rates.supplyApy.push({ x: timestamp, y: !isNaN(supplyApyValue) ? supplyApyValue : 0 }); - result.rates.borrowApy.push({ x: timestamp, y: !isNaN(borrowApyValue) ? borrowApyValue : 0 }); - - // Placeholders for data not directly available in subgraph snapshots - result.rates.rateAtUTarget.push({ x: timestamp, y: 0 }); - result.rates.utilization.push({ x: timestamp, y: 0 }); - - // no USD values available in subgraph - const finalSupplyUsd = 0; - const finalBorrowUsd = 0; - const finalLiquidityUsd = 0; - - const supplyNative = BigInt(snapshot.inputTokenBalance ?? '0'); - const borrowNative = BigInt(snapshot.variableBorrowedTokenBalance ?? '0'); - const liquidityNative = supplyNative - borrowNative; - - result.volumes.supplyAssetsUsd.push({ x: timestamp, y: finalSupplyUsd }); - result.volumes.borrowAssetsUsd.push({ x: timestamp, y: finalBorrowUsd }); - result.volumes.liquidityAssetsUsd.push({ x: timestamp, y: finalLiquidityUsd }); - - // Process Native asset amounts - - - // Convert BigInt to number for TimeseriesDataPoint. - // Warning: Potential precision loss for very large numbers. - // Consider formatting units in the UI instead if precision is critical. - result.volumes.supplyAssets.push({ x: timestamp, y: Number(supplyNative) }); - result.volumes.borrowAssets.push({ x: timestamp, y: Number(borrowNative) }); - result.volumes.liquidityAssets.push({ x: timestamp, y: Number(liquidityNative) }); - }); - - // Sort data by timestamp - Object.values(result.rates).forEach((arr: TimeseriesDataPoint[]) => arr.sort((a, b) => a.x - b.x)); - Object.values(result.volumes).forEach((arr: TimeseriesDataPoint[]) => arr.sort((a, b) => a.x - b.x)); - - return result; -}; - - -export const useSubgraphMarketHistoricalData = ( - marketId: string | undefined, - network: SupportedNetworks | undefined, - timeRange: TimeseriesOptions | undefined, -) => { - return useQuery({ - queryKey: ['subgraphMarketHistoricalData', marketId, network, timeRange?.startTimestamp, timeRange?.endTimestamp, timeRange?.interval], - queryFn: async (): Promise => { - if (!marketId || !network || !timeRange || !timeRange.startTimestamp || !timeRange.endTimestamp) { - return createEmptyHistoricalStructure(); - } - - const subgraphApiUrl = getSubgraphUrl(network); - if (!subgraphApiUrl) { - console.error(`Subgraph URL for network ${network} is not defined.`); - return createEmptyHistoricalStructure(); - } - - try { - const variables = { - marketId: marketId.toLowerCase(), - startTimestamp: String(timeRange.startTimestamp), - endTimestamp: String(timeRange.endTimestamp), - }; - - const response = await subgraphGraphqlFetcher( - subgraphApiUrl, - marketHourlySnapshotsQuery, - variables - ); - - if (!response.data || !response.data.marketHourlySnapshots) { - return createEmptyHistoricalStructure(); - } - - return transformSubgraphSnapshots(response.data.marketHourlySnapshots); - - } catch (error) { - console.error("Error fetching or processing subgraph historical data:", error); - return createEmptyHistoricalStructure(); - } - }, - enabled: !!marketId && !!network && !!timeRange && !!timeRange.startTimestamp && !!timeRange.endTimestamp, - staleTime: 1000 * 60 * 5, // 5 minutes - placeholderData: (previousData) => previousData ?? createEmptyHistoricalStructure(), - retry: 1, - }); -}; \ No newline at end of file From a4439d18a5f138337fecbdce4d76103df4b8b870 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sun, 20 Apr 2025 23:42:45 +0800 Subject: [PATCH 10/11] chore: lint --- app/market/[chainId]/[marketid]/RateChart.tsx | 69 +++++------ .../[chainId]/[marketid]/VolumeChart.tsx | 55 +++------ app/market/[chainId]/[marketid]/content.tsx | 116 +++++++++--------- src/config/dataSources.ts | 20 ++- src/data-sources/morpho-api/fetchers.ts | 12 +- src/data-sources/morpho-api/historical.ts | 68 ++++++---- src/data-sources/morpho-api/market.ts | 11 +- src/data-sources/subgraph/fetchers.ts | 17 +-- src/data-sources/subgraph/historical.ts | 55 +++++---- src/data-sources/subgraph/market.ts | 81 ++++++------ src/graphql/morpho-subgraph-queries.ts | 2 +- src/hooks/useMarketData.ts | 15 +-- src/hooks/useMarketHistoricalData.ts | 23 ++-- src/utils/subgraph-types.ts | 2 +- src/utils/subgraph-urls.ts | 2 +- 15 files changed, 275 insertions(+), 273 deletions(-) diff --git a/app/market/[chainId]/[marketid]/RateChart.tsx b/app/market/[chainId]/[marketid]/RateChart.tsx index 632c3036..0422ff7e 100644 --- a/app/market/[chainId]/[marketid]/RateChart.tsx +++ b/app/market/[chainId]/[marketid]/RateChart.tsx @@ -1,6 +1,6 @@ /* eslint-disable react/no-unstable-nested-components */ -import React, { useCallback, useState } from 'react'; +import React, { useState } from 'react'; import { Card, CardHeader, CardBody } from '@nextui-org/card'; import { Progress } from '@nextui-org/progress'; import { @@ -16,31 +16,25 @@ import { import ButtonGroup from '@/components/ButtonGroup'; import { Spinner } from '@/components/common/Spinner'; import { CHART_COLORS } from '@/constants/chartColors'; -import { - TimeseriesDataPoint, - MarketHistoricalData, - Market, - TimeseriesOptions, -} from '@/utils/types'; +import { MarketRates } from '@/data-sources/morpho-api/historical'; +import { TimeseriesDataPoint, Market, TimeseriesOptions } from '@/utils/types'; type RateChartProps = { - historicalData: MarketHistoricalData['rates'] | undefined; + historicalData: MarketRates | undefined; market: Market; isLoading: boolean; - apyTimeframe: '1day' | '7day' | '30day'; - setApyTimeframe: (timeframe: '1day' | '7day' | '30day') => void; - setTimeRangeAndRefetch: (days: number, type: 'rate') => void; - rateTimeRange: TimeseriesOptions; + selectedTimeframe: '1d' | '7d' | '30d'; + selectedTimeRange: TimeseriesOptions; + handleTimeframeChange: (timeframe: '1d' | '7d' | '30d') => void; }; function RateChart({ historicalData, market, isLoading, - apyTimeframe, - setApyTimeframe, - setTimeRangeAndRefetch, - rateTimeRange, + selectedTimeframe, + selectedTimeRange, + handleTimeframeChange, }: RateChartProps) { const [visibleLines, setVisibleLines] = useState({ supplyApy: true, @@ -69,7 +63,9 @@ function RateChart({ const getAverageApyValue = (type: 'supply' | 'borrow') => { if (!historicalData) return 0; const data = type === 'supply' ? historicalData.supplyApy : historicalData.borrowApy; - return data.length > 0 ? data.reduce((sum, point) => sum + point.y, 0) / data.length : 0; + return data.length > 0 + ? data.reduce((sum: number, point: TimeseriesDataPoint) => sum + point.y, 0) / data.length + : 0; }; const getCurrentRateAtUTargetValue = () => { @@ -77,10 +73,12 @@ function RateChart({ }; const getAverageRateAtUTargetValue = () => { - if (!historicalData?.rateAtUTarget) return 0; + if (!historicalData?.rateAtUTarget || historicalData.rateAtUTarget.length === 0) return 0; return ( - historicalData.rateAtUTarget.reduce((sum, point) => sum + point.y, 0) / - historicalData.rateAtUTarget.length + historicalData.rateAtUTarget.reduce( + (sum: number, point: TimeseriesDataPoint) => sum + point.y, + 0, + ) / historicalData.rateAtUTarget.length ); }; @@ -89,44 +87,37 @@ function RateChart({ }; const getAverageUtilizationRate = () => { - if (!historicalData?.utilization) return 0; + if (!historicalData?.utilization || historicalData.utilization.length === 0) return 0; return ( - historicalData.utilization.reduce((sum, point) => sum + point.y, 0) / - historicalData.utilization.length + historicalData.utilization.reduce( + (sum: number, point: TimeseriesDataPoint) => sum + point.y, + 0, + ) / historicalData.utilization.length ); }; const formatTime = (unixTime: number) => { const date = new Date(unixTime * 1000); - if (rateTimeRange.endTimestamp - rateTimeRange.startTimestamp <= 86400) { + if (selectedTimeRange.endTimestamp - selectedTimeRange.startTimestamp <= 86400) { return date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }); } return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); }; const timeframeOptions = [ - { key: '1day', label: '1D', value: '1day' }, - { key: '7day', label: '7D', value: '7day' }, - { key: '30day', label: '30D', value: '30day' }, + { key: '1d', label: '1D', value: '1d' }, + { key: '7d', label: '7D', value: '7d' }, + { key: '30d', label: '30D', value: '30d' }, ]; - const handleTimeframeChange = useCallback( - (value: string) => { - setApyTimeframe(value as '1day' | '7day' | '30day'); - const days = value === '1day' ? 1 : value === '7day' ? 7 : 30; - setTimeRangeAndRefetch(days, 'rate'); - }, - [setApyTimeframe, setTimeRangeAndRefetch], - ); - return ( handleTimeframeChange(value as '1d' | '7d' | '30d')} size="sm" variant="default" /> @@ -285,7 +276,7 @@ function RateChart({

Historical Averages{' '} - ({apyTimeframe}) + ({selectedTimeframe})

{isLoading ? (
diff --git a/app/market/[chainId]/[marketid]/VolumeChart.tsx b/app/market/[chainId]/[marketid]/VolumeChart.tsx index c58976de..53a54025 100644 --- a/app/market/[chainId]/[marketid]/VolumeChart.tsx +++ b/app/market/[chainId]/[marketid]/VolumeChart.tsx @@ -1,6 +1,6 @@ /* eslint-disable react/no-unstable-nested-components */ -import React, { useCallback, useState } from 'react'; +import React, { useState } from 'react'; import { Card, CardHeader, CardBody } from '@nextui-org/card'; import { AreaChart, @@ -16,24 +16,19 @@ import { formatUnits } from 'viem'; import ButtonGroup from '@/components/ButtonGroup'; import { Spinner } from '@/components/common/Spinner'; import { CHART_COLORS } from '@/constants/chartColors'; +import { MarketVolumes } from '@/data-sources/morpho-api/historical'; import { formatReadable } from '@/utils/balance'; -import { - TimeseriesDataPoint, - MarketHistoricalData, - Market, - TimeseriesOptions, -} from '@/utils/types'; +import { TimeseriesDataPoint, Market, TimeseriesOptions } from '@/utils/types'; type VolumeChartProps = { - historicalData: MarketHistoricalData['volumes'] | undefined; + historicalData: MarketVolumes | undefined; market: Market; isLoading: boolean; volumeView: 'USD' | 'Asset'; - volumeTimeframe: '1day' | '7day' | '30day'; - setVolumeTimeframe: (timeframe: '1day' | '7day' | '30day') => void; - setTimeRangeAndRefetch: (days: number, type: 'volume') => void; - volumeTimeRange: TimeseriesOptions; setVolumeView: (view: 'USD' | 'Asset') => void; + selectedTimeframe: '1d' | '7d' | '30d'; + selectedTimeRange: TimeseriesOptions; + handleTimeframeChange: (timeframe: '1d' | '7d' | '30d') => void; }; function VolumeChart({ @@ -41,11 +36,10 @@ function VolumeChart({ market, isLoading, volumeView, - volumeTimeframe, - setVolumeTimeframe, - setTimeRangeAndRefetch, - volumeTimeRange, setVolumeView, + selectedTimeframe, + selectedTimeRange, + handleTimeframeChange, }: VolumeChartProps) { const formatYAxis = (value: number) => { if (volumeView === 'USD') { @@ -57,7 +51,7 @@ function VolumeChart({ const formatTime = (unixTime: number) => { const date = new Date(unixTime * 1000); - if (volumeTimeRange.endTimestamp - volumeTimeRange.startTimestamp <= 24 * 60 * 60) { + if (selectedTimeRange.endTimestamp - selectedTimeRange.startTimestamp <= 24 * 60 * 60) { return date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }); } return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); @@ -77,8 +71,8 @@ function VolumeChart({ return supplyData .map((point: TimeseriesDataPoint, index: number) => { // Get corresponding points from other series - const borrowPoint = borrowData[index]; - const liquidityPoint = liquidityData[index]; + const borrowPoint: TimeseriesDataPoint | undefined = borrowData[index]; + const liquidityPoint: TimeseriesDataPoint | undefined = liquidityData[index]; // Convert values based on view type const supplyValue = @@ -144,7 +138,7 @@ function VolumeChart({ : historicalData?.[`${type}Assets`]; if (!data || data.length === 0) return 0; const sum = data.reduce( - (acc, point) => + (acc: number, point: TimeseriesDataPoint) => acc + Number( volumeView === 'USD' ? point.y : formatUnits(BigInt(point.y), market.loanAsset.decimals), @@ -160,20 +154,11 @@ function VolumeChart({ ]; const timeframeOptions = [ - { key: '1day', label: '1D', value: '1day' }, - { key: '7day', label: '7D', value: '7day' }, - { key: '30day', label: '30D', value: '30day' }, + { key: '1d', label: '1D', value: '1d' }, + { key: '7d', label: '7D', value: '7d' }, + { key: '30d', label: '30D', value: '30d' }, ]; - const handleTimeframeChange = useCallback( - (value: string) => { - setVolumeTimeframe(value as '1day' | '7day' | '30day'); - const days = value === '1day' ? 1 : value === '7day' ? 7 : 30; - setTimeRangeAndRefetch(days, 'volume'); - }, - [setVolumeTimeframe, setTimeRangeAndRefetch], - ); - const [visibleLines, setVisibleLines] = useState({ supply: true, borrow: true, @@ -194,8 +179,8 @@ function VolumeChart({ /> handleTimeframeChange(value as '1d' | '7d' | '30d')} size="sm" variant="default" /> @@ -343,7 +328,7 @@ function VolumeChart({

Historical Averages{' '} - ({volumeTimeframe}) + ({selectedTimeframe})

{isLoading ? (
diff --git a/app/market/[chainId]/[marketid]/content.tsx b/app/market/[chainId]/[marketid]/content.tsx index 2d865e1a..69f94499 100644 --- a/app/market/[chainId]/[marketid]/content.tsx +++ b/app/market/[chainId]/[marketid]/content.tsx @@ -16,6 +16,8 @@ import Header from '@/components/layout/header/Header'; import OracleVendorBadge from '@/components/OracleVendorBadge'; import { SupplyModalV2 } from '@/components/SupplyModalV2'; import { TokenIcon } from '@/components/TokenIcon'; +import { useMarketData } from '@/hooks/useMarketData'; +import { useMarketHistoricalData } from '@/hooks/useMarketHistoricalData'; import { useOraclePrice } from '@/hooks/useOraclePrice'; import useUserPositions from '@/hooks/useUserPosition'; import MORPHO_LOGO from '@/imgs/tokens/morpho.svg'; @@ -29,11 +31,32 @@ import { PositionStats } from './components/PositionStats'; import { SuppliesTable } from './components/SuppliesTable'; import RateChart from './RateChart'; import VolumeChart from './VolumeChart'; -import { useMarketData } from '@/hooks/useMarketData'; -import { useMarketHistoricalData } from '@/hooks/useMarketHistoricalData'; const NOW = Math.floor(Date.now() / 1000); -const WEEK_IN_SECONDS = 7 * 24 * 60 * 60; +const DAY_IN_SECONDS = 24 * 60 * 60; +const WEEK_IN_SECONDS = 7 * DAY_IN_SECONDS; + +// Helper to calculate time range based on timeframe string +const calculateTimeRange = (timeframe: '1d' | '7d' | '30d'): TimeseriesOptions => { + const endTimestamp = NOW; + let startTimestamp; + let interval: TimeseriesOptions['interval'] = 'HOUR'; + switch (timeframe) { + case '1d': + startTimestamp = endTimestamp - DAY_IN_SECONDS; + break; + case '30d': + startTimestamp = endTimestamp - 30 * DAY_IN_SECONDS; + // Use DAY interval for longer ranges if desired, adjust as needed + interval = 'DAY'; + break; + case '7d': + default: + startTimestamp = endTimestamp - WEEK_IN_SECONDS; + break; + } + return { startTimestamp, endTimestamp, interval }; +}; function MarketContent() { // 1. Get URL params and router first @@ -45,31 +68,27 @@ function MarketContent() { const network = Number(chainId as string) as SupportedNetworks; const networkImg = getNetworkImg(network); - // 3. All useState hooks grouped together + // 3. Consolidated state const [showSupplyModal, setShowSupplyModal] = useState(false); const [showBorrowModal, setShowBorrowModal] = useState(false); - const [apyTimeframe, setApyTimeframe] = useState<'1day' | '7day' | '30day'>('7day'); - const [volumeTimeframe, setVolumeTimeframe] = useState<'1day' | '7day' | '30day'>('7day'); + const [selectedTimeframe, setSelectedTimeframe] = useState<'1d' | '7d' | '30d'>('7d'); + const [selectedTimeRange, setSelectedTimeRange] = useState( + calculateTimeRange('7d'), // Initialize based on default timeframe + ); const [volumeView, setVolumeView] = useState<'USD' | 'Asset'>('USD'); - const [rateTimeRange, setRateTimeRange] = useState({ - startTimestamp: NOW - WEEK_IN_SECONDS, - endTimestamp: NOW, - interval: 'HOUR', - }); - const [volumeTimeRange, setVolumeTimeRange] = useState({ - startTimestamp: NOW - WEEK_IN_SECONDS, - endTimestamp: NOW, - interval: 'HOUR', - }); - - const {data: market, isLoading: isMarketLoading, error: marketError} = useMarketData(marketid as string, network); + // 4. Data fetching hooks - use unified time range + const { + data: market, + isLoading: isMarketLoading, + error: marketError, + } = useMarketData(marketid as string, network); const { data: historicalData, isLoading: isHistoricalLoading, - refetch: refetchHistoricalData, - } = useMarketHistoricalData(marketid as string, network, rateTimeRange); + // No need for manual refetch on time change, queryKey handles it + } = useMarketHistoricalData(marketid as string, network, selectedTimeRange); // Use selectedTimeRange // 5. Oracle price hook - safely handle undefined market const { price: oraclePrice } = useOraclePrice({ @@ -94,25 +113,12 @@ function MarketContent() { return formatUnits(adjusted, 36); }, [oraclePrice, market]); - const setTimeRangeAndRefetch = useCallback( - (days: number, type: 'rate' | 'volume') => { - const endTimestamp = Math.floor(Date.now() / 1000); - const startTimestamp = endTimestamp - days * 24 * 60 * 60; - const newTimeRange = { - startTimestamp, - endTimestamp, - interval: days > 30 ? 'DAY' : 'HOUR', - } as TimeseriesOptions; - - if (type === 'rate') { - setRateTimeRange(newTimeRange); - } else { - setVolumeTimeRange(newTimeRange); - } - void refetchHistoricalData(); - }, - [refetchHistoricalData, setRateTimeRange, setVolumeTimeRange], - ); + // Unified handler for timeframe changes + const handleTimeframeChange = useCallback((timeframe: '1d' | '7d' | '30d') => { + setSelectedTimeframe(timeframe); + setSelectedTimeRange(calculateTimeRange(timeframe)); + // No explicit refetch needed, change in selectedTimeRange (part of queryKey) triggers it + }, []); const handleBackToMarkets = useCallback(() => { const currentParams = searchParams.toString(); @@ -135,14 +141,16 @@ function MarketContent() { } if (!market) { - return (<> -
-
-
- -
-
- ); + return ( + <> +
+
+
+ +
+
+ + ); } // 8. Derived values that depend on market data @@ -342,12 +350,11 @@ function MarketContent() { @@ -355,11 +362,10 @@ function MarketContent() {

Activities

diff --git a/src/config/dataSources.ts b/src/config/dataSources.ts index 26c20b20..35064c1e 100644 --- a/src/config/dataSources.ts +++ b/src/config/dataSources.ts @@ -4,13 +4,10 @@ import { SupportedNetworks } from '@/utils/networks'; * Determines the primary data source for market details based on the network. */ export const getMarketDataSource = (network: SupportedNetworks): 'morpho' | 'subgraph' => { - // Use Subgraph for specific networks, Morpho API for others switch (network) { - // Networks using subgraph - case SupportedNetworks.Mainnet: - case SupportedNetworks.Base: - return 'subgraph'; - // Networks using morpho-api + // case SupportedNetworks.Mainnet: + // case SupportedNetworks.Base: + // return 'subgraph'; default: return 'morpho'; // Default to Morpho API } @@ -21,9 +18,8 @@ export const getMarketDataSource = (network: SupportedNetworks): 'morpho' | 'sub * Assumes only Morpho API provides this, unless explicitly excluded. */ export const getHistoricalDataSource = (network: SupportedNetworks): 'morpho' | 'subgraph' => { - // Networks excluded from Morpho API historical data - // Add networks here if they don't support historical data via Morpho API - - // Assume other networks have historical data via Morpho API - return 'morpho'; -}; \ No newline at end of file + switch (network) { + default: + return 'morpho'; + } +}; diff --git a/src/data-sources/morpho-api/fetchers.ts b/src/data-sources/morpho-api/fetchers.ts index 09105c6f..09be399a 100644 --- a/src/data-sources/morpho-api/fetchers.ts +++ b/src/data-sources/morpho-api/fetchers.ts @@ -18,11 +18,15 @@ export const morphoGraphqlFetcher = async >( const result = (await response.json()) as T; // Check for GraphQL errors - if ('errors' in result && Array.isArray((result as any).errors) && (result as any).errors.length > 0) { + if ( + 'errors' in result && + Array.isArray((result as any).errors) && + (result as any).errors.length > 0 + ) { // Log the full error for debugging - console.error('Morpho API GraphQL Error:', (result as any).errors); - throw new Error((result as any).errors[0].message || 'Unknown GraphQL error from Morpho API'); + console.error('Morpho API GraphQL Error:', result.errors); + throw new Error('Unknown GraphQL error from Morpho API'); } return result; -}; \ No newline at end of file +}; diff --git a/src/data-sources/morpho-api/historical.ts b/src/data-sources/morpho-api/historical.ts index 66886b27..bbdd5558 100644 --- a/src/data-sources/morpho-api/historical.ts +++ b/src/data-sources/morpho-api/historical.ts @@ -1,6 +1,6 @@ +import { marketHistoricalDataQuery } from '@/graphql/morpho-api-queries'; import { SupportedNetworks } from '@/utils/networks'; import { TimeseriesOptions, Market, TimeseriesDataPoint } from '@/utils/types'; -import { marketHistoricalDataQuery } from '@/graphql/morpho-api-queries'; import { morphoGraphqlFetcher } from './fetchers'; // --- Types related to Historical Data --- @@ -50,52 +50,68 @@ export const fetchMorphoMarketHistoricalData = async ( options: TimeseriesOptions, ): Promise => { try { - const response = await morphoGraphqlFetcher(marketHistoricalDataQuery, { - uniqueKey, - options, - chainId: network, - }); + const response = await morphoGraphqlFetcher( + marketHistoricalDataQuery, + { + uniqueKey, + options, + chainId: network, + }, + ); 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)); + 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 (!historicalState || Object.keys(historicalState).length === 0 || !historicalState.supplyApy) { // Example check - console.warn("Historical state not found, empty, or missing essential data in Morpho API response for", uniqueKey); + if ( + !historicalState || + Object.keys(historicalState).length === 0 || + !historicalState.supplyApy + ) { + // Example check + console.warn( + 'Historical state not found, empty, or missing essential data in Morpho API response for', + uniqueKey, + ); return null; } // Construct the expected nested structure for the hook // Assume API returns *some* data if historicalState is valid const rates: MarketRates = { - supplyApy: historicalState.supplyApy ?? [], - borrowApy: historicalState.borrowApy ?? [], - rateAtUTarget: historicalState.rateAtUTarget ?? [], - utilization: historicalState.utilization ?? [], + supplyApy: historicalState.supplyApy ?? [], + borrowApy: historicalState.borrowApy ?? [], + rateAtUTarget: historicalState.rateAtUTarget ?? [], + utilization: historicalState.utilization ?? [], }; const volumes: MarketVolumes = { - supplyAssetsUsd: historicalState.supplyAssetsUsd ?? [], - borrowAssetsUsd: historicalState.borrowAssetsUsd ?? [], - liquidityAssetsUsd: historicalState.liquidityAssetsUsd ?? [], - supplyAssets: historicalState.supplyAssets ?? [], - borrowAssets: historicalState.borrowAssets ?? [], - liquidityAssets: historicalState.liquidityAssets ?? [], + supplyAssetsUsd: historicalState.supplyAssetsUsd ?? [], + borrowAssetsUsd: historicalState.borrowAssetsUsd ?? [], + liquidityAssetsUsd: historicalState.liquidityAssetsUsd ?? [], + supplyAssets: historicalState.supplyAssets ?? [], + borrowAssets: historicalState.borrowAssets ?? [], + liquidityAssets: historicalState.liquidityAssets ?? [], }; // Sort each timeseries array by timestamp (x-axis) ascending const sortByTimestamp = (a: TimeseriesDataPoint, b: TimeseriesDataPoint) => a.x - b.x; - Object.values(rates).forEach(arr => arr.sort(sortByTimestamp)); - Object.values(volumes).forEach(arr => arr.sort(sortByTimestamp)); + Object.values(rates).forEach((arr) => arr.sort(sortByTimestamp)); + Object.values(volumes).forEach((arr) => arr.sort(sortByTimestamp)); return { rates, volumes }; - } catch (error) { - console.error("Error fetching Morpho historical data:", error); - return null; // Return null on error + console.error('Error fetching Morpho historical data:', error); + return null; // Return null on error } -}; \ No newline at end of file +}; diff --git a/src/data-sources/morpho-api/market.ts b/src/data-sources/morpho-api/market.ts index 303dff0c..efddd1e6 100644 --- a/src/data-sources/morpho-api/market.ts +++ b/src/data-sources/morpho-api/market.ts @@ -1,7 +1,7 @@ -import { SupportedNetworks } from '@/utils/networks'; -import { getMarketWarningsWithDetail } from '@/utils/warnings'; import { marketDetailQuery } from '@/graphql/morpho-api-queries'; +import { SupportedNetworks } from '@/utils/networks'; import { Market } from '@/utils/types'; +import { getMarketWarningsWithDetail } from '@/utils/warnings'; import { morphoGraphqlFetcher } from './fetchers'; // Removed historical types (MarketRates, MarketVolumes, etc.) @@ -24,7 +24,10 @@ const processMarketData = (market: Market): Market => { }; // Fetcher for market details from Morpho API -export const fetchMorphoMarket = async (uniqueKey: string, network: SupportedNetworks): Promise => { +export const fetchMorphoMarket = async ( + uniqueKey: string, + network: SupportedNetworks, +): Promise => { const response = await morphoGraphqlFetcher(marketDetailQuery, { uniqueKey, chainId: network, @@ -33,4 +36,4 @@ export const fetchMorphoMarket = async (uniqueKey: string, network: SupportedNet throw new Error('Market data not found in Morpho API response'); } return processMarketData(response.data.marketByUniqueKey); -}; \ No newline at end of file +}; diff --git a/src/data-sources/subgraph/fetchers.ts b/src/data-sources/subgraph/fetchers.ts index 1228269a..c23f00b3 100644 --- a/src/data-sources/subgraph/fetchers.ts +++ b/src/data-sources/subgraph/fetchers.ts @@ -1,6 +1,3 @@ -import { getSubgraphUrl } from '@/utils/subgraph-urls'; - -// Generic fetcher for Subgraph API export const subgraphGraphqlFetcher = async ( apiUrl: string, // Subgraph URL can vary query: string, @@ -19,12 +16,16 @@ export const subgraphGraphqlFetcher = async ( const result = (await response.json()) as T; - // Check for GraphQL errors in the Subgraph response - if ('errors' in result && Array.isArray((result as any).errors) && (result as any).errors.length > 0) { + // Check for GraphQL errors + if ( + 'errors' in result && + Array.isArray((result as any).errors) && + (result as any).errors.length > 0 + ) { // Log the full error for debugging - console.error('Subgraph API GraphQL Error:', (result as any).errors); - throw new Error((result as any).errors[0].message || 'Unknown GraphQL error from Subgraph API'); + console.error('Subgraph API GraphQL Error:', result.errors); + throw new Error('GraphQL error from Subgraph API'); } return result; -}; \ No newline at end of file +}; diff --git a/src/data-sources/subgraph/historical.ts b/src/data-sources/subgraph/historical.ts index f2116f89..6f92a7c3 100644 --- a/src/data-sources/subgraph/historical.ts +++ b/src/data-sources/subgraph/historical.ts @@ -1,22 +1,19 @@ +import { marketHourlySnapshotsQuery } from '@/graphql/morpho-subgraph-queries'; import { SupportedNetworks } from '@/utils/networks'; import { getSubgraphUrl } from '@/utils/subgraph-urls'; -import { - TimeseriesOptions, - TimeseriesDataPoint, -} from '@/utils/types'; // Assuming TimeseriesDataPoint is exported -import { marketHourlySnapshotsQuery } from '@/graphql/morpho-subgraph-queries'; -import { subgraphGraphqlFetcher } from './fetchers'; +import { TimeseriesOptions, TimeseriesDataPoint } from '@/utils/types'; // Assuming TimeseriesDataPoint is exported import { HistoricalDataSuccessResult, MarketRates, MarketVolumes } from '../morpho-api/historical'; // Updated path & added imports +import { subgraphGraphqlFetcher } from './fetchers'; // --- Subgraph Specific Types (Copied from useSubgraphMarketHistoricalData.ts) --- -interface SubgraphInterestRate { +type SubgraphInterestRate = { id: string; rate: string; side: 'LENDER' | 'BORROWER'; type: 'VARIABLE' | 'STABLE' | 'FIXED'; -} +}; -interface SubgraphMarketHourlySnapshot { +type SubgraphMarketHourlySnapshot = { id: string; timestamp: string; market: { @@ -31,18 +28,18 @@ interface SubgraphMarketHourlySnapshot { hourlyBorrowUSD: string; outputTokenSupply: string | null; variableBorrowedTokenBalance: string | null; -} +}; -interface SubgraphMarketHourlySnapshotQueryResponse { +type SubgraphMarketHourlySnapshotQueryResponse = { data: { marketHourlySnapshots: SubgraphMarketHourlySnapshot[]; }; -} +}; // --- End Subgraph Specific Types --- // Transformation function (simplified) const transformSubgraphSnapshotsToHistoricalResult = ( - snapshots: SubgraphMarketHourlySnapshot[] // Expect non-empty array here + snapshots: SubgraphMarketHourlySnapshot[], // Expect non-empty array here ): HistoricalDataSuccessResult => { const rates: MarketRates = { supplyApy: [] as TimeseriesDataPoint[], @@ -60,16 +57,16 @@ const transformSubgraphSnapshotsToHistoricalResult = ( }; // No need to check for !snapshots here, handled by caller - snapshots.forEach(snapshot => { + snapshots.forEach((snapshot) => { const timestamp = parseInt(snapshot.timestamp, 10); if (isNaN(timestamp)) { - console.warn("Skipping snapshot due to invalid timestamp:", snapshot); + console.warn('Skipping snapshot due to invalid timestamp:', snapshot); return; } const snapshotRates = Array.isArray(snapshot.rates) ? snapshot.rates : []; - const supplyRate = snapshotRates.find(r => r?.side === 'LENDER'); - const borrowRate = snapshotRates.find(r => r?.side === 'BORROWER'); + const supplyRate = snapshotRates.find((r) => r?.side === 'LENDER'); + const borrowRate = snapshotRates.find((r) => r?.side === 'BORROWER'); const supplyApyValue = supplyRate?.rate ? parseFloat(supplyRate.rate) : 0; const borrowApyValue = borrowRate?.rate ? parseFloat(borrowRate.rate) : 0; @@ -93,8 +90,12 @@ const transformSubgraphSnapshotsToHistoricalResult = ( }); // Sort data by timestamp - Object.values(rates).forEach((arr: TimeseriesDataPoint[]) => arr.sort((a: TimeseriesDataPoint, b: TimeseriesDataPoint) => a.x - b.x)); - Object.values(volumes).forEach((arr: TimeseriesDataPoint[]) => arr.sort((a: TimeseriesDataPoint, b: TimeseriesDataPoint) => a.x - b.x)); + Object.values(rates).forEach((arr: TimeseriesDataPoint[]) => + arr.sort((a: TimeseriesDataPoint, b: TimeseriesDataPoint) => a.x - b.x), + ); + Object.values(volumes).forEach((arr: TimeseriesDataPoint[]) => + arr.sort((a: TimeseriesDataPoint, b: TimeseriesDataPoint) => a.x - b.x), + ); return { rates, volumes }; }; @@ -104,7 +105,8 @@ export const fetchSubgraphMarketHistoricalData = async ( marketId: string, network: SupportedNetworks, timeRange: TimeseriesOptions, -): Promise => { // Updated return type +): Promise => { + // Updated return type if (!timeRange.startTimestamp || !timeRange.endTimestamp) { console.warn('Subgraph historical fetch requires start and end timestamps.'); @@ -127,20 +129,23 @@ export const fetchSubgraphMarketHistoricalData = async ( const response = await subgraphGraphqlFetcher( subgraphApiUrl, marketHourlySnapshotsQuery, - variables + variables, ); // If no data or empty snapshots array, return null - if (!response.data || !response.data.marketHourlySnapshots || response.data.marketHourlySnapshots.length === 0) { + if ( + !response.data || + !response.data.marketHourlySnapshots || + response.data.marketHourlySnapshots.length === 0 + ) { console.warn(`No subgraph historical snapshots found for market ${marketId}`); return null; } // Pass the guaranteed non-empty array to the transformer return transformSubgraphSnapshotsToHistoricalResult(response.data.marketHourlySnapshots); - } catch (error) { - console.error("Error fetching or processing subgraph historical data:", error); + console.error('Error fetching or processing subgraph historical data:', error); return null; // Return null on error } -}; \ No newline at end of file +}; diff --git a/src/data-sources/subgraph/market.ts b/src/data-sources/subgraph/market.ts index 14ef00f0..30c86c15 100644 --- a/src/data-sources/subgraph/market.ts +++ b/src/data-sources/subgraph/market.ts @@ -1,13 +1,9 @@ +import { Address } from 'viem'; +import { marketQuery as subgraphMarketQuery } from '@/graphql/morpho-subgraph-queries'; // Assuming query is here import { SupportedNetworks } from '@/utils/networks'; +import { SubgraphMarket, SubgraphMarketQueryResponse, SubgraphToken } from '@/utils/subgraph-types'; import { getSubgraphUrl } from '@/utils/subgraph-urls'; import { WarningWithDetail, MorphoChainlinkOracleData, Market } from '@/utils/types'; -import { marketQuery as subgraphMarketQuery } from '@/graphql/morpho-subgraph-queries'; // Assuming query is here -import { - SubgraphMarket, - SubgraphMarketQueryResponse, - SubgraphToken, -} from '@/utils/subgraph-types'; -import { Address } from 'viem'; import { subgraphGraphqlFetcher } from './fetchers'; // Helper to safely parse BigDecimal/BigInt strings @@ -21,19 +17,18 @@ const safeParseFloat = (value: string | null | undefined): number => { }; const safeParseInt = (value: string | null | undefined): number => { - if (value === null || value === undefined) return 0; - try { - return parseInt(value, 10); - } catch { - return 0; - } -} + if (value === null || value === undefined) return 0; + try { + return parseInt(value, 10); + } catch { + return 0; + } +}; const transformSubgraphMarketToMarket = ( - subgraphMarket: Partial, - network: SupportedNetworks + subgraphMarket: Partial, + network: SupportedNetworks, ): Market => { - const marketId = subgraphMarket.id ?? ''; const lltv = subgraphMarket.lltv ?? '0'; const irmAddress = subgraphMarket.irm ?? '0x'; @@ -74,11 +69,12 @@ const transformSubgraphMarketToMarket = ( const totalBorrowNum = safeParseFloat(borrowAssets); const utilization = totalSupplyNum > 0 ? (totalBorrowNum / totalSupplyNum) * 100 : 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); + 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 liquidityAssets = (BigInt(supplyAssets) - BigInt(borrowAssets)).toString(); - const liquidityAssetsUsd = safeParseFloat(totalDepositBalanceUSD) - safeParseFloat(totalBorrowBalanceUSD); + const liquidityAssetsUsd = + safeParseFloat(totalDepositBalanceUSD) - safeParseFloat(totalBorrowBalanceUSD); const warningsWithDetail: WarningWithDetail[] = []; // Subgraph doesn't provide warnings directly @@ -119,7 +115,7 @@ const transformSubgraphMarketToMarket = ( warnings: [], // Subgraph doesn't provide warnings warningsWithDetail: warningsWithDetail, oracle: { - data: defaultOracleData, // Placeholder oracle data + data: defaultOracleData, // Placeholder oracle data }, isProtectedByLiquidationBots: false, // Not available from subgraph badDebt: undefined, // Not available from subgraph @@ -131,31 +127,30 @@ const transformSubgraphMarketToMarket = ( // Fetcher for market details from Subgraph export const fetchSubgraphMarket = async ( - uniqueKey: string, - network: SupportedNetworks + uniqueKey: string, + network: SupportedNetworks, ): Promise => { + const subgraphApiUrl = getSubgraphUrl(network); - 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.`); - } + if (!subgraphApiUrl) { + console.error(`Subgraph URL for network ${network} is not defined.`); + throw new Error(`Subgraph URL for network ${network} is not defined.`); + } - const response = await subgraphGraphqlFetcher( - subgraphApiUrl, - subgraphMarketQuery, - { - id: uniqueKey.toLowerCase() // Ensure ID is lowercase for subgraph - } - ); + const response = await subgraphGraphqlFetcher( + subgraphApiUrl, + subgraphMarketQuery, + { + id: uniqueKey.toLowerCase(), // Ensure ID is lowercase for subgraph + }, + ); - const marketData = response.data.market; + const marketData = response.data.market; - if (!marketData) { - console.warn(`Market with key ${uniqueKey} not found in Subgraph response.`); - return null; // Return null if not found, hook can handle this - } + if (!marketData) { + console.warn(`Market with key ${uniqueKey} not found in Subgraph response.`); + return null; // Return null if not found, hook can handle this + } - return transformSubgraphMarketToMarket(marketData, network); -}; \ No newline at end of file + return transformSubgraphMarketToMarket(marketData, network); +}; diff --git a/src/graphql/morpho-subgraph-queries.ts b/src/graphql/morpho-subgraph-queries.ts index b358f93c..98d0d332 100644 --- a/src/graphql/morpho-subgraph-queries.ts +++ b/src/graphql/morpho-subgraph-queries.ts @@ -144,4 +144,4 @@ export const marketHourlySnapshotsQuery = ` ${marketHourlySnapshotFragment} ${tokenFragment} # Ensure TokenFields fragment is included `; -// --- End Added Section --- \ No newline at end of file +// --- End Added Section --- diff --git a/src/hooks/useMarketData.ts b/src/hooks/useMarketData.ts index 14e5022c..0668422a 100644 --- a/src/hooks/useMarketData.ts +++ b/src/hooks/useMarketData.ts @@ -1,9 +1,9 @@ import { useQuery } from '@tanstack/react-query'; -import { SupportedNetworks } from '@/utils/networks'; -import { Market } from '@/utils/types'; import { getMarketDataSource } from '@/config/dataSources'; import { fetchMorphoMarket } from '@/data-sources/morpho-api/market'; import { fetchSubgraphMarket } from '@/data-sources/subgraph/market'; +import { SupportedNetworks } from '@/utils/networks'; +import { Market } from '@/utils/types'; export const useMarketData = ( uniqueKey: string | undefined, @@ -14,7 +14,8 @@ export const useMarketData = ( // Determine the data source const dataSource = network ? getMarketDataSource(network) : null; - const { data, isLoading, error, refetch } = useQuery({ // Allow null return + const { data, isLoading, error, refetch } = useQuery({ + // Allow null return queryKey: queryKey, queryFn: async (): Promise => { // Guard clauses @@ -33,12 +34,12 @@ export const useMarketData = ( return await fetchSubgraphMarket(uniqueKey, network); } } catch (fetchError) { - console.error(`Failed to fetch market data via ${dataSource}:`, fetchError); - return null; // Return null on fetch error + console.error(`Failed to fetch market data via ${dataSource}:`, fetchError); + return null; // Return null on fetch error } // Fallback if dataSource logic is somehow incorrect - console.warn("Unknown market data source determined"); + console.warn('Unknown market data source determined'); return null; }, // Enable query only if all parameters are present AND a valid data source exists @@ -55,4 +56,4 @@ export const useMarketData = ( refetch: refetch, dataSource: dataSource, // Expose the determined data source }; -}; \ No newline at end of file +}; diff --git a/src/hooks/useMarketHistoricalData.ts b/src/hooks/useMarketHistoricalData.ts index 23e5558b..ad33446a 100644 --- a/src/hooks/useMarketHistoricalData.ts +++ b/src/hooks/useMarketHistoricalData.ts @@ -1,14 +1,12 @@ import { useQuery } from '@tanstack/react-query'; -import { SupportedNetworks } from '@/utils/networks'; -import { TimeseriesOptions } from '@/utils/types'; import { getHistoricalDataSource } from '@/config/dataSources'; import { fetchMorphoMarketHistoricalData, HistoricalDataSuccessResult, } from '@/data-sources/morpho-api/historical'; -import { - fetchSubgraphMarketHistoricalData, -} from '@/data-sources/subgraph/historical'; +import { fetchSubgraphMarketHistoricalData } from '@/data-sources/subgraph/historical'; +import { SupportedNetworks } from '@/utils/networks'; +import { TimeseriesOptions } from '@/utils/types'; export const useMarketHistoricalData = ( uniqueKey: string | undefined, @@ -26,14 +24,15 @@ export const useMarketHistoricalData = ( const dataSource = network ? getHistoricalDataSource(network) : null; - const { data, isLoading, error, refetch } = useQuery< - HistoricalDataSuccessResult | null - >({ + const { data, isLoading, error, refetch } = useQuery({ queryKey: queryKey, queryFn: async (): Promise => { if (!uniqueKey || !network || !options || !dataSource) { console.log('Historical data prerequisites not met or source unavailable.', { - uniqueKey, network, options, dataSource + uniqueKey, + network, + options, + dataSource, }); return null; } @@ -49,8 +48,8 @@ export const useMarketHistoricalData = ( console.log('res', res); return res; } - - console.warn("Unknown historical data source determined"); + + console.warn('Unknown historical data source determined'); return null; }, enabled: !!uniqueKey && !!network && !!options && !!dataSource, @@ -66,4 +65,4 @@ export const useMarketHistoricalData = ( refetch: refetch, dataSource: dataSource, }; -}; \ No newline at end of file +}; diff --git a/src/utils/subgraph-types.ts b/src/utils/subgraph-types.ts index 74f24aa9..99be139c 100644 --- a/src/utils/subgraph-types.ts +++ b/src/utils/subgraph-types.ts @@ -82,4 +82,4 @@ export type SubgraphMarketQueryResponse = { market: SubgraphMarket | null; // Assuming a query like market(id: ...) might return null }; errors?: { message: string }[]; -}; \ No newline at end of file +}; diff --git a/src/utils/subgraph-urls.ts b/src/utils/subgraph-urls.ts index 15a1da58..75fc493b 100644 --- a/src/utils/subgraph-urls.ts +++ b/src/utils/subgraph-urls.ts @@ -26,4 +26,4 @@ export const SUBGRAPH_URLS: { [key in SupportedNetworks]?: string } = { export const getSubgraphUrl = (network: SupportedNetworks): string | undefined => { return SUBGRAPH_URLS[network]; -}; \ No newline at end of file +}; From ea1b8ba2e3d53cae36df24645741f2c2be99ca75 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sun, 20 Apr 2025 23:50:21 +0800 Subject: [PATCH 11/11] misc: remove dup types --- app/market/[chainId]/[marketid]/RateChart.tsx | 2 +- .../[chainId]/[marketid]/VolumeChart.tsx | 2 +- src/data-sources/morpho-api/historical.ts | 24 ++++++------------- src/data-sources/morpho-api/market.ts | 3 --- src/data-sources/subgraph/historical.ts | 4 ++-- src/utils/types.ts | 5 ++-- 6 files changed, 14 insertions(+), 26 deletions(-) diff --git a/app/market/[chainId]/[marketid]/RateChart.tsx b/app/market/[chainId]/[marketid]/RateChart.tsx index 0422ff7e..ea60009c 100644 --- a/app/market/[chainId]/[marketid]/RateChart.tsx +++ b/app/market/[chainId]/[marketid]/RateChart.tsx @@ -16,7 +16,7 @@ import { import ButtonGroup from '@/components/ButtonGroup'; import { Spinner } from '@/components/common/Spinner'; import { CHART_COLORS } from '@/constants/chartColors'; -import { MarketRates } from '@/data-sources/morpho-api/historical'; +import { MarketRates } from '@/utils/types'; import { TimeseriesDataPoint, Market, TimeseriesOptions } from '@/utils/types'; type RateChartProps = { diff --git a/app/market/[chainId]/[marketid]/VolumeChart.tsx b/app/market/[chainId]/[marketid]/VolumeChart.tsx index 53a54025..50a6157b 100644 --- a/app/market/[chainId]/[marketid]/VolumeChart.tsx +++ b/app/market/[chainId]/[marketid]/VolumeChart.tsx @@ -16,8 +16,8 @@ import { formatUnits } from 'viem'; import ButtonGroup from '@/components/ButtonGroup'; import { Spinner } from '@/components/common/Spinner'; import { CHART_COLORS } from '@/constants/chartColors'; -import { MarketVolumes } from '@/data-sources/morpho-api/historical'; import { formatReadable } from '@/utils/balance'; +import { MarketVolumes } from '@/utils/types'; import { TimeseriesDataPoint, Market, TimeseriesOptions } from '@/utils/types'; type VolumeChartProps = { diff --git a/src/data-sources/morpho-api/historical.ts b/src/data-sources/morpho-api/historical.ts index bbdd5558..ec37ee3e 100644 --- a/src/data-sources/morpho-api/historical.ts +++ b/src/data-sources/morpho-api/historical.ts @@ -1,28 +1,18 @@ import { marketHistoricalDataQuery } from '@/graphql/morpho-api-queries'; import { SupportedNetworks } from '@/utils/networks'; -import { TimeseriesOptions, Market, TimeseriesDataPoint } from '@/utils/types'; +import { + TimeseriesOptions, + Market, + TimeseriesDataPoint, + MarketRates, + MarketVolumes, +} from '@/utils/types'; import { morphoGraphqlFetcher } from './fetchers'; // --- Types related to Historical Data --- // Re-exported from types.ts for clarity or define locally if not exported export type { TimeseriesDataPoint, TimeseriesOptions }; -export type MarketRates = { - supplyApy: TimeseriesDataPoint[]; - borrowApy: TimeseriesDataPoint[]; - rateAtUTarget: TimeseriesDataPoint[]; - utilization: TimeseriesDataPoint[]; -}; - -export type MarketVolumes = { - supplyAssetsUsd: TimeseriesDataPoint[]; - borrowAssetsUsd: TimeseriesDataPoint[]; - liquidityAssetsUsd: TimeseriesDataPoint[]; - supplyAssets: TimeseriesDataPoint[]; - borrowAssets: TimeseriesDataPoint[]; - liquidityAssets: TimeseriesDataPoint[]; -}; - // Adjust the response structure type: historicalState contains rates/volumes directly type MarketWithHistoricalState = Market & { historicalState: (Partial & Partial) | null; diff --git a/src/data-sources/morpho-api/market.ts b/src/data-sources/morpho-api/market.ts index efddd1e6..c28e2ef9 100644 --- a/src/data-sources/morpho-api/market.ts +++ b/src/data-sources/morpho-api/market.ts @@ -4,9 +4,6 @@ import { Market } from '@/utils/types'; import { getMarketWarningsWithDetail } from '@/utils/warnings'; import { morphoGraphqlFetcher } from './fetchers'; -// Removed historical types (MarketRates, MarketVolumes, etc.) -// Moved HistoricalDataResult to historical.ts - type MarketGraphQLResponse = { data: { marketByUniqueKey: Market; diff --git a/src/data-sources/subgraph/historical.ts b/src/data-sources/subgraph/historical.ts index 6f92a7c3..6b8d5053 100644 --- a/src/data-sources/subgraph/historical.ts +++ b/src/data-sources/subgraph/historical.ts @@ -1,8 +1,8 @@ import { marketHourlySnapshotsQuery } from '@/graphql/morpho-subgraph-queries'; import { SupportedNetworks } from '@/utils/networks'; import { getSubgraphUrl } from '@/utils/subgraph-urls'; -import { TimeseriesOptions, TimeseriesDataPoint } from '@/utils/types'; // Assuming TimeseriesDataPoint is exported -import { HistoricalDataSuccessResult, MarketRates, MarketVolumes } from '../morpho-api/historical'; // Updated path & added imports +import { TimeseriesOptions, TimeseriesDataPoint, MarketRates, MarketVolumes } from '@/utils/types'; +import { HistoricalDataSuccessResult } from '../morpho-api/historical'; import { subgraphGraphqlFetcher } from './fetchers'; // --- Subgraph Specific Types (Copied from useSubgraphMarketHistoricalData.ts) --- diff --git a/src/utils/types.ts b/src/utils/types.ts index 0f1e2467..3c1b6a58 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -322,14 +322,15 @@ export type TimeseriesOptions = { interval: 'HOUR' | 'DAY' | 'WEEK' | 'MONTH'; }; -type MarketRates = { +// Export MarketRates and MarketVolumes +export type MarketRates = { supplyApy: TimeseriesDataPoint[]; borrowApy: TimeseriesDataPoint[]; rateAtUTarget: TimeseriesDataPoint[]; utilization: TimeseriesDataPoint[]; }; -type MarketVolumes = { +export type MarketVolumes = { supplyAssetsUsd: TimeseriesDataPoint[]; borrowAssetsUsd: TimeseriesDataPoint[]; liquidityAssetsUsd: TimeseriesDataPoint[];