diff --git a/src/app/api/stats/route.ts b/src/app/api/stats/route.ts index c25a047b2..eaa73ce8a 100644 --- a/src/app/api/stats/route.ts +++ b/src/app/api/stats/route.ts @@ -1,11 +1,16 @@ import { CoingeckoProvider, CoinGeckoService } from '@indexcoop/analytics-sdk' import { NextRequest, NextResponse } from 'next/server' +import { fetchTokenMetrics } from '@/lib/utils/api/index-data-provider' +import { fetchCarryCosts } from '@/lib/utils/fetch' + export async function GET(req: NextRequest) { const { searchParams } = new URL(req.url) + const tokenAddress = searchParams.get('address') const symbol = searchParams.get('symbol') + const base = searchParams.get('base') const baseCurrency = searchParams.get('baseCurrency') - if (!symbol || !baseCurrency) { + if (!tokenAddress || !symbol || !base || !baseCurrency) { return NextResponse.json('Bad Request', { status: 400 }) } try { @@ -13,8 +18,24 @@ export async function GET(req: NextRequest) { process.env.COINGECKO_API_KEY!, ) const provider = new CoingeckoProvider(coingeckoService) - const data = await provider.getTokenStats(symbol, baseCurrency) - return NextResponse.json({ ...data }) + const data = await provider.getTokenStats(base, baseCurrency) + const carryCosts = await fetchCarryCosts() + const costOfCarry = carryCosts + ? carryCosts[symbol.toLowerCase()] ?? null + : null + const metrics = await fetchTokenMetrics({ + tokenAddress: tokenAddress, + metrics: ['nav', 'navchange'], + }) + return NextResponse.json({ + base: { ...data, baseCurrency }, + token: { + symbol, + costOfCarry, + nav: metrics?.NetAssetValue ?? 0, + navchange: (metrics?.NavChange24Hr ?? 0) * 100, + }, + }) } catch (error) { return NextResponse.json(error, { status: 500 }) } diff --git a/src/app/leverage/components/leverage-widget/components/summary.tsx b/src/app/leverage/components/leverage-widget/components/summary.tsx index 774aff79c..83d7b5e9a 100644 --- a/src/app/leverage/components/leverage-widget/components/summary.tsx +++ b/src/app/leverage/components/leverage-widget/components/summary.tsx @@ -1,7 +1,6 @@ import { Disclosure } from '@headlessui/react' import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/20/solid' -import { useLeverageToken } from '@/app/leverage/provider' import { GasFees } from '@/components/gas-fees' import { StyledSkeleton } from '@/components/skeleton' @@ -26,7 +25,6 @@ function SummaryQuote(props: SummaryQuoteProps) { } export function Summary() { - const { stats } = useLeverageToken() const { gasFeesEth, gasFeesUsd, @@ -36,7 +34,7 @@ export function Summary() { ouputAmount, outputAmountUsd, shouldShowSummaryDetails, - } = useFormattedLeverageData(stats) + } = useFormattedLeverageData() if (!shouldShowSummaryDetails && !isFetchingQuote) return null return ( diff --git a/src/app/leverage/components/leverage-widget/index.tsx b/src/app/leverage/components/leverage-widget/index.tsx index 4d9fa817f..881101d6d 100644 --- a/src/app/leverage/components/leverage-widget/index.tsx +++ b/src/app/leverage/components/leverage-widget/index.tsx @@ -3,6 +3,7 @@ import { useDisclosure } from '@chakra-ui/react' import { useCallback } from 'react' +import { useQuickStats } from '@/app/leverage/components/stats/use-quick-stats' import { supportedNetworks } from '@/app/leverage/constants' import { useLeverageToken } from '@/app/leverage/provider' import { Receive } from '@/components/receive' @@ -33,15 +34,15 @@ export function LeverageWidget() { const { queryParams } = useQueryParams() const { address } = useWallet() const { + indexToken, inputToken, inputTokenAmount, inputTokens, inputValue, isMinting, - costOfCarry, leverageType, + market, outputTokens, - stats, transactionReview, onChangeInputTokenAmount, onSelectInputToken, @@ -52,6 +53,7 @@ export function LeverageWidget() { supportedLeverageTypes, toggleIsMinting, } = useLeverageToken() + const { data } = useQuickStats(market, indexToken) const { contract, @@ -63,7 +65,7 @@ export function LeverageWidget() { ouputAmount, outputAmountUsd, resetData, - } = useFormattedLeverageData(stats) + } = useFormattedLeverageData() const { isOpen: isSelectInputTokenOpen, @@ -116,7 +118,7 @@ export function LeverageWidget() { onSelectToken={onOpenSelectOutputToken} /> - + {/* @@ -146,10 +148,12 @@ export function LeverageSelectorContainer() { = 0 ? 'text-ic-green' : 'text-ic-red'} + value={`${formatAmount(data.token.navchange, 2)}%`} + overrideLabelColor={ + data.token.navchange >= 0 ? 'text-ic-green' : 'text-ic-red' + } /> ) diff --git a/src/app/leverage/components/stats/use-quick-stats.tsx b/src/app/leverage/components/stats/use-quick-stats.tsx index 1ef0ad6c1..248e59f10 100644 --- a/src/app/leverage/components/stats/use-quick-stats.tsx +++ b/src/app/leverage/components/stats/use-quick-stats.tsx @@ -1,57 +1,95 @@ -import { useQuery } from '@tanstack/react-query' +import { QueryFunctionContext, useQuery } from '@tanstack/react-query' import { formatAmount, formatDollarAmount } from '@/lib/utils' interface QuickStats { - symbol: string - price: string - change24h: number - low24h: string - high24h: string + base: { + symbol: string + price: string + change24h: number + low24h: string + high24h: string + } + token: { + symbol: string + costOfCarry: number + nav: number + navchange: number + } } interface QuickStatsApiResponse { - symbol: string - price: number - change24h: number - low24h: number - high24h: number + base: { + symbol: string + price: number + change24h: number + low24h: number + high24h: number + } + token: { + symbol: string + costOfCarry: number + nav: number + navchange: number + } } +type QuickStatsQueryKey = [ + string, + { address: string | undefined; symbol: string; market: string }, +] + function formatStatsAmount(amount: number, baseCurrency: string): string { if (baseCurrency === 'btc') return `${formatAmount(amount, 4)} BTC` if (baseCurrency === 'eth') return `${formatAmount(amount, 4)} ETH` return formatDollarAmount(amount, true, 2) } -export function useQuickStats(market: string) { - async function fetchStats(): Promise { +export function useQuickStats( + market: string, + indexToken: { address: string | undefined; symbol: string }, +) { + async function fetchStats( + context: QueryFunctionContext, + ): Promise { + const [, { address, symbol, market }] = context.queryKey const m = market.split(' / ') - const symbol = m[0] + const baseToken = m[0] const baseCurrency = m[1].toLowerCase() try { const response = await fetch( - `/api/stats?symbol=${symbol}&baseCurrency=${baseCurrency}`, + `/api/stats?address=${address}&symbol=${symbol}&base=${baseToken}&baseCurrency=${baseCurrency}`, { method: 'GET', }, ) - const json: QuickStatsApiResponse = await response.json() + const { base, token }: QuickStatsApiResponse = await response.json() return { - symbol: json.symbol, - price: formatStatsAmount(json.price, baseCurrency), - change24h: json.change24h, - low24h: formatStatsAmount(json.low24h, baseCurrency), - high24h: formatStatsAmount(json.high24h, baseCurrency), + base: { + symbol: base.symbol, + price: formatStatsAmount(base.price, baseCurrency), + change24h: base.change24h, + low24h: formatStatsAmount(base.low24h, baseCurrency), + high24h: formatStatsAmount(base.high24h, baseCurrency), + }, + token: { ...token }, } } catch (error) { console.warn('Error fetching quick stats:', error) return { - symbol, - price: '', - change24h: 0, - low24h: '', - high24h: '', + base: { + symbol: baseToken, + price: '', + change24h: 0, + low24h: '', + high24h: '', + }, + token: { + symbol: '', + costOfCarry: 0, + nav: 0, + navchange: 0, + }, } } } @@ -60,19 +98,30 @@ export function useQuickStats(market: string) { queryKey: [ 'fetch-quick-stats', { + symbol: indexToken.symbol, + address: indexToken.address, market, }, ], queryFn: fetchStats, + enabled: !!indexToken.address, }) return { data: data ?? { - symbol: '', - price: '', - change24h: 0, - low24h: '', - high24h: '', + base: { + symbol: '', + price: '', + change24h: 0, + low24h: '', + high24h: '', + }, + token: { + symbol: '', + costOfCarry: 0, + nav: 0, + navchange: 0, + }, }, isFetchingQuickStats: isFetching, } diff --git a/src/app/leverage/page.tsx b/src/app/leverage/page.tsx index 1073d14e7..71361ede4 100644 --- a/src/app/leverage/page.tsx +++ b/src/app/leverage/page.tsx @@ -6,6 +6,7 @@ import { Suspense, useEffect, useState } from 'react' import { ChartTabs } from '@/app/leverage/components/chart-tabs' import { QuickStats } from '@/app/leverage/components/stats/index' +import { useQuickStats } from '@/app/leverage/components/stats/use-quick-stats' import TradingViewWidget from '@/app/leverage/components/trading-view-widget' import { useLeverageToken } from '@/app/leverage/provider' import { ChartTab } from '@/app/leverage/types' @@ -18,7 +19,11 @@ import { YourTokens } from './components/your-tokens' const surveyTracking = { utm_source: 'app' } export default function Page() { - const { indexToken, nav } = useLeverageToken() + const { indexToken, market } = useLeverageToken() + const { data } = useQuickStats(market, { + address: indexToken.address!, + symbol: indexToken.symbol, + }) const [currentTab, setCurrentTab] = useState('indexcoop-chart') const { colorMode, toggleColorMode } = useColorMode() @@ -53,7 +58,7 @@ export default function Page() { ) : ( <> diff --git a/src/app/leverage/provider.tsx b/src/app/leverage/provider.tsx index b215ce7ce..176515aa4 100644 --- a/src/app/leverage/provider.tsx +++ b/src/app/leverage/provider.tsx @@ -10,7 +10,6 @@ import { useMemo, useState, } from 'react' -import { isAddress } from 'viem' import { arbitrum } from 'viem/chains' import { usePublicClient } from 'wagmi' @@ -28,16 +27,13 @@ import { useQueryParams } from '@/lib/hooks/use-query-params' import { getTokenPrice, useNativeTokenPrice } from '@/lib/hooks/use-token-price' import { useWallet } from '@/lib/hooks/use-wallet' import { isValidTokenInput, parseUnits } from '@/lib/utils' -import { IndexApi } from '@/lib/utils/api/index-api' -import { fetchTokenMetrics } from '@/lib/utils/api/index-data-provider' -import { fetchCarryCosts } from '@/lib/utils/fetch' import { getCurrencyTokens, getLeverageTokens, supportedLeverageTypes, } from './constants' -import { BaseTokenStats, LeverageToken, LeverageType } from './types' +import { LeverageToken, LeverageType } from './types' const eth2x = getTokenByChainAndSymbol(1, 'ETH2X') @@ -50,18 +46,13 @@ export interface TokenContext { indexToken: Token indexTokens: Token[] market: string - nav: number - navchange: number inputToken: Token outputToken: Token inputTokenAmount: bigint - costOfCarry: number | null inputTokens: Token[] outputTokens: Token[] isFetchingQuote: boolean - isFetchingStats: boolean quoteResult: QuoteResult | null - stats: BaseTokenStats | null supportedLeverageTypes: LeverageType[] transactionReview: TransactionReview | null onChangeInputTokenAmount: (input: string) => void @@ -81,18 +72,13 @@ export const LeverageTokenContext = createContext({ indexToken: { ...eth2x, image: eth2x.logoURI }, indexTokens: [], market: 'ETH / USD', - nav: 0, - navchange: 0, inputToken: ETH, outputToken: { ...eth2x, image: eth2x.logoURI }, inputTokenAmount: BigInt(0), - costOfCarry: null, inputTokens: [], outputTokens: [], isFetchingQuote: false, - isFetchingStats: false, quoteResult: null, - stats: null, supportedLeverageTypes: [], transactionReview: null, onChangeInputTokenAmount: () => {}, @@ -136,12 +122,6 @@ export function LeverageProvider(props: { children: any }) { const publicClient = usePublicClient({ chainId: chainIdRaw }) const [inputValue, setInputValue] = useState('') - const [carryCosts, setCarryCosts] = useState | null>( - null, - ) - const [costOfCarry, setCostOfCarry] = useState(null) - const [isFetchingStats, setFetchingStats] = useState(true) - const [stats, setStats] = useState(null) const [quoteResult, setQuoteResult] = useState({ type: QuoteType.flashmint, isAvailable: true, @@ -163,10 +143,6 @@ export function LeverageProvider(props: { children: any }) { return inputToken }, [inputToken, isMinting, outputToken]) - const indexTokenAddress = useMemo(() => { - return getTokenByChainAndSymbol(chainId, indexToken.symbol)?.address ?? '' - }, [chainId, indexToken.symbol]) - const indexTokens = useMemo(() => { return getLeverageTokens(chainId) }, [chainId]) @@ -180,25 +156,6 @@ export function LeverageProvider(props: { children: any }) { indexTokenAddresses, ) - const { - data: { nav, navchange }, - } = useQuery({ - enabled: isAddress(indexTokenAddress), - initialData: { nav: 0, navchange: 0 }, - queryKey: ['token-nav', indexTokenAddress], - queryFn: async () => { - const data = await fetchTokenMetrics({ - tokenAddress: indexTokenAddress!, - metrics: ['nav', 'navchange'], - }) - - return { - nav: data?.NetAssetValue ?? 0, - navchange: (data?.NavChange24Hr ?? 0) * 100, - } - }, - }) - const inputTokenAmount = useMemo( () => inputValue === '' @@ -246,37 +203,6 @@ export function LeverageProvider(props: { children: any }) { }) }, [chainId, isMinting, inputToken, outputToken, updateQueryParams]) - useEffect(() => { - const fetchStats = async () => { - try { - setFetchingStats(true) - const indexApi = new IndexApi() - const res = await indexApi.get(`/token/${baseToken.symbol}`) - setStats(res.data) - } catch (err) { - console.warn('Error fetching token stats', err) - } - setFetchingStats(false) - } - fetchStats() - }, [baseToken]) - - useEffect(() => { - async function fetchCosts() { - const carryCosts = await fetchCarryCosts() - setCarryCosts(carryCosts) - } - - fetchCosts() - }, []) - - useEffect(() => { - const inputOutputToken = isMinting ? outputToken : inputToken - if (!inputOutputToken) return - if (carryCosts) - setCostOfCarry(carryCosts[inputOutputToken.symbol.toLowerCase()] ?? null) - }, [isMinting, inputToken, outputToken, carryCosts]) - const onChangeInputTokenAmount = useCallback( (input: string) => { if (input === '') { @@ -526,15 +452,10 @@ export function LeverageProvider(props: { children: any }) { outputToken, inputTokenAmount, market, - nav, - navchange, - costOfCarry, inputTokens, outputTokens, isFetchingQuote, - isFetchingStats, quoteResult, - stats, supportedLeverageTypes: isRatioToken ? [LeverageType.Long2x] : supportedLeverageTypes[chainId], diff --git a/src/app/leverage/use-formatted-data.tsx b/src/app/leverage/use-formatted-data.tsx index 8dfc69d3e..5e6f22637 100644 --- a/src/app/leverage/use-formatted-data.tsx +++ b/src/app/leverage/use-formatted-data.tsx @@ -5,15 +5,8 @@ import { useWallet } from '@/lib/hooks/use-wallet' import { formatAmount, formatDollarAmount, formatWei } from '@/lib/utils' import { useLeverageToken } from './provider' -import { BaseTokenStats } from './types' export interface FormattedLeverageData { - symbol: string - price: string - change24h: string - change24hIsPositive: boolean - low24h: string - high24h: string hasInsufficientFunds: boolean gasFeesEth: string gasFeesUsd: string @@ -29,9 +22,7 @@ export interface FormattedLeverageData { shouldShowSummaryDetails: boolean } -export function useFormattedLeverageData( - stats: BaseTokenStats | null, -): FormattedLeverageData { +export function useFormattedLeverageData(): FormattedLeverageData { const { address } = useWallet() const { inputToken, @@ -96,12 +87,6 @@ export function useFormattedLeverageData( ) return { - symbol: stats?.symbol ?? '', - price: stats ? formatDollarAmount(stats.price) : '', - change24h: stats ? `${stats.change24h.toFixed(2)}%` : '', - change24hIsPositive: stats ? stats.change24h >= 0 : true, - low24h: stats ? formatAmount(stats.low24h) : '', - high24h: stats ? formatAmount(stats.high24h) : '', hasInsufficientFunds, gasFeesEth, gasFeesUsd: quote?.gasCostsInUsd