From 8aaac9f77f07c5063301b6c48f28bd81e43f174f Mon Sep 17 00:00:00 2001 From: eli-d <64763513+eli-d@users.noreply.github.com> Date: Wed, 9 Oct 2024 15:52:15 +1030 Subject: [PATCH] add leo unclaimed rewards to displayed amounts --- web/src/app/stake/MyPositions.tsx | 112 ++++++++++++++++++++++++++---- web/src/app/stake/pool/page.tsx | 102 ++++++++++++++++++++++++--- web/src/lib/amounts.ts | 17 +++++ 3 files changed, 209 insertions(+), 22 deletions(-) diff --git a/web/src/app/stake/MyPositions.tsx b/web/src/app/stake/MyPositions.tsx index 25c11f8e..33c1f04f 100644 --- a/web/src/app/stake/MyPositions.tsx +++ b/web/src/app/stake/MyPositions.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import List from "@/assets/icons/list.svg"; import Grid from "@/assets/icons/grid.svg"; @@ -26,6 +26,10 @@ import { usePositions } from "@/hooks/usePostions"; import { LoaderIcon } from "lucide-react"; import { useTokens, type Token, getTokenFromAddress } from "@/config/tokens"; import { useContracts } from "@/config/contracts"; +import { simulateContract } from "wagmi/actions"; +import config from "@/config"; +import { getFormattedPriceFromUnscaledAmount } from "@/lib/amounts"; +import { CampaignPrices } from "./pool/page"; export const MyPositions = () => { const [displayMode, setDisplayMode] = useState<"list" | "grid">("list"); @@ -147,26 +151,108 @@ export const MyPositions = () => { args: collectSeawaterArgs, }); + const { data: unclaimedLeoRewardsData } = useSimulateContract({ + address: leoContract.address, + abi: leoContract.abi, + functionName: "collect", + args: [vestedPositions, campaignIds], + }); + + // campaignTokenPrices is the price of each token used in a campaign for this position + const [campaignTokenPrices, setCampaignTokenPrices] = + useState({}); + useEffect(() => { + (async () => { + const prices: CampaignPrices = {}; + // no campaign rewards + if (!unclaimedLeoRewardsData) return prices; + for (const reward of unclaimedLeoRewardsData.result.campaignRewards) { + // looking at a seawater reward, not a leo reward + if (!("campaignToken" in reward)) continue; + const campaignToken = + reward.campaignToken.toLowerCase() as `0x${string}`; + // already seen this token + if (campaignToken in prices) continue; + // find token details + const token = getTokenFromAddress(chainId, campaignToken); + if (!token) { + console.warn("Token not found, skipping!", campaignToken); + continue; + } + // look up price + const { result: poolSqrtPriceX96 } = await simulateContract( + config.wagmiConfig, + { + address: ammContract.address, + abi: ammContract.abi, + functionName: "sqrtPriceX967B8F5FC5", + args: [token.address], + }, + ); + // store converted price + const tokenPrice = sqrtPriceX96ToPrice( + poolSqrtPriceX96, + token.decimals, + ); + prices[campaignToken] = { decimals: token.decimals, tokenPrice }; + } + setCampaignTokenPrices(prices); + })(); + }, [ + unclaimedLeoRewardsData, + setCampaignTokenPrices, + ammContract.abi, + ammContract.address, + chainId, + ]); + const unclaimedRewards = useMemo(() => { - if (!unclaimedRewardsData) return "$0.00"; + if (!(unclaimedRewardsData || unclaimedLeoRewardsData)) return "$0.00"; + + // Sum all Leo rewards, scaled by the price of their token + const campaignRewards = + unclaimedLeoRewardsData?.result.campaignRewards.reduce( + (acc, campaignReward) => { + if (!("campaignToken" in campaignReward)) return acc; + const campaignToken = + campaignReward.campaignToken.toLowerCase() as `0x${string}`; + if (!(campaignToken in campaignTokenPrices)) return acc; + const tokenDetails = campaignTokenPrices[campaignToken]; + const reward = getFormattedPriceFromUnscaledAmount( + campaignReward.rewards, + tokenDetails.decimals, + tokenDetails.tokenPrice, + fUSDC.decimals, + ); + return acc + reward; + }, + 0, + ) ?? 0; - const rewards = unclaimedRewardsData.result.reduce((p, c, i) => { - const token = getTokenFromAddress(chainId, nonVestedPositions[i].id); - // this should never happen as nonVestedPositions is passed to collect - if (!token) return 0; - const token0AmountScaled = - (Number(c.amount0) * Number(tokenPrice)) / - 10 ** (token.decimals + fUSDC.decimals); - const token1AmountScaled = Number(c.amount1) / 10 ** fUSDC.decimals; - return p + token0AmountScaled + token1AmountScaled; - }, 0); - return usdFormat(rewards); + // Sum regular rewards + const rewards = + unclaimedRewardsData?.result.reduce((p, c, i) => { + const token = getTokenFromAddress(chainId, nonVestedPositions[i].id); + // this should never happen as nonVestedPositions is passed to collect + if (!token) return 0; + const token0AmountScaled = getFormattedPriceFromUnscaledAmount( + c.amount0, + token.decimals, + tokenPrice, + fUSDC.decimals, + ); + const token1AmountScaled = Number(c.amount1) / 10 ** fUSDC.decimals; + return p + token0AmountScaled + token1AmountScaled; + }, 0) ?? 0; + return usdFormat(rewards + campaignRewards); }, [ unclaimedRewardsData, + unclaimedLeoRewardsData, tokenPrice, chainId, fUSDC.decimals, nonVestedPositions, + campaignTokenPrices, ]); const collectAll = useCallback(() => { diff --git a/web/src/app/stake/pool/page.tsx b/web/src/app/stake/pool/page.tsx index 7c09d864..eb838892 100644 --- a/web/src/app/stake/pool/page.tsx +++ b/web/src/app/stake/pool/page.tsx @@ -9,7 +9,7 @@ import { Badge } from "@/components/ui/badge"; import { Line } from "rc-progress"; import { motion } from "framer-motion"; import Link from "next/link"; -import { useCallback, useEffect, useMemo } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { graphql, useFragment } from "@/gql"; import { useGraphqlGlobal } from "@/hooks/useGraphql"; import { useFeatureFlag } from "@/hooks/useFeatureFlag"; @@ -21,7 +21,10 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { getFormattedPriceFromTick } from "@/lib/amounts"; +import { + getFormattedPriceFromTick, + getFormattedPriceFromUnscaledAmount, +} from "@/lib/amounts"; import { useStakeStore } from "@/stores/useStakeStore"; import { useSwapStore } from "@/stores/useSwapStore"; import { @@ -44,6 +47,12 @@ import { } from "@/config/tokens"; import { useContracts } from "@/config/contracts"; import { superpositionTestnet } from "@/config/chains"; +import { simulateContract } from "wagmi/actions"; +import config from "@/config"; + +export type CampaignPrices = { + [k: `0x${string}`]: { decimals: number; tokenPrice: bigint }; +}; const ManagePoolFragment = graphql(` fragment ManagePoolFragment on SeawaterPool { @@ -237,23 +246,98 @@ export default function PoolPage() { ], }); - const unclaimedRewards = useMemo(() => { - if (!unclaimedRewardsData || !positionId) return "$0.00"; + // campaignTokenPrices is the price of each token used in a campaign for this position + const [campaignTokenPrices, setCampaignTokenPrices] = + useState({}); + useEffect(() => { + (async () => { + const prices: CampaignPrices = {}; + // no campaign rewards + if (!unclaimedLeoRewardsData) return prices; + for (const reward of unclaimedLeoRewardsData.result.campaignRewards) { + // looking at a seawater reward, not a leo reward + if (!("campaignToken" in reward)) continue; + const campaignToken = + reward.campaignToken.toLowerCase() as `0x${string}`; + // already seen this token + if (campaignToken in prices) continue; + // find token details + const token = getTokenFromAddress(chainId, campaignToken); + if (!token) { + console.warn("Token not found, skipping!", campaignToken); + continue; + } + // look up price + const { result: poolSqrtPriceX96 } = await simulateContract( + config.wagmiConfig, + { + address: ammContract.address, + abi: ammContract.abi, + functionName: "sqrtPriceX967B8F5FC5", + args: [token.address], + }, + ); + // store converted price + const tokenPrice = sqrtPriceX96ToPrice( + poolSqrtPriceX96, + token.decimals, + ); + prices[campaignToken] = { decimals: token.decimals, tokenPrice }; + } + setCampaignTokenPrices(prices); + })(); + }, [ + unclaimedLeoRewardsData, + setCampaignTokenPrices, + ammContract.abi, + ammContract.address, + chainId, + ]); - const [{ amount0, amount1 }] = unclaimedRewardsData.result || [ + const unclaimedRewards = useMemo(() => { + if (!(unclaimedRewardsData || unclaimedLeoRewardsData) || !positionId) + return "$0.00"; + + // Sum all Leo rewards, scaled by the price of their token + const campaignRewards = + unclaimedLeoRewardsData?.result.campaignRewards.reduce( + (acc, campaignReward) => { + if (!("campaignToken" in campaignReward)) return acc; + const campaignToken = + campaignReward.campaignToken.toLowerCase() as `0x${string}`; + if (!(campaignToken in campaignTokenPrices)) return acc; + const tokenDetails = campaignTokenPrices[campaignToken]; + const reward = getFormattedPriceFromUnscaledAmount( + campaignReward.rewards, + tokenDetails.decimals, + tokenDetails.tokenPrice, + fUSDC.decimals, + ); + return acc + reward; + }, + 0, + ) ?? 0; + + // Sum regular rewards + const [{ amount0, amount1 }] = unclaimedRewardsData?.result || [ { amount0: 0n, amount1: 0n }, ]; - const token0AmountScaled = - (Number(amount0) * Number(tokenPrice)) / - 10 ** (token0.decimals + fUSDC.decimals); + const token0AmountScaled = getFormattedPriceFromUnscaledAmount( + amount0, + token0.decimals, + tokenPrice, + fUSDC.decimals, + ); const token1AmountScaled = Number(amount1) / 10 ** fUSDC.decimals; - return usdFormat(token0AmountScaled + token1AmountScaled); + return usdFormat(token0AmountScaled + token1AmountScaled + campaignRewards); }, [ unclaimedRewardsData, + unclaimedLeoRewardsData, fUSDC.decimals, positionId, token0.decimals, tokenPrice, + campaignTokenPrices, ]); const collect = useCallback( diff --git a/web/src/lib/amounts.ts b/web/src/lib/amounts.ts index 71c046f0..c5cf1d29 100644 --- a/web/src/lib/amounts.ts +++ b/web/src/lib/amounts.ts @@ -108,6 +108,22 @@ const getFormattedPriceFromAmount = ( decimalsFusdc: number, ): number => (Number(amount) * Number(price)) / 10 ** decimalsFusdc; +/** + * @description scale an unscaled amount by the price of the pool + * @param amount - unscaled token amount + * @param decimals - the decimals of the token + * @param price - the pool price as a regular number, scaled up by fUSDC decimals + * @param decimalsFusdc - the decimals of fUSDC + * @returns the scaled price amount in USD + */ +const getFormattedPriceFromUnscaledAmount = ( + amount: bigint | number, + decimals: number, + price: bigint | number, + decimalsFusdc: number, +): number => + (Number(amount) * Number(price)) / 10 ** (decimals + decimalsFusdc); + // convert a tick to a formatted price, scaled by decimals const getFormattedPriceFromTick = ( tick: number, @@ -162,6 +178,7 @@ export { snapAmountToDecimals, getTokenAmountFromFormattedString, getFormattedPriceFromAmount, + getFormattedPriceFromUnscaledAmount, getFormattedPriceFromTick, getUsdTokenAmountsForPosition, };