From 0868722fd556e899bbf828f5406e5dffb9e779da Mon Sep 17 00:00:00 2001 From: Toki Date: Fri, 20 Jun 2025 14:14:00 +0200 Subject: [PATCH 1/8] added earn accrued yield and pi integration --- src/app/api/earn/history/route.ts | 121 ++++++++++++++ src/app/api/leverage/history/route.ts | 12 +- src/app/earn/components/balance-card.tsx | 153 +++++++++++++----- src/app/earn/components/product-card.tsx | 2 + src/app/earn/page.tsx | 50 +++++- .../portfolio-widget/portfolio-widget.tsx | 4 +- src/app/store/positions-atom.ts | 2 +- 7 files changed, 293 insertions(+), 51 deletions(-) create mode 100644 src/app/api/earn/history/route.ts diff --git a/src/app/api/earn/history/route.ts b/src/app/api/earn/history/route.ts new file mode 100644 index 000000000..1c5d5f01f --- /dev/null +++ b/src/app/api/earn/history/route.ts @@ -0,0 +1,121 @@ +import { + getTokenByChainAndAddress, + getTokenByChainAndSymbol, + isYieldToken, +} from '@indexcoop/tokenlists' +import { uniqBy } from 'lodash' +import mapKeys from 'lodash/mapKeys' +import { NextRequest, NextResponse } from 'next/server' +import { Address } from 'viem' + +import { calculateAverageEntryPrice } from '@/app/leverage/utils/fetch-leverage-token-prices' +import { + getApiV2PriceCoingeckoSimplePrice, + getApiV2UserAddressPositions, +} from '@/gen' + +type TokenTransferRequest = { + user: Address +} + +const fetchCoingeckoPrices = async ( + ids: string[], + vs_currencies: string[], +): Promise> => { + const response = await getApiV2PriceCoingeckoSimplePrice({ + ids: ids.join(','), + vs_currencies: vs_currencies.join(','), + }) + return response.data +} + +const mapCoingeckoIdToSymbol = (id: string) => { + switch (id) { + case 'ethereum': + return 'eth' + case 'bitcoin': + return 'btc' + case 'wrapped-solana-universal': + return 'sol' + case 'wrapped-sui-universal': + return 'sui' + default: + return id + } +} + +const wsteth = getTokenByChainAndSymbol(8453, 'wstETH') + +export async function POST(req: NextRequest) { + try { + const { user } = (await req.json()) as TokenTransferRequest + + const { data: positions } = await getApiV2UserAddressPositions({ + address: user, + }) + + const history = positions + .filter(({ rawContract }) => { + const token1 = getTokenByChainAndAddress(1, rawContract.address) + const token8453 = getTokenByChainAndAddress(8453, rawContract.address) + const token42161 = getTokenByChainAndAddress(42161, rawContract.address) + + return ( + isYieldToken(token1) || + isYieldToken(token8453) || + isYieldToken(token42161) + ) + }) + .sort( + (a, b) => + new Date(b.metadata.blockTimestamp).getTime() - + new Date(a.metadata.blockTimestamp).getTime(), + ) + + let prices: Record = {} + try { + prices = mapKeys( + await fetchCoingeckoPrices( + ['ethereum', 'bitcoin', wsteth?.extensions.coingeckoId].filter( + (str) => str !== undefined, + ), + ['btc', 'eth', 'usd'], + ), + (_, key) => mapCoingeckoIdToSymbol(key), + ) + } catch (error) { + console.log(JSON.stringify(error, null, 2)) + console.error('Failed to fetch coingecko prices', error) + } + + const openPositions = history.filter( + (position) => position.metrics?.positionStatus === 'open', + ) + + const averages = calculateAverageEntryPrice(openPositions) + + const open = uniqBy( + openPositions.map((position) => ({ + ...position, + trade: { + ...position.trade, + underlyingAssetUnitPrice: averages[position.metrics!.tokenAddress], + }, + })), + 'metrics.tokenAddress', + ) + + return NextResponse.json( + { open, history, stats: prices }, + { + status: 200, + headers: { + 'Cache-Control': 'public, max-age=1, stale-while-revalidate=1', + }, + }, + ) + } catch (error) { + console.log(JSON.stringify(error, null, 2)) + return NextResponse.json(error, { status: 500 }) + } +} diff --git a/src/app/api/leverage/history/route.ts b/src/app/api/leverage/history/route.ts index 955784278..3078f1099 100644 --- a/src/app/api/leverage/history/route.ts +++ b/src/app/api/leverage/history/route.ts @@ -13,6 +13,7 @@ import { GetApiV2UserAddressPositionsQueryParamsChainIdEnum as ApiChainId, getApiV2PriceCoingeckoSimplePrice, getApiV2UserAddressPositions, + GetApiV2UserAddressPositions200, } from '@/gen' type TokenTransferRequest = { @@ -53,12 +54,12 @@ export async function POST(req: NextRequest) { const USUI = getTokenByChainAndSymbol(chainId, 'uSUI') const USOL = getTokenByChainAndSymbol(chainId, 'uSOL') - const positions = await getApiV2UserAddressPositions( + const { data: positions } = await getApiV2UserAddressPositions( { address: user }, { chainId: chainId.toString() as ApiChainId }, ) - const history = positions.data + const history = positions .filter(({ rawContract }) => { const token = getTokenByChainAndAddress(chainId, rawContract.address) @@ -85,7 +86,7 @@ export async function POST(req: NextRequest) { trade: { ...position.trade, underlyingAssetUnitPrice: averages[position.metrics!.tokenAddress], - }, + } as GetApiV2UserAddressPositions200[number]['trade'], })), 'metrics.tokenAddress', ) @@ -111,8 +112,9 @@ export async function POST(req: NextRequest) { const stats = open.reduce( (acc, position) => ({ ...acc, - ...(position.trade.underlyingAssetSymbol && - position.trade.underlyingAssetUnitPriceDenominator + ...(position.trade?.underlyingAssetUnitPrice && + position.trade?.underlyingAssetUnitPriceDenominator && + position.trade?.underlyingAssetSymbol ? { [`${position.trade.underlyingAssetSymbol}-${position.trade.underlyingAssetUnitPriceDenominator}`]: prices[position.trade.underlyingAssetSymbol.toLowerCase()][ diff --git a/src/app/earn/components/balance-card.tsx b/src/app/earn/components/balance-card.tsx index 6fa21ca51..c6951b333 100644 --- a/src/app/earn/components/balance-card.tsx +++ b/src/app/earn/components/balance-card.tsx @@ -2,53 +2,65 @@ import { getTokenByChainAndAddress, isAddressEqual, } from '@indexcoop/tokenlists' -import { motion } from 'framer-motion' import Link from 'next/link' import { useMemo } from 'react' import { formatUnits } from 'viem' -import { GetApiV2ProductsEarn200 } from '@/gen' +import { GetApiV2ProductsEarn200, GetApiV2UserAddressPositions200 } from '@/gen' import { TokenBalance } from '@/lib/hooks/use-balance' import { formatAmount } from '@/lib/utils' - -const BoxedData = ({ label, value }: { label: string; value: string }) => ( -
-

{label}

-
-

{value}

-
-
-) +import { SkeletonLoader } from '@/lib/utils/skeleton-loader' +import { cn } from '@/lib/utils/tailwind' const Position = ({ balance, product, + position, + isLoading, }: { balance: TokenBalance product?: GetApiV2ProductsEarn200[number] + position?: GetApiV2UserAddressPositions200[number] + isLoading?: boolean }) => { const token = useMemo( () => getTokenByChainAndAddress(product?.chainId, product?.tokenAddress), [product], ) + const accruedYield = useMemo(() => { + if (!product || !token) return 0 + + if (position && position.metrics) { + return ( + (position.metrics.endingUnits ?? 0) * product.metrics.nav - + (position.metrics.endingPositionCost ?? 0) + ) + } + return 0 + }, [product, token, position]) + return product && token ? ( - -

{product.name}

-

+

+ + {product.name} + + $ {formatAmount( Number(formatUnits(balance.value, token.decimals)) * product.metrics.nav, )} -

- +
+ {isLoading ? ( + + ) : ( + + ${formatAmount(accruedYield)} + + )} +
) : ( <> @@ -86,10 +98,17 @@ const calculateEffectiveAPY = ( export type BalanceCardProps = { products: GetApiV2ProductsEarn200 + positions: GetApiV2UserAddressPositions200 balances: TokenBalance[] + isLoading: boolean } -export const BalanceCard = ({ products, balances }: BalanceCardProps) => { +export const BalanceCard = ({ + products, + positions, + balances, + isLoading, +}: BalanceCardProps) => { const deposits = useMemo( () => balances.reduce((acc, curr) => { @@ -114,14 +133,41 @@ export const BalanceCard = ({ products, balances }: BalanceCardProps) => { [products, balances], ) + const accruedYield = useMemo( + () => + products.reduce((acc, curr) => { + const position = positions.find((p) => + isAddressEqual(p.metrics?.tokenAddress, curr.tokenAddress), + ) + if (position && position.metrics) { + return ( + acc + + (position.metrics.endingUnits ?? 0) * curr.metrics.nav - + (position.metrics.endingPositionCost ?? 0) + ) + } + + return acc + }, 0), + [products, positions], + ) + return ( - +

My Earn

-

- My positions -

+
+

+ My positions +

+

+ Total deposited +

+

+ Accrued yield +

+
{balances.map((balance) => ( @@ -131,27 +177,62 @@ export const BalanceCard = ({ products, balances }: BalanceCardProps) => { isAddressEqual(p.tokenAddress, balance.token), )} balance={balance} + position={positions.find((p) => + isAddressEqual(p.metrics?.tokenAddress, balance.token), + )} + isLoading={isLoading} />
))}
-
-
-

Total Deposits

+
+
+

Total Deposits

${formatAmount(deposits)}

-
- - {/* */} +
+
+

+ Net APY +

+
+

+ {formatAmount(calculateEffectiveAPY(products, balances))}% +

+
+
+
+

+ Accrued Yield +

+ + {isLoading ? ( + + ) : ( +
+

+ ${formatAmount(accruedYield)} +

+
+ )} +
- +
) } diff --git a/src/app/earn/components/product-card.tsx b/src/app/earn/components/product-card.tsx index ad381574c..928aecb92 100644 --- a/src/app/earn/components/product-card.tsx +++ b/src/app/earn/components/product-card.tsx @@ -5,6 +5,7 @@ import { FC, ReactNode } from 'react' import { ProductTitlePill } from '@/app/earn/components/product-pill' import { ProductTag } from '@/app/earn/components/product-tag' +import { Positions } from '@/app/store/positions-atom' import { GetApiV2ProductsEarn200 } from '@/gen' import { formatAmount } from '@/lib/utils' @@ -14,6 +15,7 @@ export type ProductCardProps = { icon: ReactNode } product: GetApiV2ProductsEarn200[number] + position?: Positions['open'][number] } export const ProductCard: FC = ({ product, pill }) => { diff --git a/src/app/earn/page.tsx b/src/app/earn/page.tsx index a9cdba4db..84d7fa119 100644 --- a/src/app/earn/page.tsx +++ b/src/app/earn/page.tsx @@ -1,8 +1,11 @@ 'use client' import { ArrowPathIcon } from '@heroicons/react/20/solid' -import { AnimatePresence } from 'framer-motion' +import { useQuery } from '@tanstack/react-query' import { useEffect } from 'react' +import { useAccount } from 'wagmi' + +import { Positions } from '@/app/store/positions-atom' import { BalanceCard } from './components/balance-card' import { ProductCard } from './components/product-card' @@ -11,6 +14,8 @@ import { useEarnContext } from './provider' export default function Page() { const { products, balances } = useEarnContext() + const { address: user } = useAccount() + useEffect(() => { document.body.classList.add('dark', 'bg-ic-black') return () => { @@ -18,15 +23,42 @@ export default function Page() { } }, []) + const { + data: { open }, + isFetching, + } = useQuery({ + initialData: { + open: [], + history: [], + stats: {}, + }, + enabled: Boolean(user), + queryKey: ['earn-history', user], + queryFn: async () => { + const response = await fetch('/api/earn/history', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + user, + }), + }) + return response.json() as Promise + }, + }) + return (
- - {balances.length > 0 && ( - - )} - - + {balances.length > 0 && ( + + )}

Strategies @@ -36,6 +68,10 @@ export default function Page() { + position.metrics?.tokenAddress === p.tokenAddress, + )} pill={ ['wsteth15x', 'iceth'].includes(p.id) ? { diff --git a/src/app/leverage/components/portfolio-widget/portfolio-widget.tsx b/src/app/leverage/components/portfolio-widget/portfolio-widget.tsx index e66a18ad0..defedb73e 100644 --- a/src/app/leverage/components/portfolio-widget/portfolio-widget.tsx +++ b/src/app/leverage/components/portfolio-widget/portfolio-widget.tsx @@ -15,7 +15,7 @@ import { getLeverageTokens } from '@/app/leverage/constants' import { EnrichedToken } from '@/app/leverage/types' import { fetchLeverageTokenPrices } from '@/app/leverage/utils/fetch-leverage-token-prices' import { getLeverageType } from '@/app/leverage/utils/get-leverage-type' -import { fetchPositionsAtom } from '@/app/store/positions-atom' +import { fetchLeveragePositionsAtom } from '@/app/store/positions-atom' import { ETH } from '@/constants/tokens' import { useAnalytics } from '@/lib/hooks/use-analytics' import { useBalances } from '@/lib/hooks/use-balance' @@ -31,7 +31,7 @@ const OpenPositions = () => { undefined, ) const { queryParams, updateQueryParams } = useQueryParams() - const fetchPositions = useSetAtom(fetchPositionsAtom) + const fetchPositions = useSetAtom(fetchLeveragePositionsAtom) const { logEvent } = useAnalytics() const indexTokenAddresses = useMemo(() => { diff --git a/src/app/store/positions-atom.ts b/src/app/store/positions-atom.ts index d7d85d068..840adf460 100644 --- a/src/app/store/positions-atom.ts +++ b/src/app/store/positions-atom.ts @@ -18,7 +18,7 @@ const positionsAtomDefaultValue: Positions = { export const positionsAtom = atom(positionsAtomDefaultValue) -export const fetchPositionsAtom = atom( +export const fetchLeveragePositionsAtom = atom( null, async (_, set, address: string, chainId: number) => { try { From 3ad2c952289d6cc746f96d2269f2e5327fac304f Mon Sep 17 00:00:00 2001 From: Toki Date: Fri, 20 Jun 2025 15:08:45 +0200 Subject: [PATCH 2/8] fiat vs. eth switch --- public/assets/fiat.svg | 3 + src/app/earn/components/balance-card.tsx | 79 ++++++++++++++----- .../components/portfolio-widget/columns.tsx | 2 +- 3 files changed, 64 insertions(+), 20 deletions(-) create mode 100644 public/assets/fiat.svg diff --git a/public/assets/fiat.svg b/public/assets/fiat.svg new file mode 100644 index 000000000..9b5f0d491 --- /dev/null +++ b/public/assets/fiat.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/app/earn/components/balance-card.tsx b/src/app/earn/components/balance-card.tsx index c6951b333..8135ffe09 100644 --- a/src/app/earn/components/balance-card.tsx +++ b/src/app/earn/components/balance-card.tsx @@ -1,9 +1,11 @@ +import { Button } from '@headlessui/react' import { getTokenByChainAndAddress, isAddressEqual, } from '@indexcoop/tokenlists' +import Image from 'next/image' import Link from 'next/link' -import { useMemo } from 'react' +import { FC, useMemo, useState } from 'react' import { formatUnits } from 'viem' import { GetApiV2ProductsEarn200, GetApiV2UserAddressPositions200 } from '@/gen' @@ -12,17 +14,38 @@ import { formatAmount } from '@/lib/utils' import { SkeletonLoader } from '@/lib/utils/skeleton-loader' import { cn } from '@/lib/utils/tailwind' -const Position = ({ - balance, - product, - position, - isLoading, -}: { +const DenominatorSwitch: FC<{ + selected: 'fiat' | 'eth' + onSelect: (denominator: 'fiat' | 'eth') => void +}> = ({ selected, onSelect }) => { + return ( + + ) +} + +const Position: FC<{ balance: TokenBalance product?: GetApiV2ProductsEarn200[number] position?: GetApiV2UserAddressPositions200[number] isLoading?: boolean -}) => { + denominator: 'fiat' | 'eth' +}> = ({ balance, product, position, isLoading, denominator }) => { const token = useMemo( () => getTokenByChainAndAddress(product?.chainId, product?.tokenAddress), [product], @@ -32,13 +55,15 @@ const Position = ({ if (!product || !token) return 0 if (position && position.metrics) { - return ( - (position.metrics.endingUnits ?? 0) * product.metrics.nav - - (position.metrics.endingPositionCost ?? 0) - ) + return denominator === 'fiat' + ? (position.metrics.endingUnits ?? 0) * product.metrics.nav - + (position.metrics.endingPositionCost ?? 0) + : ((position.metrics.endingUnits ?? 0) * product.metrics.nav - + (position.metrics.endingPositionCost ?? 0)) / + product.metrics.nav } return 0 - }, [product, token, position]) + }, [product, token, position, denominator]) return product && token ? ( @@ -57,7 +82,8 @@ const Position = ({ ) : ( - ${formatAmount(accruedYield)} + {denominator === 'fiat' ? '$' : 'Ξ'} + {formatAmount(accruedYield, denominator === 'fiat' ? 2 : 6)} )}

@@ -109,6 +135,7 @@ export const BalanceCard = ({ balances, isLoading, }: BalanceCardProps) => { + const [denominator, setDenominator] = useState<'fiat' | 'eth'>('fiat') const deposits = useMemo( () => balances.reduce((acc, curr) => { @@ -142,14 +169,18 @@ export const BalanceCard = ({ if (position && position.metrics) { return ( acc + - (position.metrics.endingUnits ?? 0) * curr.metrics.nav - - (position.metrics.endingPositionCost ?? 0) + (denominator === 'fiat' + ? (position.metrics.endingUnits ?? 0) * curr.metrics.nav - + (position.metrics.endingPositionCost ?? 0) + : ((position.metrics.endingUnits ?? 0) * curr.metrics.nav - + (position.metrics.endingPositionCost ?? 0)) / + curr.metrics.nav) ) } return acc }, 0), - [products, positions], + [products, positions, denominator], ) return ( @@ -181,6 +212,7 @@ export const BalanceCard = ({ isAddressEqual(p.metrics?.tokenAddress, balance.token), )} isLoading={isLoading} + denominator={denominator} />
))} @@ -189,7 +221,15 @@ export const BalanceCard = ({
-

Total Deposits

+
+

+ Total Deposits +

+ +

${formatAmount(deposits)}

@@ -226,7 +266,8 @@ export const BalanceCard = ({ )} >

- ${formatAmount(accruedYield)} + {denominator === 'fiat' ? '$' : 'Ξ'} + {formatAmount(accruedYield, denominator === 'fiat' ? 2 : 6)}

)} diff --git a/src/app/leverage/components/portfolio-widget/columns.tsx b/src/app/leverage/components/portfolio-widget/columns.tsx index ade5f13f4..3abc5790e 100644 --- a/src/app/leverage/components/portfolio-widget/columns.tsx +++ b/src/app/leverage/components/portfolio-widget/columns.tsx @@ -56,7 +56,7 @@ const getAction = ( ) { return 'Open' } else if ( - data.from.toLowerCase() === user?.toLowerCase() && + data.from?.toLowerCase() === user?.toLowerCase() && data.to === zeroAddress ) { return 'Close' From c861fbe74d6bcc7981a544f774494ae24a077999 Mon Sep 17 00:00:00 2001 From: Toki Date: Fri, 20 Jun 2025 15:22:54 +0200 Subject: [PATCH 3/8] apply fiat - nav switch to every $ value in balance card --- src/app/earn/components/balance-card.tsx | 35 +++++++++++++++++------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/app/earn/components/balance-card.tsx b/src/app/earn/components/balance-card.tsx index 8135ffe09..6bda19877 100644 --- a/src/app/earn/components/balance-card.tsx +++ b/src/app/earn/components/balance-card.tsx @@ -30,10 +30,15 @@ const DenominatorSwitch: FC<{ )} />
- +
- +
) @@ -72,10 +77,15 @@ const Position: FC<{ {product.name} - $ + {denominator === 'fiat' ? '$' : 'Ξ'} {formatAmount( - Number(formatUnits(balance.value, token.decimals)) * - product.metrics.nav, + denominator === 'fiat' + ? Number(formatUnits(balance.value, token.decimals)) * + product.metrics.nav + : (Number(formatUnits(balance.value, token.decimals)) * + product.metrics.nav) / + product.metrics.nav, + denominator === 'fiat' ? 2 : 6, )} {isLoading ? ( @@ -151,13 +161,17 @@ export const BalanceCard = ({ if (product && token) { return ( acc + - Number(formatUnits(curr.value, token.decimals)) * - product.metrics.nav + (denominator === 'fiat' + ? Number(formatUnits(curr.value, token.decimals)) * + product.metrics.nav + : (Number(formatUnits(curr.value, token.decimals)) * + product.metrics.nav) / + product.metrics.nav) ) } return acc }, 0), - [products, balances], + [products, balances, denominator], ) const accruedYield = useMemo( @@ -219,7 +233,7 @@ export const BalanceCard = ({
-
+

@@ -231,7 +245,8 @@ export const BalanceCard = ({ />

- ${formatAmount(deposits)} + {denominator === 'fiat' ? '$' : 'Ξ'} + {formatAmount(deposits, denominator === 'fiat' ? 2 : 6)}

From cecef649be98bcd563169f7dd343ada91caf63f5 Mon Sep 17 00:00:00 2001 From: Toki Date: Fri, 20 Jun 2025 15:31:25 +0200 Subject: [PATCH 4/8] increase total deposits card size on medium or larger screens --- src/app/earn/components/balance-card.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/earn/components/balance-card.tsx b/src/app/earn/components/balance-card.tsx index 6bda19877..faa677df0 100644 --- a/src/app/earn/components/balance-card.tsx +++ b/src/app/earn/components/balance-card.tsx @@ -233,7 +233,7 @@ export const BalanceCard = ({
-
+

From 53ea22380e4a59539aa818a948fd70661b8d0ce4 Mon Sep 17 00:00:00 2001 From: Toki Date: Fri, 20 Jun 2025 15:42:13 +0200 Subject: [PATCH 5/8] accrued yield right --- src/app/earn/components/balance-card.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/earn/components/balance-card.tsx b/src/app/earn/components/balance-card.tsx index faa677df0..98e6fd88b 100644 --- a/src/app/earn/components/balance-card.tsx +++ b/src/app/earn/components/balance-card.tsx @@ -273,14 +273,14 @@ export const BalanceCard = ({

{isLoading ? ( - + ) : (
-

+

{denominator === 'fiat' ? '$' : 'Ξ'} {formatAmount(accruedYield, denominator === 'fiat' ? 2 : 6)}

From 4f407a9783b3ac70ead9bfd0062144237181b7c0 Mon Sep 17 00:00:00 2001 From: Toki Date: Tue, 8 Jul 2025 15:08:27 +0200 Subject: [PATCH 6/8] update to new formula --- src/app/earn/components/balance-card.tsx | 25 ++++++++++++++---------- src/app/earn/page.tsx | 3 ++- src/app/store/positions-atom.ts | 4 +--- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/app/earn/components/balance-card.tsx b/src/app/earn/components/balance-card.tsx index 98e6fd88b..1fa3b81bc 100644 --- a/src/app/earn/components/balance-card.tsx +++ b/src/app/earn/components/balance-card.tsx @@ -136,6 +136,7 @@ export type BalanceCardProps = { products: GetApiV2ProductsEarn200 positions: GetApiV2UserAddressPositions200 balances: TokenBalance[] + stats: Record> isLoading: boolean } @@ -143,6 +144,7 @@ export const BalanceCard = ({ products, positions, balances, + stats, isLoading, }: BalanceCardProps) => { const [denominator, setDenominator] = useState<'fiat' | 'eth'>('fiat') @@ -171,7 +173,7 @@ export const BalanceCard = ({ } return acc }, 0), - [products, balances, denominator], + [products, balances, denominator, stats], ) const accruedYield = useMemo( @@ -181,15 +183,18 @@ export const BalanceCard = ({ isAddressEqual(p.metrics?.tokenAddress, curr.tokenAddress), ) if (position && position.metrics) { - return ( - acc + - (denominator === 'fiat' - ? (position.metrics.endingUnits ?? 0) * curr.metrics.nav - - (position.metrics.endingPositionCost ?? 0) - : ((position.metrics.endingUnits ?? 0) * curr.metrics.nav - - (position.metrics.endingPositionCost ?? 0)) / - curr.metrics.nav) - ) + const underlyingTokenId = + getTokenByChainAndAddress(curr.chainId, curr.tokenAddress) + ?.extensions.coingeckoId ?? 'eth' + const currentRatio = + curr.metrics.nav / (stats[underlyingTokenId]?.usd ?? 1) + + const yieldETH = + (position.metrics.endingUnits ?? 0) * + (currentRatio - (position.metrics.avgEntryUnderlyingPerToken ?? 0)) + const yieldUSD = yieldETH * (stats.eth?.usd ?? 0) + + return acc + (denominator === 'fiat' ? yieldUSD : yieldETH) } return acc diff --git a/src/app/earn/page.tsx b/src/app/earn/page.tsx index 84d7fa119..c85a17a64 100644 --- a/src/app/earn/page.tsx +++ b/src/app/earn/page.tsx @@ -24,7 +24,7 @@ export default function Page() { }, []) const { - data: { open }, + data: { open, stats }, isFetching, } = useQuery({ initialData: { @@ -56,6 +56,7 @@ export default function Page() { products={products} balances={balances} positions={open} + stats={stats} isLoading={isFetching} /> )} diff --git a/src/app/store/positions-atom.ts b/src/app/store/positions-atom.ts index 840adf460..b1eecd190 100644 --- a/src/app/store/positions-atom.ts +++ b/src/app/store/positions-atom.ts @@ -5,9 +5,7 @@ import { GetApiV2UserAddressPositions200 } from '@/gen' export type Positions = { open: GetApiV2UserAddressPositions200 history: GetApiV2UserAddressPositions200 - stats: { - [key: string]: number - } + stats: Record> } const positionsAtomDefaultValue: Positions = { From c68fa12db62d9d283be23ffacebe29a402c41a93 Mon Sep 17 00:00:00 2001 From: Toki Date: Tue, 8 Jul 2025 15:28:13 +0200 Subject: [PATCH 7/8] calculateAccruedYield function --- src/app/earn/components/balance-card.tsx | 62 ++++++++++++++++-------- 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/src/app/earn/components/balance-card.tsx b/src/app/earn/components/balance-card.tsx index 1fa3b81bc..2cdac4aed 100644 --- a/src/app/earn/components/balance-card.tsx +++ b/src/app/earn/components/balance-card.tsx @@ -14,6 +14,31 @@ import { formatAmount } from '@/lib/utils' import { SkeletonLoader } from '@/lib/utils/skeleton-loader' import { cn } from '@/lib/utils/tailwind' +const calculateAccruedYield = ({ + product, + position, + denominator, + stats = {}, +}: { + product: GetApiV2ProductsEarn200[number] + stats: Record> + position: GetApiV2UserAddressPositions200[number] + denominator: 'fiat' | 'eth' +}) => { + const underlyingTokenId = + getTokenByChainAndAddress(product.chainId, product.tokenAddress)?.extensions + .coingeckoId ?? 'eth' + const currentRatio = + product.metrics.nav / (stats[underlyingTokenId]?.usd ?? 1) + + const yieldETH = + (position.metrics?.endingUnits ?? 0) * + (currentRatio - (position.metrics?.avgEntryUnderlyingPerToken ?? 0)) + const yieldUSD = yieldETH * (stats.eth?.usd ?? 0) + + return denominator === 'fiat' ? yieldUSD : yieldETH +} + const DenominatorSwitch: FC<{ selected: 'fiat' | 'eth' onSelect: (denominator: 'fiat' | 'eth') => void @@ -50,7 +75,8 @@ const Position: FC<{ position?: GetApiV2UserAddressPositions200[number] isLoading?: boolean denominator: 'fiat' | 'eth' -}> = ({ balance, product, position, isLoading, denominator }) => { + stats: Record> +}> = ({ balance, product, position, isLoading, denominator, stats = {} }) => { const token = useMemo( () => getTokenByChainAndAddress(product?.chainId, product?.tokenAddress), [product], @@ -60,12 +86,12 @@ const Position: FC<{ if (!product || !token) return 0 if (position && position.metrics) { - return denominator === 'fiat' - ? (position.metrics.endingUnits ?? 0) * product.metrics.nav - - (position.metrics.endingPositionCost ?? 0) - : ((position.metrics.endingUnits ?? 0) * product.metrics.nav - - (position.metrics.endingPositionCost ?? 0)) / - product.metrics.nav + return calculateAccruedYield({ + product, + position, + denominator, + stats, + }) } return 0 }, [product, token, position, denominator]) @@ -182,24 +208,19 @@ export const BalanceCard = ({ const position = positions.find((p) => isAddressEqual(p.metrics?.tokenAddress, curr.tokenAddress), ) - if (position && position.metrics) { - const underlyingTokenId = - getTokenByChainAndAddress(curr.chainId, curr.tokenAddress) - ?.extensions.coingeckoId ?? 'eth' - const currentRatio = - curr.metrics.nav / (stats[underlyingTokenId]?.usd ?? 1) - const yieldETH = - (position.metrics.endingUnits ?? 0) * - (currentRatio - (position.metrics.avgEntryUnderlyingPerToken ?? 0)) - const yieldUSD = yieldETH * (stats.eth?.usd ?? 0) - - return acc + (denominator === 'fiat' ? yieldUSD : yieldETH) + if (position && position.metrics) { + return calculateAccruedYield({ + product: curr, + position, + denominator, + stats, + }) } return acc }, 0), - [products, positions, denominator], + [products, positions, denominator, stats], ) return ( @@ -223,6 +244,7 @@ export const BalanceCard = ({ {balances.map((balance) => (
isAddressEqual(p.tokenAddress, balance.token), )} From a5d4934f4dbe9e48fa2f218c1f650c16109db965 Mon Sep 17 00:00:00 2001 From: Toki Date: Tue, 8 Jul 2025 15:50:08 +0200 Subject: [PATCH 8/8] fix typing issues --- src/app/api/earn/history/route.ts | 2 +- src/app/earn/components/balance-card.tsx | 26 ++++++++++++------------ src/app/earn/components/product-card.tsx | 4 ++-- src/app/earn/page.tsx | 10 ++++----- src/app/store/positions-atom.ts | 18 +++++++++++----- 5 files changed, 34 insertions(+), 26 deletions(-) diff --git a/src/app/api/earn/history/route.ts b/src/app/api/earn/history/route.ts index 1c5d5f01f..babaea73e 100644 --- a/src/app/api/earn/history/route.ts +++ b/src/app/api/earn/history/route.ts @@ -106,7 +106,7 @@ export async function POST(req: NextRequest) { ) return NextResponse.json( - { open, history, stats: prices }, + { open, history, prices }, { status: 200, headers: { diff --git a/src/app/earn/components/balance-card.tsx b/src/app/earn/components/balance-card.tsx index 2cdac4aed..cbd58fa68 100644 --- a/src/app/earn/components/balance-card.tsx +++ b/src/app/earn/components/balance-card.tsx @@ -18,10 +18,10 @@ const calculateAccruedYield = ({ product, position, denominator, - stats = {}, + prices = {}, }: { product: GetApiV2ProductsEarn200[number] - stats: Record> + prices: Record> position: GetApiV2UserAddressPositions200[number] denominator: 'fiat' | 'eth' }) => { @@ -29,12 +29,12 @@ const calculateAccruedYield = ({ getTokenByChainAndAddress(product.chainId, product.tokenAddress)?.extensions .coingeckoId ?? 'eth' const currentRatio = - product.metrics.nav / (stats[underlyingTokenId]?.usd ?? 1) + product.metrics.nav / (prices[underlyingTokenId]?.usd ?? 1) const yieldETH = (position.metrics?.endingUnits ?? 0) * (currentRatio - (position.metrics?.avgEntryUnderlyingPerToken ?? 0)) - const yieldUSD = yieldETH * (stats.eth?.usd ?? 0) + const yieldUSD = yieldETH * (prices.eth?.usd ?? 0) return denominator === 'fiat' ? yieldUSD : yieldETH } @@ -75,8 +75,8 @@ const Position: FC<{ position?: GetApiV2UserAddressPositions200[number] isLoading?: boolean denominator: 'fiat' | 'eth' - stats: Record> -}> = ({ balance, product, position, isLoading, denominator, stats = {} }) => { + prices: Record> +}> = ({ balance, product, position, isLoading, denominator, prices = {} }) => { const token = useMemo( () => getTokenByChainAndAddress(product?.chainId, product?.tokenAddress), [product], @@ -90,7 +90,7 @@ const Position: FC<{ product, position, denominator, - stats, + prices, }) } return 0 @@ -162,7 +162,7 @@ export type BalanceCardProps = { products: GetApiV2ProductsEarn200 positions: GetApiV2UserAddressPositions200 balances: TokenBalance[] - stats: Record> + prices: Record> isLoading: boolean } @@ -170,7 +170,7 @@ export const BalanceCard = ({ products, positions, balances, - stats, + prices, isLoading, }: BalanceCardProps) => { const [denominator, setDenominator] = useState<'fiat' | 'eth'>('fiat') @@ -199,7 +199,7 @@ export const BalanceCard = ({ } return acc }, 0), - [products, balances, denominator, stats], + [products, balances, denominator, prices], ) const accruedYield = useMemo( @@ -214,13 +214,13 @@ export const BalanceCard = ({ product: curr, position, denominator, - stats, + prices, }) } return acc }, 0), - [products, positions, denominator, stats], + [products, positions, denominator, prices], ) return ( @@ -244,7 +244,7 @@ export const BalanceCard = ({ {balances.map((balance) => (
isAddressEqual(p.tokenAddress, balance.token), )} diff --git a/src/app/earn/components/product-card.tsx b/src/app/earn/components/product-card.tsx index 928aecb92..e9472ab94 100644 --- a/src/app/earn/components/product-card.tsx +++ b/src/app/earn/components/product-card.tsx @@ -5,7 +5,7 @@ import { FC, ReactNode } from 'react' import { ProductTitlePill } from '@/app/earn/components/product-pill' import { ProductTag } from '@/app/earn/components/product-tag' -import { Positions } from '@/app/store/positions-atom' +import { LeveragePositions } from '@/app/store/positions-atom' import { GetApiV2ProductsEarn200 } from '@/gen' import { formatAmount } from '@/lib/utils' @@ -15,7 +15,7 @@ export type ProductCardProps = { icon: ReactNode } product: GetApiV2ProductsEarn200[number] - position?: Positions['open'][number] + position?: LeveragePositions['open'][number] } export const ProductCard: FC = ({ product, pill }) => { diff --git a/src/app/earn/page.tsx b/src/app/earn/page.tsx index c85a17a64..a8fb8c265 100644 --- a/src/app/earn/page.tsx +++ b/src/app/earn/page.tsx @@ -5,7 +5,7 @@ import { useQuery } from '@tanstack/react-query' import { useEffect } from 'react' import { useAccount } from 'wagmi' -import { Positions } from '@/app/store/positions-atom' +import { EarnPositions } from '@/app/store/positions-atom' import { BalanceCard } from './components/balance-card' import { ProductCard } from './components/product-card' @@ -24,13 +24,13 @@ export default function Page() { }, []) const { - data: { open, stats }, + data: { open, prices }, isFetching, } = useQuery({ initialData: { open: [], history: [], - stats: {}, + prices: {}, }, enabled: Boolean(user), queryKey: ['earn-history', user], @@ -44,7 +44,7 @@ export default function Page() { user, }), }) - return response.json() as Promise + return response.json() as Promise }, }) @@ -56,7 +56,7 @@ export default function Page() { products={products} balances={balances} positions={open} - stats={stats} + prices={prices} isLoading={isFetching} /> )} diff --git a/src/app/store/positions-atom.ts b/src/app/store/positions-atom.ts index b1eecd190..f97424218 100644 --- a/src/app/store/positions-atom.ts +++ b/src/app/store/positions-atom.ts @@ -2,19 +2,27 @@ import { atom } from 'jotai' import { GetApiV2UserAddressPositions200 } from '@/gen' -export type Positions = { +export type LeveragePositions = { open: GetApiV2UserAddressPositions200 history: GetApiV2UserAddressPositions200 - stats: Record> + stats: { + [key: string]: number + } } -const positionsAtomDefaultValue: Positions = { +export type EarnPositions = { + open: GetApiV2UserAddressPositions200 + history: GetApiV2UserAddressPositions200 + prices: Record> +} + +const positionsAtomDefaultValue: LeveragePositions = { open: [], history: [], stats: {}, } -export const positionsAtom = atom(positionsAtomDefaultValue) +export const positionsAtom = atom(positionsAtomDefaultValue) export const fetchLeveragePositionsAtom = atom( null, @@ -28,7 +36,7 @@ export const fetchLeveragePositionsAtom = atom( chainId, }), }) - ).json()) as Positions + ).json()) as LeveragePositions set(positionsAtom, positions)