diff --git a/packages/ui/app/_components/markets/Cells/APR.tsx b/packages/ui/app/_components/markets/Cells/APR.tsx index 347786e1b..90eb378f4 100644 --- a/packages/ui/app/_components/markets/Cells/APR.tsx +++ b/packages/ui/app/_components/markets/Cells/APR.tsx @@ -48,7 +48,7 @@ const RewardRow = ({ icon, text }: { icon: string; text: string }) => ( ); -export default function APRCell(props: APRCellProps) { +export default function APR(props: APRCellProps) { const { baseAPRFormatted, effectiveNativeYield, diff --git a/packages/ui/app/market/page.tsx b/packages/ui/app/market/page.tsx index b27178b21..764d10834 100644 --- a/packages/ui/app/market/page.tsx +++ b/packages/ui/app/market/page.tsx @@ -11,7 +11,6 @@ import { useChainId } from 'wagmi'; import type { MarketRowData } from '@ui/hooks/market/useMarketData'; import { useMarketData } from '@ui/hooks/market/useMarketData'; import { VaultRowData } from '@ui/types/SupplyVaults'; -import { useSupplyVaultsRealData } from '@ui/hooks/market/useSupplyVaultsRealData'; import ManageDialog from '../_components/dialogs/manage'; import FeaturedMarketTile from '../_components/markets/FeaturedMarketTile'; @@ -23,6 +22,7 @@ import TvlTile from '../_components/markets/TvlTile'; import PoolToggle from '../_components/markets/PoolToggle'; import { isAddress } from 'viem'; import SearchInput from '../_components/markets/SearcInput'; +import { useSupplyVaultsData } from '@ui/hooks/market/useSupplyVaultsData'; const NetworkSelector = dynamic( () => import('../_components/markets/NetworkSelector'), @@ -45,8 +45,7 @@ export default function Market() { const [searchTerm, setSearchTerm] = useState(''); - const { vaultData, isLoading: isLoadingVaults } = - useSupplyVaultsRealData(chain); + const { vaultData, isLoading: isLoadingVaults } = useSupplyVaultsData(chain); const { marketData, selectedMarketData, diff --git a/packages/ui/hooks/market/useSupplyVaultsData.ts b/packages/ui/hooks/market/useSupplyVaultsData.ts index 3dbf64c2e..c4e437f57 100644 --- a/packages/ui/hooks/market/useSupplyVaultsData.ts +++ b/packages/ui/hooks/market/useSupplyVaultsData.ts @@ -1,15 +1,39 @@ import { useState, useEffect, useMemo } from 'react'; -import type { Address, Hex } from 'viem'; +import { useReadContracts } from 'wagmi'; +import type { Address } from 'viem'; import { pools } from '@ui/constants/index'; import { VaultRowData } from '@ui/types/SupplyVaults'; +import { useTokenPrices, type TokenConfig } from '../useTokenPrices'; -interface UseSupplyVaultsReturn { - vaultData: VaultRowData[]; - isLoading: boolean; - error: Error | null; -} +const COMPOUND_MARKET_ABI = [ + { + inputs: [], + name: 'apr', + outputs: [{ type: 'uint256', name: '' }], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'totalAssets', + outputs: [{ type: 'uint256', name: '' }], + stateMutability: 'view', + type: 'function' + } +] as const; + +const TOKEN_CONFIGS: TokenConfig[] = [ + { + cgId: 'usd-coin', + symbol: 'USDC' + }, + { + cgId: 'ethereum', + symbol: 'WETH' + } +]; -const ALL_VAULT_DATA: VaultRowData[] = [ +const BASE_VAULT_DATA: VaultRowData[] = [ { asset: 'USDC', logo: '/img/symbols/32/color/usdc.png', @@ -21,23 +45,23 @@ const ALL_VAULT_DATA: VaultRowData[] = [ ] }, apr: { - total: 12.5, + total: 0, breakdown: [ - { source: 'Lending APR', value: 8.2 }, - { source: 'Rewards', value: 4.3 } + { source: 'Lending APR', value: 0 }, + { source: 'Rewards', value: 0 } ] }, totalSupply: { - tokens: 25000000, - usd: 25000000 + tokens: 0, + usd: 0 }, utilisation: 85, userPosition: { - tokens: 1000, - usd: 1000 + tokens: 0, + usd: 0 }, vaultAddress: '0x1234567890123456789012345678901234567890' as Address, - underlyingDecimals: 18, + underlyingDecimals: 6, underlyingToken: '0x6b175474e89094c44da98b954eedeac495271d0f', underlyingSymbol: 'USDC', cToken: '0x6b175474e89094c44da98b954eedeac495271d0f' @@ -53,67 +77,40 @@ const ALL_VAULT_DATA: VaultRowData[] = [ ] }, apr: { - total: 8.7, + total: 0, breakdown: [ - { source: 'Lending APR', value: 5.5 }, - { source: 'Rewards', value: 3.2 } + { source: 'Lending APR', value: 0 }, + { source: 'Rewards', value: 0 } ] }, totalSupply: { - tokens: 1200, - usd: 3000000 + tokens: 0, + usd: 0 }, utilisation: 72, userPosition: { - tokens: 5, - usd: 12500 + tokens: 0, + usd: 0 }, vaultAddress: '0x2345678901234567890123456789012345678901' as Address, underlyingDecimals: 18, underlyingToken: '0x6b175474e89094c44da98b954eedeac495271d0f', underlyingSymbol: 'WETH', cToken: '0x6b175474e89094c44da98b954eedeac495271d0f' - }, - { - asset: 'WBTC', - logo: '/img/symbols/32/color/wbtc.png', - strategy: { - description: 'Bitcoin-backed lending strategy', - distribution: [ - { poolName: 'Main Pool', percentage: 70 }, - { poolName: 'Native Pool', percentage: 30 } - ] - }, - apr: { - total: 6.8, - breakdown: [ - { source: 'Lending APR', value: 4.8 }, - { source: 'Rewards', value: 2.0 } - ] - }, - totalSupply: { - tokens: 180, - usd: 7200000 - }, - utilisation: 65, - userPosition: { - tokens: 0.5, - usd: 20000 - }, - vaultAddress: '0x3456789012345678901234567890123456789012' as Address, - underlyingDecimals: 18, - underlyingToken: '0x6b175474e89094c44da98b954eedeac495271d0f', - underlyingSymbol: 'WBTC', - cToken: '0x6b175474e89094c44da98b954eedeac495271d0f' } ]; -export function useSupplyVaultsData( - chain: string, - address?: string -): UseSupplyVaultsReturn { +export function useSupplyVaultsData(chain: string): { + vaultData: VaultRowData[]; + isLoading: boolean; + error: Error | null; +} { const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); + const [vaultData, setVaultData] = useState([]); + + const { data: tokenPrices, isLoading: isPricesLoading } = + useTokenPrices(TOKEN_CONFIGS); const filteredVaultData = useMemo(() => { const chainConfig = pools[+chain]; @@ -123,32 +120,122 @@ export function useSupplyVaultsData( return []; } - return ALL_VAULT_DATA.filter((vault) => + return BASE_VAULT_DATA.filter((vault) => vaultConfig.assets.includes(vault.asset) ); }, [chain]); - useEffect(() => { - const fetchData = async () => { - try { - setIsLoading(true); - // Simulate API call - await new Promise((resolve) => setTimeout(resolve, 1000)); - setIsLoading(false); - } catch (err) { - setError( - err instanceof Error ? err : new Error('Failed to fetch vault data') - ); - setIsLoading(false); + // Prepare contract reads for each vault + const contractReads = useMemo(() => { + return filteredVaultData.flatMap((vault) => [ + { + address: vault.vaultAddress, + abi: COMPOUND_MARKET_ABI, + functionName: 'apr' + }, + { + address: vault.vaultAddress, + abi: COMPOUND_MARKET_ABI, + functionName: 'totalAssets' } - }; + ]); + }, [filteredVaultData]); + + // Read contract data + const { data: contractData, isLoading: isContractLoading } = useReadContracts( + { + contracts: contractReads, + query: { + enabled: contractReads.length > 0 + } + } + ); - fetchData(); - }, [chain, address]); + useEffect(() => { + if (!contractData) { + return; + } + + try { + setIsLoading(true); + + // Update vault data with real values + const updatedVaultData = filteredVaultData.map((vault, index) => { + const aprData = contractData[index * 2]; + const totalAssetsData = contractData[index * 2 + 1]; + + // Convert APR from contract (scaled by 1e18) to percentage + let aprValue = 0; + let lendingApr = 0; + let rewardsApr = 0; + + if (aprData) { + aprValue = Number(aprData) / 1e16; + if (!Number.isFinite(aprValue)) aprValue = 0; + + lendingApr = aprValue * 0.7; + rewardsApr = aprValue * 0.3; + } + + // Convert total assets from contract + let totalTokens = 0; + if (totalAssetsData) { + totalTokens = + Number(totalAssetsData) / 10 ** vault.underlyingDecimals; + if (!Number.isFinite(totalTokens)) totalTokens = 0; + } + + // Get token price from our prices data + const tokenPrice = tokenPrices?.[vault.asset]?.price ?? 0; + const totalUsdValue = totalTokens * tokenPrice; + + return { + ...vault, + apr: { + total: aprValue, + breakdown: [ + { source: 'Lending APR', value: lendingApr }, + { source: 'Rewards', value: rewardsApr } + ] + }, + totalSupply: { + tokens: totalTokens, + usd: Number.isFinite(totalUsdValue) ? totalUsdValue : 0 + } + }; + }); + + setVaultData(updatedVaultData); + setIsLoading(false); + } catch (err) { + console.error('Error processing vault data:', err); + setError( + err instanceof Error ? err : new Error('Failed to fetch vault data') + ); + setIsLoading(false); + + setVaultData( + filteredVaultData.map((vault) => ({ + ...vault, + apr: { + total: 0, + breakdown: [ + { source: 'Lending APR', value: 0 }, + { source: 'Rewards', value: 0 } + ] + }, + totalSupply: { + tokens: 0, + usd: 0 + } + })) + ); + } + }, [contractData, filteredVaultData, tokenPrices]); return { - vaultData: filteredVaultData, - isLoading, + vaultData, + isLoading: isLoading || isPricesLoading || isContractLoading, error }; } diff --git a/packages/ui/hooks/market/useSupplyVaultsRealData.ts b/packages/ui/hooks/market/useSupplyVaultsRealData.ts deleted file mode 100644 index 6426f0ecf..000000000 --- a/packages/ui/hooks/market/useSupplyVaultsRealData.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { useState, useEffect, useMemo } from 'react'; -import { useReadContracts } from 'wagmi'; -import type { Address } from 'viem'; -import { pools } from '@ui/constants/index'; -import { VaultRowData } from '@ui/types/SupplyVaults'; - -// CompoundMarketERC4626 ABI (only the functions we need) -const COMPOUND_MARKET_ABI = [ - { - inputs: [], - name: 'apr', - outputs: [{ type: 'uint256', name: '' }], - stateMutability: 'view', - type: 'function' - }, - { - inputs: [], - name: 'totalAssets', - outputs: [{ type: 'uint256', name: '' }], - stateMutability: 'view', - type: 'function' - } -] as const; - -interface UseSupplyVaultsRealDataReturn { - vaultData: VaultRowData[]; - isLoading: boolean; - error: Error | null; -} - -// Base data structure similar to ALL_VAULT_DATA -const BASE_VAULT_DATA: VaultRowData[] = [ - { - asset: 'USDC', - logo: '/img/symbols/32/color/usdc.png', - strategy: { - description: 'Multi-pool lending optimization', - distribution: [ - { poolName: 'Main Pool', percentage: 60 }, - { poolName: 'Native Pool', percentage: 40 } - ] - }, - apr: { - total: 0, // Will be updated with real data - breakdown: [ - { source: 'Lending APR', value: 0 }, // Will be updated - { source: 'Rewards', value: 0 } - ] - }, - totalSupply: { - tokens: 0, // Will be updated with real data - usd: 0 - }, - utilisation: 85, - userPosition: { - tokens: 0, - usd: 0 - }, - vaultAddress: '0x1234567890123456789012345678901234567890' as Address, - underlyingDecimals: 6, // USDC has 6 decimals - underlyingToken: '0x6b175474e89094c44da98b954eedeac495271d0f', - underlyingSymbol: 'USDC', - cToken: '0x6b175474e89094c44da98b954eedeac495271d0f' - }, - { - asset: 'WETH', - logo: '/img/symbols/32/color/weth.png', - strategy: { - description: 'Isolated pool', - distribution: [ - { poolName: 'Main Pool', percentage: 45 }, - { poolName: 'Native Pool', percentage: 55 } - ] - }, - apr: { - total: 0, - breakdown: [ - { source: 'Lending APR', value: 0 }, - { source: 'Rewards', value: 0 } - ] - }, - totalSupply: { - tokens: 0, - usd: 0 - }, - utilisation: 72, - userPosition: { - tokens: 0, - usd: 0 - }, - vaultAddress: '0x2345678901234567890123456789012345678901' as Address, - underlyingDecimals: 18, // WETH has 18 decimals - underlyingToken: '0x6b175474e89094c44da98b954eedeac495271d0f', - underlyingSymbol: 'WETH', - cToken: '0x6b175474e89094c44da98b954eedeac495271d0f' - } -]; - -export function useSupplyVaultsRealData( - chain: string, - address?: string -): UseSupplyVaultsRealDataReturn { - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [vaultData, setVaultData] = useState([]); - - const filteredVaultData = useMemo(() => { - const chainConfig = pools[+chain]; - const vaultConfig = chainConfig.vaults?.[0]; - - if (!vaultConfig) { - return []; - } - - return BASE_VAULT_DATA.filter((vault) => - vaultConfig.assets.includes(vault.asset) - ); - }, [chain]); - - // Prepare contract reads for each vault - const contractReads = useMemo(() => { - return filteredVaultData.flatMap((vault) => [ - { - address: vault.vaultAddress, - abi: COMPOUND_MARKET_ABI, - functionName: 'apr' - }, - { - address: vault.vaultAddress, - abi: COMPOUND_MARKET_ABI, - functionName: 'totalAssets' - } - ]); - }, [filteredVaultData]); - - // Read contract data - const { data: contractData } = useReadContracts({ - contracts: contractReads, - query: { - enabled: contractReads.length > 0 - } - }); - - useEffect(() => { - if (!contractData) { - return; - } - - try { - setIsLoading(true); - - // Update vault data with real values - const updatedVaultData = filteredVaultData.map((vault, index) => { - const aprData = contractData[index * 2]; - const totalAssetsData = contractData[index * 2 + 1]; - - if (!aprData || !totalAssetsData) { - return vault; - } - - // Convert APR from contract (scaled by 1e18) to percentage - const aprValue = Number(aprData) / 1e16; // Divide by 1e16 to convert to percentage - // Assume 70% of APR is from lending and 30% from rewards for this example - const lendingApr = aprValue * 0.7; - const rewardsApr = aprValue * 0.3; - - // Convert total assets from contract (in underlying token decimals) - const totalTokens = - Number(totalAssetsData) / 10 ** vault.underlyingDecimals; - - return { - ...vault, - apr: { - total: aprValue, - breakdown: [ - { source: 'Lending APR', value: lendingApr }, - { source: 'Rewards', value: rewardsApr } - ] - }, - totalSupply: { - tokens: totalTokens, - usd: totalTokens * (vault.asset === 'WETH' ? 2500 : 1) // Mock price for example - } - }; - }); - - setVaultData(updatedVaultData); - setIsLoading(false); - } catch (err) { - setError( - err instanceof Error ? err : new Error('Failed to fetch vault data') - ); - setIsLoading(false); - } - }, [contractData, filteredVaultData]); - - return { - vaultData, - isLoading, - error - }; -} diff --git a/packages/ui/hooks/useTokenPrices.ts b/packages/ui/hooks/useTokenPrices.ts new file mode 100644 index 000000000..2f823d50d --- /dev/null +++ b/packages/ui/hooks/useTokenPrices.ts @@ -0,0 +1,110 @@ +import { useQuery } from '@tanstack/react-query'; +import axios from 'axios'; +import { COINGECKO_API, DEFI_LLAMA_API } from '@ui/constants/index'; + +export interface TokenConfig { + cgId: string; + symbol: string; + address?: string; +} + +interface TokenPrice { + symbol: string; + price: number; +} + +export function useTokenPrices(tokens: TokenConfig[]) { + return useQuery({ + queryKey: ['tokenPrices', tokens.map((t) => t.cgId).sort()], + + queryFn: async () => { + const prices: Record = {}; + + if (tokens.length === 0) return prices; + + const cgIds = tokens.map((t) => t.cgId).join(','); + + try { + // Try CoinGecko first + const { data } = await axios.get(`${COINGECKO_API}${cgIds}`); + + tokens.forEach((token) => { + const price = data[token.cgId]?.usd ?? 0; + prices[token.symbol] = { + symbol: token.symbol, + price: Number.isFinite(price) ? price : 0 + }; + }); + + // For any tokens with zero price, try DefiLlama as fallback + const missingTokens = tokens.filter( + (token) => !prices[token.symbol]?.price + ); + + if (missingTokens.length > 0) { + await Promise.all( + missingTokens.map(async (token) => { + try { + const { data } = await axios.get( + `${DEFI_LLAMA_API}coingecko:${token.cgId}` + ); + + const price = data.coins[`coingecko:${token.cgId}`]?.price ?? 0; + prices[token.symbol] = { + symbol: token.symbol, + price: Number.isFinite(price) ? price : 0 + }; + + // Set default price of 1 for stablecoins if we got 0 + if ( + price === 0 && + (token.symbol === 'USDC' || + token.symbol === 'USDT' || + token.symbol === 'DAI') + ) { + prices[token.symbol].price = 1; + } + } catch (e) { + console.warn( + `Failed to fetch price for ${token.symbol} from DefiLlama` + ); + // Set default price of 1 for stablecoins, 0 for others + prices[token.symbol] = { + symbol: token.symbol, + price: + token.symbol === 'USDC' || + token.symbol === 'USDT' || + token.symbol === 'DAI' + ? 1 + : 0 + }; + } + }) + ); + } + + return prices; + } catch (error) { + console.error('Failed to fetch token prices:', error); + + // Return default prices in case of complete failure + tokens.forEach((token) => { + prices[token.symbol] = { + symbol: token.symbol, + price: + token.symbol === 'USDC' || + token.symbol === 'USDT' || + token.symbol === 'DAI' + ? 1 + : 0 + }; + }); + + return prices; + } + }, + + staleTime: 5 * 60 * 1000, // 5 minutes + refetchInterval: 5 * 60 * 1000 // Refetch every 5 minutes + }); +}