From f469f686f9d78e6f8b92376cb3ecdf16427d4bda Mon Sep 17 00:00:00 2001 From: Hayden Fan Date: Wed, 22 May 2024 17:47:02 +0800 Subject: [PATCH] feat(market): maket lp rewards (#253) * feat(market): maket lp rewards * wip: vdot * wip: ui --- .../GaugesChart/VotePercentChart.tsx | 8 +- apps/gauge/pages/index.tsx | 4 +- apps/gauge/pages/lock.tsx | 100 ++++++------- .../MarketAdd/MarketAddManualReviewModal.tsx | 2 +- .../MarketMintReviewModal.tsx | 2 +- .../MarketRedeemReviewModal.tsx | 2 +- .../MarketRemoveManualReviewModal.tsx | 2 +- .../MarketSwap/CurrencyInput.tsx | 2 + .../MarketRewards/MarketLPRewards.tsx | 63 ++++++++ .../MarketRewards/MarketRewards.tsx | 19 ++- .../MarketRewardsReviewModal.tsx | 6 +- .../Tables/MarketsTable/Cells/columns.tsx | 3 +- .../market/src/Token/implementations/index.ts | 1 + .../market/src/Token/implementations/vDOT.ts | 39 +++++ packages/wagmi/hooks/markets/config.ts | 18 +++ packages/wagmi/hooks/markets/index.ts | 1 + .../markets/markets-config/vDOT-NOV2024.ts | 60 ++++++++ .../wagmi/hooks/markets/useMarketRewards.ts | 140 ++++++++++++++++++ .../hooks/markets/useYtInterestAndRewards.ts | 2 +- 19 files changed, 407 insertions(+), 67 deletions(-) create mode 100644 apps/market/components/MarketSection/MarketRewards/MarketLPRewards.tsx create mode 100644 packages/market/src/Token/implementations/vDOT.ts create mode 100644 packages/wagmi/hooks/markets/markets-config/vDOT-NOV2024.ts create mode 100644 packages/wagmi/hooks/markets/useMarketRewards.ts diff --git a/apps/gauge/components/GaugesChart/VotePercentChart.tsx b/apps/gauge/components/GaugesChart/VotePercentChart.tsx index 475fd3ac5..fc274bd64 100644 --- a/apps/gauge/components/GaugesChart/VotePercentChart.tsx +++ b/apps/gauge/components/GaugesChart/VotePercentChart.tsx @@ -18,6 +18,8 @@ const COLORS = [ '#6BDF9EFF', '#F3E500FF', '#FF733EFF', + '#FF669DFF', + '#FFF176FF', ] export const VotePercentChart: FC = () => { @@ -36,7 +38,7 @@ export const VotePercentChart: FC = () => { return [] const sortedGauges = gauges.sort( - (a, b) => a.communityVotedPercentage.greaterThan(b.communityVotedPercentage) ? 1 : -1, + (a, b) => a.communityVotedPercentage.greaterThan(b.communityVotedPercentage) ? -1 : 1, ) const topGauges = sortedGauges.slice(0, 7) const otherGauges = sortedGauges.slice(7) @@ -64,8 +66,8 @@ export const VotePercentChart: FC = () => { .sort( (a, b) => (voteInputMap[a.id] || votedPercentMap[a.id]) .greaterThan((voteInputMap[b.id] || votedPercentMap[b.id])) - ? 1 - : -1, + ? -1 + : 1, ) const topGauges = sortedGauges.slice(0, 7) const otherGauges = sortedGauges.slice(7) diff --git a/apps/gauge/pages/index.tsx b/apps/gauge/pages/index.tsx index 68ff7fca9..b1eafa743 100644 --- a/apps/gauge/pages/index.tsx +++ b/apps/gauge/pages/index.tsx @@ -26,13 +26,13 @@ function Gauge() { -
+
-
+
diff --git a/apps/gauge/pages/lock.tsx b/apps/gauge/pages/lock.tsx index 1515e7c92..1db9853b6 100644 --- a/apps/gauge/pages/lock.tsx +++ b/apps/gauge/pages/lock.tsx @@ -26,40 +26,40 @@ function Lock() { return ( - - - -
- - - classNames( - selected ? TAB_SELECTED_CLASS : TAB_NOT_SELECTED_CLASS, - TAB_DEFAULT_CLASS, - )} - > - Lock - - - classNames( - selected ? TAB_SELECTED_CLASS : TAB_NOT_SELECTED_CLASS, - TAB_DEFAULT_CLASS, - )} - > - Redeem - - -
- - - - - - - - -
-
-
+ +
+ + + classNames( + selected ? TAB_SELECTED_CLASS : TAB_NOT_SELECTED_CLASS, + TAB_DEFAULT_CLASS, + )} + > + Lock + + + classNames( + selected ? TAB_SELECTED_CLASS : TAB_NOT_SELECTED_CLASS, + TAB_DEFAULT_CLASS, + )} + > + Redeem + + + + + + + + + + + + + + +
+
) @@ -111,7 +111,7 @@ function LockPanel() { }) return ( -
+
Update your Lock @@ -249,7 +249,7 @@ function RedeemPanel() { }) return ( -
+
Redeem Unlocked ZLK @@ -280,20 +280,20 @@ function RedeemPanel() {
- - - + + +
) diff --git a/apps/market/components/MarketSection/MarketActions/MarketAdd/MarketAddManualReviewModal.tsx b/apps/market/components/MarketSection/MarketActions/MarketAdd/MarketAddManualReviewModal.tsx index 46bff5716..51d04a4d5 100644 --- a/apps/market/components/MarketSection/MarketActions/MarketAdd/MarketAddManualReviewModal.tsx +++ b/apps/market/components/MarketSection/MarketActions/MarketAdd/MarketAddManualReviewModal.tsx @@ -78,7 +78,7 @@ export const MarketAddManualReviewModal: FC = ( onSuccess={createNotification} render={({ approved }) => { return ( - ) diff --git a/apps/market/components/MarketSection/MarketActions/MarketMintAndRedeem/MarketMintReviewModal.tsx b/apps/market/components/MarketSection/MarketActions/MarketMintAndRedeem/MarketMintReviewModal.tsx index 20198983b..b4d9a0888 100644 --- a/apps/market/components/MarketSection/MarketActions/MarketMintAndRedeem/MarketMintReviewModal.tsx +++ b/apps/market/components/MarketSection/MarketActions/MarketMintAndRedeem/MarketMintReviewModal.tsx @@ -63,7 +63,7 @@ export const MarketMintReviewModal: FC = ({ onSuccess={createNotification} render={({ approved }) => { return ( - ) diff --git a/apps/market/components/MarketSection/MarketActions/MarketMintAndRedeem/MarketRedeemReviewModal.tsx b/apps/market/components/MarketSection/MarketActions/MarketMintAndRedeem/MarketRedeemReviewModal.tsx index f50e47f80..f89cc2217 100644 --- a/apps/market/components/MarketSection/MarketActions/MarketMintAndRedeem/MarketRedeemReviewModal.tsx +++ b/apps/market/components/MarketSection/MarketActions/MarketMintAndRedeem/MarketRedeemReviewModal.tsx @@ -74,7 +74,7 @@ export const MarketRedeemReviewModal: FC = ({ onSuccess={createNotification} render={({ approved }) => { return ( - ) diff --git a/apps/market/components/MarketSection/MarketActions/MarketRemove/MarketRemoveManualReviewModal.tsx b/apps/market/components/MarketSection/MarketActions/MarketRemove/MarketRemoveManualReviewModal.tsx index eef6622ce..6e75da295 100644 --- a/apps/market/components/MarketSection/MarketActions/MarketRemove/MarketRemoveManualReviewModal.tsx +++ b/apps/market/components/MarketSection/MarketActions/MarketRemove/MarketRemoveManualReviewModal.tsx @@ -69,7 +69,7 @@ export const MarketRemoveManualReviewModal: FC = ( onSuccess={createNotification} render={({ approved }) => { return ( - ) diff --git a/apps/market/components/MarketSection/MarketActions/MarketSwap/CurrencyInput.tsx b/apps/market/components/MarketSection/MarketActions/MarketSwap/CurrencyInput.tsx index 77d7fc1e0..31f83c08a 100644 --- a/apps/market/components/MarketSection/MarketActions/MarketSwap/CurrencyInput.tsx +++ b/apps/market/components/MarketSection/MarketActions/MarketSwap/CurrencyInput.tsx @@ -32,6 +32,7 @@ export const CurrencyInput: FC<_CurrencyInputProps> = ({ chainId, disabled, includeNative = false, + includeHotTokens = false, loading = false, isInputType, }) => { @@ -60,6 +61,7 @@ export const CurrencyInput: FC<_CurrencyInputProps> = ({ disableMaxButton={disableMaxButton} disabled={disabled} displayValue={displayValue} + includeHotTokens={includeHotTokens} includeNative={includeNative} loading={loading} onAddToken={onAddToken} diff --git a/apps/market/components/MarketSection/MarketRewards/MarketLPRewards.tsx b/apps/market/components/MarketSection/MarketRewards/MarketLPRewards.tsx new file mode 100644 index 000000000..6181d40e6 --- /dev/null +++ b/apps/market/components/MarketSection/MarketRewards/MarketLPRewards.tsx @@ -0,0 +1,63 @@ +import { Trans } from '@lingui/macro' +import type { Amount, Token } from '@zenlink-interface/currency' +import { Currency, Typography } from '@zenlink-interface/ui' +import { type FC, useMemo } from 'react' + +interface MarketLPRewardsProps { + isLoading: boolean + isError: boolean + data: Amount[] | undefined +} + +export const MarketLPRewards: FC = ({ data, isLoading, isError }) => { + const rewards = useMemo(() => { + if (!data) + return [] + return data.filter(reward => reward.greaterThan(0)) + }, [data]) + + if (isLoading && !isError) { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ) + } + + if (!isLoading && !isError && rewards.length) { + return ( +
+
+ + + Market LP Rewards + + +
+ {rewards.map(reward => ( +
+
+ + + {reward?.toSignificant(6)} {reward.currency.symbol} + +
+
+ ))} +
+ ) + } + + return <> +} diff --git a/apps/market/components/MarketSection/MarketRewards/MarketRewards.tsx b/apps/market/components/MarketSection/MarketRewards/MarketRewards.tsx index b38baeb32..16a1dbc51 100644 --- a/apps/market/components/MarketSection/MarketRewards/MarketRewards.tsx +++ b/apps/market/components/MarketSection/MarketRewards/MarketRewards.tsx @@ -1,10 +1,11 @@ import { Trans } from '@lingui/macro' import type { Market } from '@zenlink-interface/market' import { Button, Dots, Typography } from '@zenlink-interface/ui' -import { useYtInterestAndRewards } from '@zenlink-interface/wagmi' +import { useMarketRewards, useYtInterestAndRewards } from '@zenlink-interface/wagmi' import type { FC } from 'react' import { YtInterestAndRewards } from './YtInterestAndRewards' import { MarketRewardsReviewModal } from './MarketRewardsReviewModal' +import { MarketLPRewards } from './MarketLPRewards' interface MarketRewardsProps { market: Market @@ -12,10 +13,16 @@ interface MarketRewardsProps { export const MarketRewards: FC = ({ market }) => { const { - data, + data: ytData, isLoading: isYtLoading, isError: isYtError, - } = useYtInterestAndRewards(market.chainId, [market], { enabled: true }) + } = useYtInterestAndRewards(market.chainId, [market]) + + const { + data: lpRewardsData, + isLoading: isLpRewardsLoading, + isError: isLpRewardsError, + } = useMarketRewards(market.chainId, [market]) return (
@@ -25,7 +32,8 @@ export const MarketRewards: FC = ({ market }) => { {({ isWritePending, setOpen }) => (
- + +
) } diff --git a/apps/market/components/MarketSection/MarketRewards/MarketRewardsReviewModal.tsx b/apps/market/components/MarketSection/MarketRewards/MarketRewardsReviewModal.tsx index 393e816df..eb974b399 100644 --- a/apps/market/components/MarketSection/MarketRewards/MarketRewardsReviewModal.tsx +++ b/apps/market/components/MarketSection/MarketRewards/MarketRewardsReviewModal.tsx @@ -3,25 +3,29 @@ import { Button, Dialog, Dots } from '@zenlink-interface/ui' import type { YtInterestAndRewardsResult } from '@zenlink-interface/wagmi' import { useRedeemRewardsReview } from '@zenlink-interface/wagmi' import { type FC, type ReactNode, useMemo, useState } from 'react' +import type { Market } from '@zenlink-interface/market' import { YtInterestAndRewards } from './YtInterestAndRewards' interface MarketRewardsReviewModalProps { chainId: number ytData: YtInterestAndRewardsResult | undefined + lpRewardsMarkets: Market[] | undefined children: ({ isWritePending, setOpen }: { isWritePending: boolean, setOpen: (open: boolean) => void }) => ReactNode } export const MarketRewardsReviewModal: FC = ({ chainId, ytData, + lpRewardsMarkets, children, }) => { const [open, setOpen] = useState(false) - const { isWritePending, sendTransaction, routerAddress } = useRedeemRewardsReview({ + const { isWritePending, sendTransaction } = useRedeemRewardsReview({ chainId, setOpen, yts: useMemo(() => ytData && [ytData.market.YT], [ytData]), + markets: useMemo(() => lpRewardsMarkets, [lpRewardsMarkets]), }) return ( diff --git a/apps/market/components/MarketsSection/Tables/MarketsTable/Cells/columns.tsx b/apps/market/components/MarketsSection/Tables/MarketsTable/Cells/columns.tsx index 1099c07b9..bf315a599 100644 --- a/apps/market/components/MarketsSection/Tables/MarketsTable/Cells/columns.tsx +++ b/apps/market/components/MarketsSection/Tables/MarketsTable/Cells/columns.tsx @@ -1,6 +1,7 @@ import { Trans } from '@lingui/macro' import type { ColumnDef } from '@tanstack/react-table' import type { Market } from '@zenlink-interface/market' +import { JSBI } from '@zenlink-interface/math' import { MarketTVLCell } from './MarketTVLCell' import { MarketNameCell } from './MarketNameCell' import { MarketMaturityCell } from './MarketMaturityCell' @@ -28,7 +29,7 @@ export const NAME_COLUMN: ColumnDef = { export const MATURITY_COLUMN: ColumnDef = { id: 'maturity', header: _ => Maturity, - accessorFn: row => Number(row.expiry.toString()), + accessorFn: row => JSBI.toNumber(row.expiry), cell: props => , size: 80, meta: { diff --git a/packages/market/src/Token/implementations/index.ts b/packages/market/src/Token/implementations/index.ts index 0a07866e7..062ca35e5 100644 --- a/packages/market/src/Token/implementations/index.ts +++ b/packages/market/src/Token/implementations/index.ts @@ -1 +1,2 @@ export * from './stDOT' +export * from './vDOT' diff --git a/packages/market/src/Token/implementations/vDOT.ts b/packages/market/src/Token/implementations/vDOT.ts new file mode 100644 index 000000000..13b9a5d22 --- /dev/null +++ b/packages/market/src/Token/implementations/vDOT.ts @@ -0,0 +1,39 @@ +import type { Currency, Token } from '@zenlink-interface/currency' +import { Amount } from '@zenlink-interface/currency' +import type { JSBI } from '@zenlink-interface/math' +import { SYBase } from '../SYBase' + +export class VDOT extends SYBase { + public readonly xcDOT: Token + public readonly vDOT: Token + + public constructor( + token: { + chainId: number | string + address: string + decimals: number + symbol?: string + name?: string + }, + xcDOT: Token, + vDOT: Token, + tokensIn: Currency[], + tokensOut: Currency[], + ) { + super(token, vDOT, [], tokensIn, tokensOut) + this.xcDOT = xcDOT + this.vDOT = vDOT + } + + public updateExchangeRate(exchageRate: JSBI) { + super.updateExchangeRate(exchageRate) + } + + protected _previewDeposit(_tokenIn: Currency, amountTokenToDeposit: Amount): Amount { + return Amount.fromRawAmount(this, amountTokenToDeposit.quotient) + } + + protected _previewRedeem(_tokenOut: Currency, amountSharesToRedeem: Amount): Amount { + return Amount.fromRawAmount(this.vDOT, amountSharesToRedeem.quotient) + } +} diff --git a/packages/wagmi/hooks/markets/config.ts b/packages/wagmi/hooks/markets/config.ts index 6c2e994fd..23c8f3097 100644 --- a/packages/wagmi/hooks/markets/config.ts +++ b/packages/wagmi/hooks/markets/config.ts @@ -2,6 +2,7 @@ import { ParachainId } from '@zenlink-interface/chain' import { Market, type PT, type SYBase, type YT } from '@zenlink-interface/market' import type { Address } from 'viem' import { PT_stDOT_MAY2025, SY_stDOT_MAY2025, YT_stDOT_MAY2025 } from './markets-config/stDOT-MAY2025' +import { PT_vDOT_NOV2024, SY_vDOT_NOV2024, YT_vDOT_NOV2024 } from './markets-config/vDOT-NOV2024' export const MarketEntities: Record> = { [ParachainId.MOONBEAM]: { @@ -16,6 +17,17 @@ export const MarketEntities: Record> = { }, PT_stDOT_MAY2025, ), + // vDOT-NOV2024 + '0x38Ad9b14ae4502adE99799dA4695Ff177265b14a': new Market( + { + chainId: ParachainId.MOONBEAM, + address: '0x38Ad9b14ae4502adE99799dA4695Ff177265b14a', + decimals: 18, + symbol: 'LP vDOT', + name: 'Zenlink Market LP vDOT', + }, + PT_vDOT_NOV2024, + ), }, } @@ -27,5 +39,11 @@ export const YieldTokensEntities: Record[][] | undefined +} + +export function useMarketRewards( + chainId: number | undefined, + markets: Market[], + config: { enabled?: boolean } = { enabled: true }, +): UseMarketRewardsReturn { + const { address: account } = useAccount() + const blockNumber = useBlockNumber(chainId) + + const { data: rewardTokens } = useMarketRewardTokens(chainId, markets) + + const rewardsCalls = useMemo( + () => rewardTokens?.length + ? markets.map( + (market, i) => + (rewardTokens[i] || []).map(token => ({ + chainId: chainsParachainIdToChainId[chainId ?? -1], + address: market.address as Address, + abi: marketABI, + functionName: 'userReward', + args: [token.address, account], + }) as const), + ).flat() + : [], + [account, chainId, markets, rewardTokens], + ) + + const { + data: rewardsData, + isLoading, + isError, + refetch, + } = useReadContracts({ contracts: rewardsCalls }) + + useEffect(() => { + if (config?.enabled && blockNumber && account) + refetch() + }, [account, blockNumber, config?.enabled, refetch]) + + return useMemo(() => { + if (!rewardsData || !rewardTokens) + return { isLoading, isError, data: [] } + + let rewardDataIndex = 0 + return { + isLoading, + isError, + data: markets.map((_, i) => { + const rewardData = rewardsData[rewardDataIndex]?.result + const tokens = rewardTokens[i] + + if (!rewardData || !tokens.length) + return [] + + return tokens.map((token) => { + rewardDataIndex++ + return Amount.fromRawAmount(token, JSBI.BigInt(rewardData[1].toString())) + }) + }), + } + }, [isError, isLoading, markets, rewardTokens, rewardsData]) +} + +interface UseMarketRewardTokensReturn { + isLoading: boolean + isError: boolean + data: Token[][] | undefined +} + +export function useMarketRewardTokens( + chainId: number | undefined, + markets: Market[], +): UseMarketRewardTokensReturn { + const blockNumber = useBlockNumber(chainId) + + const rewardTokensCalls = useMemo( + () => markets.map(market => ({ + chainId: chainsParachainIdToChainId[chainId ?? -1], + address: market.address as Address, + abi: marketABI, + functionName: 'getRewardTokens', + }) as const), + [chainId, markets], + ) + + const { data, isLoading, isError } = useReadContracts({ contracts: rewardTokensCalls }) + + const tokensFetchArgs = useMemo(() => { + if (!data || !chainId) + return { tokens: [] } + + return { + tokens: Array.from( + new Set( + data.map(d => (d.result || []).map(address => ({ chainId, address }))).flat(), + ), + ), + } + }, [chainId, data]) + + const tokensResult = useTokens(tokensFetchArgs) + + return useMemo(() => { + if (!data || !chainId) + return { isLoading, isError, data: undefined } + + const tokenMap: Record = tokensResult.data?.reduce((prev, token) => { + return { + ...prev, + [token.address]: new Token({ chainId, ...token }), + } + }, {}) || {} + + return { + isLoading, + isError, + data: data.map((d) => { + if (!d.result) + return [] + return d.result.map(address => tokenMap[address]).filter(t => !!t) + }), + } + }, [chainId, data, isError, isLoading, tokensResult.data]) +} diff --git a/packages/wagmi/hooks/markets/useYtInterestAndRewards.ts b/packages/wagmi/hooks/markets/useYtInterestAndRewards.ts index 536e85798..5ea2f8a7e 100644 --- a/packages/wagmi/hooks/markets/useYtInterestAndRewards.ts +++ b/packages/wagmi/hooks/markets/useYtInterestAndRewards.ts @@ -24,7 +24,7 @@ interface UseYtInterestAndRewardsReturn { export function useYtInterestAndRewards( chainId: number | undefined, markets: Market[], - config?: { enabled?: boolean }, + config: { enabled?: boolean } = { enabled: true }, ): UseYtInterestAndRewardsReturn { const { address: account } = useAccount() const blockNumber = useBlockNumber(chainId)