From 5181956f395ecf65585be142a83057465a7e15a8 Mon Sep 17 00:00:00 2001 From: Anton Cheng Date: Wed, 8 Jan 2025 12:45:25 +0800 Subject: [PATCH 01/17] chore: single page reward --- app/rewards/components/MarketProgram.tsx | 90 ++++++++++++------ app/rewards/components/RewardContent.tsx | 111 ++++++++++++++-------- app/rewards/components/UniformProgram.tsx | 104 ++++++++++++-------- src/imgs/tokens/Ionic.png | Bin 0 -> 341 bytes src/imgs/tokens/well.png | Bin 0 -> 715 bytes src/utils/tokens.ts | 28 ++++++ 6 files changed, 221 insertions(+), 112 deletions(-) create mode 100644 src/imgs/tokens/Ionic.png create mode 100644 src/imgs/tokens/well.png diff --git a/app/rewards/components/MarketProgram.tsx b/app/rewards/components/MarketProgram.tsx index 09aa714e..3385cb0c 100644 --- a/app/rewards/components/MarketProgram.tsx +++ b/app/rewards/components/MarketProgram.tsx @@ -2,19 +2,21 @@ import { useMemo, useState } from 'react'; import { Table, TableHeader, TableBody, TableColumn, TableRow, TableCell } from '@nextui-org/table'; +import { Switch } from '@nextui-org/react'; import Image from 'next/image'; import Link from 'next/link'; import { toast } from 'react-toastify'; import { Address } from 'viem'; import { useAccount, useSwitchChain } from 'wagmi'; import { Button } from '@/components/common/Button'; +import { TokenIcon } from '@/components/TokenIcon'; import { DistributionResponseType } from '@/hooks/useRewards'; import { useTransactionWithToast } from '@/hooks/useTransactionWithToast'; import { formatReadable, formatBalance } from '@/utils/balance'; import { getNetworkImg } from '@/utils/networks'; import { findToken } from '@/utils/tokens'; -import { Market } from '@/utils/types'; -import { MarketProgramType } from '@/utils/types'; +import { getAssetURL } from '@/utils/external'; +import { Market, MarketProgramType } from '@/utils/types'; type MarketProgramProps = { account: string; @@ -32,6 +34,7 @@ export default function MarketProgram({ const { chainId } = useAccount(); const { switchChain } = useSwitchChain(); const [selectedToken, setSelectedToken] = useState(null); + const [showPending, setShowPending] = useState(false); const { sendTransaction } = useTransactionWithToast({ toastId: 'claim', @@ -85,20 +88,26 @@ export default function MarketProgram({ [marketRewards], ); + const filteredRewardTokens = useMemo( + () => allRewardTokens.filter((tokenReward) => showPending || tokenReward.claimable > BigInt(0)), + [allRewardTokens, showPending] + ); + const handleRowClick = (token: string) => { setSelectedToken((prevToken) => (prevToken === token ? null : token)); }; return (
-
Market Program Rewards
-

- Market Program Rewards are incentives tailored to specific markets on Morpho. These rewards - encourage particular actions within each market, such as supplying, borrowing, or providing - collateral. The program may include additional incentives designed to stimulate activity in - targeted markets. -

- +
+ Show Pending + +
Action - {allRewardTokens + {filteredRewardTokens .filter((tokenReward) => tokenReward !== null && tokenReward !== undefined) .map((tokenReward, index) => { const matchedToken = findToken(tokenReward.token, tokenReward.chainId) ?? { @@ -139,12 +148,21 @@ export default function MarketProgram({ onClick={() => handleRowClick(tokenReward.token)} > -
+ e.stopPropagation()} + className="flex items-center justify-center gap-2 hover:opacity-80" + >

{matchedToken.symbol}

- {matchedToken.img && ( - - )} -
+ +
@@ -163,9 +181,12 @@ export default function MarketProgram({ formatBalance(tokenReward.claimable, matchedToken.decimals), )}

- {matchedToken.img && ( - - )} +
@@ -175,19 +196,27 @@ export default function MarketProgram({ formatBalance(tokenReward.pending, matchedToken.decimals), )}

- {matchedToken.img && ( - - )} +

- {formatReadable(formatBalance(tokenReward.total, matchedToken.decimals))} + {formatReadable( + formatBalance(tokenReward.total, matchedToken.decimals), + )}

- {matchedToken.img && ( - - )} +
@@ -197,9 +226,12 @@ export default function MarketProgram({ formatBalance(tokenReward.claimed, matchedToken.decimals), )}

- {matchedToken.img && ( - - )} +
diff --git a/app/rewards/components/RewardContent.tsx b/app/rewards/components/RewardContent.tsx index 80639e95..90168831 100644 --- a/app/rewards/components/RewardContent.tsx +++ b/app/rewards/components/RewardContent.tsx @@ -1,53 +1,45 @@ 'use client'; -import { useMemo, useState } from 'react'; +import { useMemo } from 'react'; import { useParams } from 'next/navigation'; -import ButtonGroup from '@/components/ButtonGroup'; +import { BsQuestionCircle } from "react-icons/bs"; + +import { Tooltip } from '@nextui-org/react'; import Header from '@/components/layout/header/Header'; import EmptyScreen from '@/components/Status/EmptyScreen'; import LoadingScreen from '@/components/Status/LoadingScreen'; import { useMarkets } from '@/hooks/useMarkets'; import useUserRewards from '@/hooks/useRewards'; import { filterMarketRewards, filterUniformRewards } from '@/utils/rewardHelpers'; +import { TooltipContent } from '@/components/TooltipContent'; import MarketProgram from './MarketProgram'; -import UniformProgram from './UniformProgram'; // You'll need to create this component +import UniformProgram from './UniformProgram'; -const programOptions = [ - { key: 'market', label: 'Market Program', value: 'market' }, - { key: 'uniform', label: 'Uniform Program', value: 'uniform' }, -]; +const PROGRAM_INFO = { + market: { + title: 'Market Program', + tooltip: { + title: 'Market Program Rewards', + detail: 'Market Program Rewards are incentives tailored to specific markets on Morpho. These rewards encourage particular actions within each market, such as supplying, borrowing, or providing collateral. The program may include additional incentives designed to stimulate activity in targeted markets.' + } + }, + uniform: { + title: 'Uniform Program', + tooltip: { + title: 'Uniform Program Rewards', + detail: 'The Uniform Program is a new reward system that applies to all users who supply to Morpho, regardless of the specific market. It provides a consistent reward rate for each dollar supplied across eligible markets, promoting broader participation in the Morpho ecosystem.' + } + } +}; export default function Rewards() { const { account } = useParams<{ account: string }>(); - const [activeProgram, setActiveProgram] = useState<'market' | 'uniform'>('market'); - const { loading, markets } = useMarkets(); const { rewards, distributions, loading: loadingRewards } = useUserRewards(account); const marketRewards = useMemo(() => filterMarketRewards(rewards), [rewards]); const uniformRewards = useMemo(() => filterUniformRewards(rewards), [rewards]); - const renderActiveProgram = () => { - if (activeProgram === 'market') { - return ( - - ); - } else { - return ( - - ); - } - }; - return (
@@ -55,17 +47,7 @@ export default function Rewards() {

Reward

- Morpho offers multiple reward programs to incentivize user participation. Choose a - program type below to see more details. -
- -
- setActiveProgram(value as 'market' | 'uniform')} - size="md" - /> + Morpho offers multiple reward programs to incentivize user participation.
@@ -74,7 +56,52 @@ export default function Rewards() { ) : rewards.length === 0 ? ( ) : ( - renderActiveProgram() +
+
+
+

{PROGRAM_INFO.market.title}

+ + } + > + + +
+ +
+ +
+
+

{PROGRAM_INFO.uniform.title}

+ + } + > + + +
+ +
+
)}
diff --git a/app/rewards/components/UniformProgram.tsx b/app/rewards/components/UniformProgram.tsx index 3797bf76..00d4d3bc 100644 --- a/app/rewards/components/UniformProgram.tsx +++ b/app/rewards/components/UniformProgram.tsx @@ -1,17 +1,21 @@ 'use client'; -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; import { Table, TableHeader, TableBody, TableColumn, TableRow, TableCell } from '@nextui-org/table'; +import { Switch } from '@nextui-org/react'; import Image from 'next/image'; +import Link from 'next/link'; import { toast } from 'react-toastify'; import { Address } from 'viem'; import { useAccount, useSwitchChain } from 'wagmi'; import { Button } from '@/components/common/Button'; +import { TokenIcon } from '@/components/TokenIcon'; import { DistributionResponseType } from '@/hooks/useRewards'; import { useTransactionWithToast } from '@/hooks/useTransactionWithToast'; import { formatReadable, formatBalance } from '@/utils/balance'; import { getNetworkImg } from '@/utils/networks'; import { findToken } from '@/utils/tokens'; +import { getAssetURL } from '@/utils/external'; import { UniformRewardType } from '@/utils/types'; type UniformProgramProps = { @@ -27,6 +31,7 @@ export default function UniformProgram({ }: UniformProgramProps) { const { chainId } = useAccount(); const { switchChain } = useSwitchChain(); + const [showPending, setShowPending] = useState(false); const { sendTransaction } = useTransactionWithToast({ toastId: 'claim-uniform', @@ -58,25 +63,22 @@ export default function UniformProgram({ [uniformRewards, distributions], ); + const filteredRewardsData = useMemo( + () => rewardsData.filter((reward) => showPending || reward.claimable > BigInt(0)), + [rewardsData, showPending] + ); + return (
-
Uniform Program Rewards
-

- The Uniform Program is a new reward system that applies to all users who supply to Morpho, - regardless of the specific market. It provides a consistent reward rate for each dollar - supplied across eligible markets, promoting broader participation in the Morpho ecosystem. - For more details, check the{' '} - - forum post here - - . -

- +
+ Show Pending + +
Action - {rewardsData.map((reward, index) => ( + {filteredRewardsData.map((reward, index) => ( -
-

{reward.token?.symbol}

- {reward.token?.img && ( - - )} -
+ +

{reward.token?.symbol ?? 'Unknown'}

+ +
@@ -117,45 +127,57 @@ export default function UniformProgram({
-
+

{formatReadable( formatBalance(reward.claimable, reward.token?.decimals ?? 18), )}

- {reward.token?.img && ( - - )} +
-
+

{formatReadable(formatBalance(reward.pending, reward.token?.decimals ?? 18))}

- {reward.token?.img && ( - - )} +
-
+

{formatReadable(formatBalance(reward.claimed, reward.token?.decimals ?? 18))}

- {reward.token?.img && ( - - )} +
-
+

{formatReadable(formatBalance(reward.total, reward.token?.decimals ?? 18))}

- {reward.token?.img && ( - - )} +
diff --git a/src/imgs/tokens/Ionic.png b/src/imgs/tokens/Ionic.png new file mode 100644 index 0000000000000000000000000000000000000000..7c506135977c065b22a605801e77eeaae7574233 GIT binary patch literal 341 zcmV-b0jmCqP)qS^QY;Zm zC=pL8FxhhrJ0B^!V+9)ySO=Zo0002UNklN& zT8^#Je+|fXIxHGUP n#|-4rGV(NrJ^%kYkN?&WvOx*qTMvF*00000NkvXXu0mjfgNA|c literal 0 HcmV?d00001 diff --git a/src/imgs/tokens/well.png b/src/imgs/tokens/well.png new file mode 100644 index 0000000000000000000000000000000000000000..1d89ff8cc2fab3ed87336f1a41f477da862dd0be GIT binary patch literal 715 zcmV;+0yO=JP)u+ey@{P@<5eO zgcERAMySq}JvWTG%e6zb!>_2J4ildS@dBI?%*k@YWE;u!b1(>w$acwc=CjVxkbX>J zeBc}lVl1do5i8Lktn&xf^a@rM(*F|nL=-ZNfC!x(2Bkhb8bT+sxUNi&|$z)%5@MVwY;0S6xR)&ZeBDx$;@9MA#?pO7!p6 z@pIUl>R&l$;h!_B&!4;kggA!uoNcENC)68fPro?<0BiRJaJ!~ydV?ggBD&waKl?F+ z)*|t|*b5^s@wSN>CNOECJI!%&^kUNkE52B;TgFE#QOLZY(g*mC$N_-u511F2_N%B! xa{zFGBgDH9VEQ7)1@-Q$@&+(b2Hl!)e*snAos5x<4c7nw002ovPDHLkV1oF@SOow8 literal 0 HcmV?d00001 diff --git a/src/utils/tokens.ts b/src/utils/tokens.ts index 465eaaab..511f78ef 100644 --- a/src/utils/tokens.ts +++ b/src/utils/tokens.ts @@ -420,6 +420,34 @@ const supportedTokens = [ decimals: 18, networks: [{ chain: base, address: '0xb0505e5a99abd03d94a1169e638B78EDfEd26ea4' }], }, + // rewards + { + symbol: 'WELL', + img: require('../imgs/tokens/well.png') as string, + decimals: 18, + networks: [{ chain: base, address: '0xA88594D404727625A9437C3f886C7643872296AE' }], + }, + { + symbol: 'ION', + img: require('../imgs/tokens/ionic.png') as string, + decimals: 18, + networks: [{ chain: base, address: '0x3eE5e23eEE121094f1cFc0Ccc79d6C809Ebd22e5' }], + }, + { + symbol: 'MORPHO', + img: require('../imgs/tokens/morpho.svg') as string, + decimals: 18, + networks: [ + { + address: '0x58D97B57BB95320F9a05dC918Aef65434969c2B2', + chain: mainnet, + }, + { + address: '0xBAa5CC21fd487B8Fcc2F632f3F4E8D37262a0842', + chain: base, + }, + ], + } ]; const isWhitelisted = (address: string, chainId: number) => { From 8d873ddf97d465e7f26665d3c45ddc0fb6a2184f Mon Sep 17 00:00:00 2001 From: Anton Cheng Date: Wed, 8 Jan 2025 12:58:41 +0800 Subject: [PATCH 02/17] feat: include borrow rewards --- app/rewards/components/MarketProgram.tsx | 257 ++++++++++++++-------- app/rewards/components/RewardContent.tsx | 36 +-- app/rewards/components/UniformProgram.tsx | 10 +- src/utils/tokens.ts | 2 +- 4 files changed, 194 insertions(+), 111 deletions(-) diff --git a/app/rewards/components/MarketProgram.tsx b/app/rewards/components/MarketProgram.tsx index 3385cb0c..f39b0c6e 100644 --- a/app/rewards/components/MarketProgram.tsx +++ b/app/rewards/components/MarketProgram.tsx @@ -1,8 +1,8 @@ 'use client'; import { useMemo, useState } from 'react'; -import { Table, TableHeader, TableBody, TableColumn, TableRow, TableCell } from '@nextui-org/table'; import { Switch } from '@nextui-org/react'; +import { Table, TableHeader, TableBody, TableColumn, TableRow, TableCell } from '@nextui-org/table'; import Image from 'next/image'; import Link from 'next/link'; import { toast } from 'react-toastify'; @@ -13,9 +13,9 @@ import { TokenIcon } from '@/components/TokenIcon'; import { DistributionResponseType } from '@/hooks/useRewards'; import { useTransactionWithToast } from '@/hooks/useTransactionWithToast'; import { formatReadable, formatBalance } from '@/utils/balance'; +import { getAssetURL } from '@/utils/external'; import { getNetworkImg } from '@/utils/networks'; import { findToken } from '@/utils/tokens'; -import { getAssetURL } from '@/utils/external'; import { Market, MarketProgramType } from '@/utils/types'; type MarketProgramProps = { @@ -38,59 +38,89 @@ export default function MarketProgram({ const { sendTransaction } = useTransactionWithToast({ toastId: 'claim', - pendingText: 'Claiming Reward...', - successText: 'Reward Claimed!', - errorText: 'Failed to claim rewards', + pendingText: 'Claiming Market Reward...', + successText: 'Market Reward Claimed!', + errorText: 'Failed to claim market rewards', chainId, - pendingDescription: `Claiming rewards`, - successDescription: `Successfully claimed rewards`, + pendingDescription: `Claiming market rewards`, + successDescription: `Successfully claimed market rewards`, }); const allRewardTokens = useMemo( - () => - marketRewards.reduce( - ( - entries: { - token: string; - claimed: bigint; - claimable: bigint; - pending: bigint; - total: bigint; - chainId: number; - }[], - reward: MarketProgramType, - ) => { - if (reward.program === undefined) return entries; + () => { + // First, group rewards by asset address and chain ID + const groupedRewards = marketRewards.reduce((acc, reward) => { + const key = `${reward.asset.address.toLowerCase()}-${reward.asset.chain_id}`; + if (!acc[key]) { + acc[key] = { + rewards: [], + token: reward.asset.address, + chainId: reward.asset.chain_id, + distribution: distributions.find( + (d) => d.asset.address.toLowerCase() === reward.asset.address.toLowerCase(), + ), + }; + } + acc[key].rewards.push(reward); + return acc; + }, {} as Record); + + // Then, calculate totals for each group + return Object.values(groupedRewards).map((group) => { + const claimable = group.rewards.reduce( + (sum, reward) => + sum + + BigInt(reward.for_supply?.claimable_now ?? '0') + + BigInt(reward.for_borrow?.claimable_now ?? '0') + + BigInt(reward.for_collateral?.claimable_now ?? '0'), + BigInt(0), + ); + + const pending = group.rewards.reduce( + (sum, reward) => + sum + + BigInt(reward.for_supply?.claimable_next ?? '0') + + BigInt(reward.for_borrow?.claimable_next ?? '0') + + BigInt(reward.for_collateral?.claimable_next ?? '0'), + BigInt(0), + ); - const idx = entries.findIndex((e) => e.token === reward.program.asset.address); - if (idx === -1) { - return [ - ...entries, - { - token: reward.program.asset.address, - claimed: BigInt(reward.for_supply?.claimed ?? '0'), - claimable: BigInt(reward.for_supply?.claimable_now ?? '0'), - pending: BigInt(reward.for_supply?.claimable_next ?? '0'), - total: BigInt(reward.for_supply?.total ?? '0'), - chainId: reward.program.asset.chain_id, - }, - ]; - } else { - entries[idx].claimed += BigInt(reward.for_supply?.claimed ?? '0'); - entries[idx].claimable += BigInt(reward.for_supply?.claimable_now ?? '0'); - entries[idx].pending += BigInt(reward.for_supply?.claimable_next ?? '0'); - entries[idx].total += BigInt(reward.for_supply?.total ?? '0'); - return entries; - } - }, - [], - ), - [marketRewards], + const total = group.rewards.reduce( + (sum, reward) => + sum + + BigInt(reward.for_supply?.total ?? '0') + + BigInt(reward.for_borrow?.total ?? '0') + + BigInt(reward.for_collateral?.total ?? '0'), + BigInt(0), + ); + + const claimed = group.rewards.reduce( + (sum, reward) => + sum + + BigInt(reward.for_supply?.claimed ?? '0') + + BigInt(reward.for_borrow?.claimed ?? '0') + + BigInt(reward.for_collateral?.claimed ?? '0'), + BigInt(0), + ); + + return { + token: group.token, + chainId: group.chainId, + distribution: group.distribution, + claimable, + pending, + total, + claimed, + rewards: group.rewards, // Keep original rewards for detail view + }; + }); + }, + [marketRewards, distributions], ); const filteredRewardTokens = useMemo( () => allRewardTokens.filter((tokenReward) => showPending || tokenReward.claimable > BigInt(0)), - [allRewardTokens, showPending] + [allRewardTokens, showPending], ); const handleRowClick = (token: string) => { @@ -99,7 +129,7 @@ export default function MarketProgram({ return (
-
+
Show Pending d.asset.address.toLowerCase() === tokenReward.token.toLowerCase(), - ); return ( e.stopPropagation()} className="flex items-center justify-center gap-2 hover:opacity-80" + onClick={(e) => e.stopPropagation()} >

{matchedToken.symbol}

-
+

- {formatReadable( - formatBalance(tokenReward.claimable, matchedToken.decimals), - )} + {formatReadable(formatBalance(tokenReward.claimable, matchedToken.decimals))}

-
+

- {formatReadable( - formatBalance(tokenReward.pending, matchedToken.decimals), - )} + {formatReadable(formatBalance(tokenReward.pending, matchedToken.decimals))}

-
+

- {formatReadable( - formatBalance(tokenReward.total, matchedToken.decimals), - )} + {formatReadable(formatBalance(tokenReward.claimed, matchedToken.decimals))}

-
-

- {formatReadable( - formatBalance(tokenReward.claimed, matchedToken.decimals), - )} -

+
+

{formatReadable(formatBalance(tokenReward.total, matchedToken.decimals))}

{ e.stopPropagation(); @@ -248,20 +265,20 @@ export default function MarketProgram({ toast.error('Connect wallet'); return; } - if (!distribution) { + if (!tokenReward.distribution) { toast.error('No claim data'); return; } - if (chainId !== distribution.distributor.chain_id) { + if (chainId !== tokenReward.distribution.distributor.chain_id) { switchChain({ chainId: tokenReward.chainId }); toast('Click on claim again after switching network'); return; } sendTransaction({ account: account as Address, - to: distribution.distributor.address as Address, - data: distribution.tx_data as `0x${string}`, - chainId: distribution.distributor.chain_id, + to: tokenReward.distribution.distributor.address as Address, + data: tokenReward.distribution.tx_data as `0x${string}`, + chainId: tokenReward.distribution.distributor.chain_id, }); }} > @@ -294,10 +311,18 @@ export default function MarketProgram({ Loan Asset Collateral LLTV - Claimable - Pending - Claimed - Total + Supply Claimable + Supply Pending + Supply Claimed + Supply Total + Borrow Claimable + Borrow Pending + Borrow Claimed + Borrow Total + Collateral Claimable + Collateral Pending + Collateral Claimed + Collateral Total {markets @@ -318,21 +343,45 @@ export default function MarketProgram({ ); }); - const claimable = tokenRewardsForMarket.reduce((a: bigint, b) => { + const supplyClaimable = tokenRewardsForMarket.reduce((a: bigint, b) => { return a + BigInt(b.for_supply?.claimable_now ?? '0'); }, BigInt(0)); - const pending = tokenRewardsForMarket.reduce((a: bigint, b) => { + const supplyPending = tokenRewardsForMarket.reduce((a: bigint, b) => { return a + BigInt(b.for_supply?.claimable_next ?? '0'); }, BigInt(0)); - - const total = tokenRewardsForMarket.reduce((a: bigint, b) => { + const supplyTotal = tokenRewardsForMarket.reduce((a: bigint, b) => { return a + BigInt(b.for_supply?.total ?? '0'); }, BigInt(0)); - - const claimed = tokenRewardsForMarket.reduce((a: bigint, b) => { + const supplyClaimed = tokenRewardsForMarket.reduce((a: bigint, b) => { return a + BigInt(b.for_supply?.claimed ?? '0'); }, BigInt(0)); + const borrowClaimable = tokenRewardsForMarket.reduce((a: bigint, b) => { + return a + BigInt(b.for_borrow?.claimable_now ?? '0'); + }, BigInt(0)); + const borrowPending = tokenRewardsForMarket.reduce((a: bigint, b) => { + return a + BigInt(b.for_borrow?.claimable_next ?? '0'); + }, BigInt(0)); + const borrowTotal = tokenRewardsForMarket.reduce((a: bigint, b) => { + return a + BigInt(b.for_borrow?.total ?? '0'); + }, BigInt(0)); + const borrowClaimed = tokenRewardsForMarket.reduce((a: bigint, b) => { + return a + BigInt(b.for_borrow?.claimed ?? '0'); + }, BigInt(0)); + + const collateralClaimable = tokenRewardsForMarket.reduce((a: bigint, b) => { + return a + BigInt(b.for_collateral?.claimable_now ?? '0'); + }, BigInt(0)); + const collateralPending = tokenRewardsForMarket.reduce((a: bigint, b) => { + return a + BigInt(b.for_collateral?.claimable_next ?? '0'); + }, BigInt(0)); + const collateralTotal = tokenRewardsForMarket.reduce((a: bigint, b) => { + return a + BigInt(b.for_collateral?.total ?? '0'); + }, BigInt(0)); + const collateralClaimed = tokenRewardsForMarket.reduce((a: bigint, b) => { + return a + BigInt(b.for_collateral?.claimed ?? '0'); + }, BigInt(0)); + const matchedToken = findToken(selectedToken, market.morphoBlue.chain.id); return ( @@ -346,16 +395,46 @@ export default function MarketProgram({ {market.collateralAsset.symbol} {formatBalance(market.lltv, 16)}% - {formatReadable(formatBalance(claimable, matchedToken?.decimals ?? 18))} + {formatReadable(formatBalance(supplyClaimable, matchedToken?.decimals ?? 18))} + + + {formatReadable(formatBalance(supplyPending, matchedToken?.decimals ?? 18))} + + + {formatReadable(formatBalance(supplyClaimed, matchedToken?.decimals ?? 18))} + + + {formatReadable(formatBalance(supplyTotal, matchedToken?.decimals ?? 18))} + + + {formatReadable(formatBalance(borrowClaimable, matchedToken?.decimals ?? 18))} + + + {formatReadable(formatBalance(borrowPending, matchedToken?.decimals ?? 18))} + + + {formatReadable(formatBalance(borrowClaimed, matchedToken?.decimals ?? 18))} + + + {formatReadable(formatBalance(borrowTotal, matchedToken?.decimals ?? 18))} + + + {formatReadable( + formatBalance(collateralClaimable, matchedToken?.decimals ?? 18), + )} - {formatReadable(formatBalance(pending, matchedToken?.decimals ?? 18))} + {formatReadable( + formatBalance(collateralPending, matchedToken?.decimals ?? 18), + )} - {formatReadable(formatBalance(claimed, matchedToken?.decimals ?? 18))} + {formatReadable( + formatBalance(collateralClaimed, matchedToken?.decimals ?? 18), + )} - {formatReadable(formatBalance(total, matchedToken?.decimals ?? 18))} + {formatReadable(formatBalance(collateralTotal, matchedToken?.decimals ?? 18))} ); diff --git a/app/rewards/components/RewardContent.tsx b/app/rewards/components/RewardContent.tsx index 90168831..030428ad 100644 --- a/app/rewards/components/RewardContent.tsx +++ b/app/rewards/components/RewardContent.tsx @@ -1,17 +1,17 @@ 'use client'; import { useMemo } from 'react'; +import { Tooltip } from '@nextui-org/react'; import { useParams } from 'next/navigation'; -import { BsQuestionCircle } from "react-icons/bs"; +import { BsQuestionCircle } from 'react-icons/bs'; -import { Tooltip } from '@nextui-org/react'; import Header from '@/components/layout/header/Header'; import EmptyScreen from '@/components/Status/EmptyScreen'; import LoadingScreen from '@/components/Status/LoadingScreen'; +import { TooltipContent } from '@/components/TooltipContent'; import { useMarkets } from '@/hooks/useMarkets'; import useUserRewards from '@/hooks/useRewards'; import { filterMarketRewards, filterUniformRewards } from '@/utils/rewardHelpers'; -import { TooltipContent } from '@/components/TooltipContent'; import MarketProgram from './MarketProgram'; import UniformProgram from './UniformProgram'; @@ -20,16 +20,18 @@ const PROGRAM_INFO = { title: 'Market Program', tooltip: { title: 'Market Program Rewards', - detail: 'Market Program Rewards are incentives tailored to specific markets on Morpho. These rewards encourage particular actions within each market, such as supplying, borrowing, or providing collateral. The program may include additional incentives designed to stimulate activity in targeted markets.' - } + detail: + 'Market Program Rewards are incentives tailored to specific markets on Morpho. These rewards encourage particular actions within each market, such as supplying, borrowing, or providing collateral. The program may include additional incentives designed to stimulate activity in targeted markets.', + }, }, uniform: { title: 'Uniform Program', tooltip: { title: 'Uniform Program Rewards', - detail: 'The Uniform Program is a new reward system that applies to all users who supply to Morpho, regardless of the specific market. It provides a consistent reward rate for each dollar supplied across eligible markets, promoting broader participation in the Morpho ecosystem.' - } - } + detail: + 'The Uniform Program is a new reward system that applies to all users who supply to Morpho, regardless of the specific market. It provides a consistent reward rate for each dollar supplied across eligible markets, promoting broader participation in the Morpho ecosystem.', + }, + }, }; export default function Rewards() { @@ -58,16 +60,17 @@ export default function Rewards() { ) : (
-
-

{PROGRAM_INFO.market.title}

- +

{PROGRAM_INFO.market.title}

+ } + placement="right" > @@ -81,16 +84,17 @@ export default function Rewards() {
-
-

{PROGRAM_INFO.uniform.title}

- +

{PROGRAM_INFO.uniform.title}

+ } + placement="right" > diff --git a/app/rewards/components/UniformProgram.tsx b/app/rewards/components/UniformProgram.tsx index 00d4d3bc..f444b9ef 100644 --- a/app/rewards/components/UniformProgram.tsx +++ b/app/rewards/components/UniformProgram.tsx @@ -1,8 +1,8 @@ 'use client'; import { useMemo, useState } from 'react'; -import { Table, TableHeader, TableBody, TableColumn, TableRow, TableCell } from '@nextui-org/table'; import { Switch } from '@nextui-org/react'; +import { Table, TableHeader, TableBody, TableColumn, TableRow, TableCell } from '@nextui-org/table'; import Image from 'next/image'; import Link from 'next/link'; import { toast } from 'react-toastify'; @@ -13,9 +13,9 @@ import { TokenIcon } from '@/components/TokenIcon'; import { DistributionResponseType } from '@/hooks/useRewards'; import { useTransactionWithToast } from '@/hooks/useTransactionWithToast'; import { formatReadable, formatBalance } from '@/utils/balance'; +import { getAssetURL } from '@/utils/external'; import { getNetworkImg } from '@/utils/networks'; import { findToken } from '@/utils/tokens'; -import { getAssetURL } from '@/utils/external'; import { UniformRewardType } from '@/utils/types'; type UniformProgramProps = { @@ -65,12 +65,12 @@ export default function UniformProgram({ const filteredRewardsData = useMemo( () => rewardsData.filter((reward) => showPending || reward.claimable > BigInt(0)), - [rewardsData, showPending] + [rewardsData, showPending], ); return (
-
+
Show Pending ( - { From d3993c2847d09454420953fa8336aaa0fdbcf5fa Mon Sep 17 00:00:00 2001 From: Anton Cheng Date: Wed, 8 Jan 2025 13:08:35 +0800 Subject: [PATCH 03/17] ui: align ui --- app/rewards/components/MarketProgram.tsx | 158 ++++++++++++---------- app/rewards/components/RewardContent.tsx | 90 ++++++++---- app/rewards/components/UniformProgram.tsx | 15 +- 3 files changed, 150 insertions(+), 113 deletions(-) diff --git a/app/rewards/components/MarketProgram.tsx b/app/rewards/components/MarketProgram.tsx index f39b0c6e..b2519faf 100644 --- a/app/rewards/components/MarketProgram.tsx +++ b/app/rewards/components/MarketProgram.tsx @@ -1,7 +1,6 @@ 'use client'; import { useMemo, useState } from 'react'; -import { Switch } from '@nextui-org/react'; import { Table, TableHeader, TableBody, TableColumn, TableRow, TableCell } from '@nextui-org/table'; import Image from 'next/image'; import Link from 'next/link'; @@ -23,6 +22,7 @@ type MarketProgramProps = { markets: Market[]; marketRewards: MarketProgramType[]; distributions: DistributionResponseType[]; + showPending: boolean; }; export default function MarketProgram({ @@ -30,11 +30,11 @@ export default function MarketProgram({ markets, distributions, account, + showPending, }: MarketProgramProps) { const { chainId } = useAccount(); const { switchChain } = useSwitchChain(); const [selectedToken, setSelectedToken] = useState(null); - const [showPending, setShowPending] = useState(false); const { sendTransaction } = useTransactionWithToast({ toastId: 'claim', @@ -46,10 +46,10 @@ export default function MarketProgram({ successDescription: `Successfully claimed market rewards`, }); - const allRewardTokens = useMemo( - () => { - // First, group rewards by asset address and chain ID - const groupedRewards = marketRewards.reduce((acc, reward) => { + const allRewardTokens = useMemo(() => { + // First, group rewards by asset address and chain ID + const groupedRewards = marketRewards.reduce( + (acc, reward) => { const key = `${reward.asset.address.toLowerCase()}-${reward.asset.chain_id}`; if (!acc[key]) { acc[key] = { @@ -63,60 +63,68 @@ export default function MarketProgram({ } acc[key].rewards.push(reward); return acc; - }, {} as Record); + }, + {} as Record< + string, + { + rewards: MarketProgramType[]; + token: string; + chainId: number; + distribution?: DistributionResponseType; + } + >, + ); - // Then, calculate totals for each group - return Object.values(groupedRewards).map((group) => { - const claimable = group.rewards.reduce( - (sum, reward) => - sum + - BigInt(reward.for_supply?.claimable_now ?? '0') + - BigInt(reward.for_borrow?.claimable_now ?? '0') + - BigInt(reward.for_collateral?.claimable_now ?? '0'), - BigInt(0), - ); + // Then, calculate totals for each group + return Object.values(groupedRewards).map((group) => { + const claimable = group.rewards.reduce( + (sum, reward) => + sum + + BigInt(reward.for_supply?.claimable_now ?? '0') + + BigInt(reward.for_borrow?.claimable_now ?? '0') + + BigInt(reward.for_collateral?.claimable_now ?? '0'), + BigInt(0), + ); - const pending = group.rewards.reduce( - (sum, reward) => - sum + - BigInt(reward.for_supply?.claimable_next ?? '0') + - BigInt(reward.for_borrow?.claimable_next ?? '0') + - BigInt(reward.for_collateral?.claimable_next ?? '0'), - BigInt(0), - ); + const pending = group.rewards.reduce( + (sum, reward) => + sum + + BigInt(reward.for_supply?.claimable_next ?? '0') + + BigInt(reward.for_borrow?.claimable_next ?? '0') + + BigInt(reward.for_collateral?.claimable_next ?? '0'), + BigInt(0), + ); - const total = group.rewards.reduce( - (sum, reward) => - sum + - BigInt(reward.for_supply?.total ?? '0') + - BigInt(reward.for_borrow?.total ?? '0') + - BigInt(reward.for_collateral?.total ?? '0'), - BigInt(0), - ); + const total = group.rewards.reduce( + (sum, reward) => + sum + + BigInt(reward.for_supply?.total ?? '0') + + BigInt(reward.for_borrow?.total ?? '0') + + BigInt(reward.for_collateral?.total ?? '0'), + BigInt(0), + ); - const claimed = group.rewards.reduce( - (sum, reward) => - sum + - BigInt(reward.for_supply?.claimed ?? '0') + - BigInt(reward.for_borrow?.claimed ?? '0') + - BigInt(reward.for_collateral?.claimed ?? '0'), - BigInt(0), - ); + const claimed = group.rewards.reduce( + (sum, reward) => + sum + + BigInt(reward.for_supply?.claimed ?? '0') + + BigInt(reward.for_borrow?.claimed ?? '0') + + BigInt(reward.for_collateral?.claimed ?? '0'), + BigInt(0), + ); - return { - token: group.token, - chainId: group.chainId, - distribution: group.distribution, - claimable, - pending, - total, - claimed, - rewards: group.rewards, // Keep original rewards for detail view - }; - }); - }, - [marketRewards, distributions], - ); + return { + token: group.token, + chainId: group.chainId, + distribution: group.distribution, + claimable, + pending, + total, + claimed, + rewards: group.rewards, // Keep original rewards for detail view + }; + }); + }, [marketRewards, distributions]); const filteredRewardTokens = useMemo( () => allRewardTokens.filter((tokenReward) => showPending || tokenReward.claimable > BigInt(0)), @@ -129,15 +137,6 @@ export default function MarketProgram({ return (
-
- Show Pending - -

- {formatReadable(formatBalance(tokenReward.claimable, matchedToken.decimals))} + {formatReadable( + formatBalance(tokenReward.claimable, matchedToken.decimals), + )}

- {formatReadable(formatBalance(tokenReward.pending, matchedToken.decimals))} + {formatReadable( + formatBalance(tokenReward.pending, matchedToken.decimals), + )}

- {formatReadable(formatBalance(tokenReward.claimed, matchedToken.decimals))} + {formatReadable( + formatBalance(tokenReward.claimed, matchedToken.decimals), + )}

-

{formatReadable(formatBalance(tokenReward.total, matchedToken.decimals))}

+

+ {formatReadable(formatBalance(tokenReward.total, matchedToken.decimals))} +

{ e.stopPropagation(); @@ -395,7 +403,9 @@ export default function MarketProgram({ {market.collateralAsset.symbol} {formatBalance(market.lltv, 16)}% - {formatReadable(formatBalance(supplyClaimable, matchedToken?.decimals ?? 18))} + {formatReadable( + formatBalance(supplyClaimable, matchedToken?.decimals ?? 18), + )} {formatReadable(formatBalance(supplyPending, matchedToken?.decimals ?? 18))} @@ -407,7 +417,9 @@ export default function MarketProgram({ {formatReadable(formatBalance(supplyTotal, matchedToken?.decimals ?? 18))} - {formatReadable(formatBalance(borrowClaimable, matchedToken?.decimals ?? 18))} + {formatReadable( + formatBalance(borrowClaimable, matchedToken?.decimals ?? 18), + )} {formatReadable(formatBalance(borrowPending, matchedToken?.decimals ?? 18))} @@ -434,7 +446,9 @@ export default function MarketProgram({ )} - {formatReadable(formatBalance(collateralTotal, matchedToken?.decimals ?? 18))} + {formatReadable( + formatBalance(collateralTotal, matchedToken?.decimals ?? 18), + )} ); diff --git a/app/rewards/components/RewardContent.tsx b/app/rewards/components/RewardContent.tsx index 030428ad..a311c04c 100644 --- a/app/rewards/components/RewardContent.tsx +++ b/app/rewards/components/RewardContent.tsx @@ -1,7 +1,8 @@ 'use client'; -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; import { Tooltip } from '@nextui-org/react'; +import { Switch } from '@nextui-org/react'; import { useParams } from 'next/navigation'; import { BsQuestionCircle } from 'react-icons/bs'; @@ -42,6 +43,9 @@ export default function Rewards() { const marketRewards = useMemo(() => filterMarketRewards(rewards), [rewards]); const uniformRewards = useMemo(() => filterUniformRewards(rewards), [rewards]); + const [showMarketPending, setShowMarketPending] = useState(false); + const [showUniformPending, setShowUniformPending] = useState(false); + return (
@@ -60,49 +64,77 @@ export default function Rewards() { ) : (
-
-

{PROGRAM_INFO.market.title}

- - } - placement="right" - > - - +
+
+

{PROGRAM_INFO.market.title}

+ + } + placement="right" + > +
+ +
+
+
+
+ Show Pending + +
-
-

{PROGRAM_INFO.uniform.title}

- - } - placement="right" - > - - +
+
+

{PROGRAM_INFO.uniform.title}

+ + } + placement="right" + > +
+ +
+
+
+
+ Show Pending + +
diff --git a/app/rewards/components/UniformProgram.tsx b/app/rewards/components/UniformProgram.tsx index f444b9ef..3c0c7698 100644 --- a/app/rewards/components/UniformProgram.tsx +++ b/app/rewards/components/UniformProgram.tsx @@ -1,7 +1,6 @@ 'use client'; -import { useMemo, useState } from 'react'; -import { Switch } from '@nextui-org/react'; +import { useMemo } from 'react'; import { Table, TableHeader, TableBody, TableColumn, TableRow, TableCell } from '@nextui-org/table'; import Image from 'next/image'; import Link from 'next/link'; @@ -22,16 +21,17 @@ type UniformProgramProps = { account: string; uniformRewards: UniformRewardType[]; distributions: DistributionResponseType[]; + showPending: boolean; }; export default function UniformProgram({ account, uniformRewards, distributions, + showPending, }: UniformProgramProps) { const { chainId } = useAccount(); const { switchChain } = useSwitchChain(); - const [showPending, setShowPending] = useState(false); const { sendTransaction } = useTransactionWithToast({ toastId: 'claim-uniform', @@ -70,15 +70,6 @@ export default function UniformProgram({ return (
-
- Show Pending - -
Date: Wed, 8 Jan 2025 13:09:06 +0800 Subject: [PATCH 04/17] chore: market section --- app/rewards/components/MarketProgram.tsx | 129 +++++++---------------- 1 file changed, 41 insertions(+), 88 deletions(-) diff --git a/app/rewards/components/MarketProgram.tsx b/app/rewards/components/MarketProgram.tsx index b2519faf..de4b3e92 100644 --- a/app/rewards/components/MarketProgram.tsx +++ b/app/rewards/components/MarketProgram.tsx @@ -319,18 +319,10 @@ export default function MarketProgram({ Loan AssetCollateralLLTV - Supply Claimable - Supply Pending - Supply Claimed - Supply Total - Borrow Claimable - Borrow Pending - Borrow Claimed - Borrow Total - Collateral Claimable - Collateral Pending - Collateral Claimed - Collateral Total + Claimable + Pending + Claimed + Total {markets @@ -351,44 +343,41 @@ export default function MarketProgram({ ); }); - const supplyClaimable = tokenRewardsForMarket.reduce((a: bigint, b) => { - return a + BigInt(b.for_supply?.claimable_now ?? '0'); - }, BigInt(0)); - const supplyPending = tokenRewardsForMarket.reduce((a: bigint, b) => { - return a + BigInt(b.for_supply?.claimable_next ?? '0'); - }, BigInt(0)); - const supplyTotal = tokenRewardsForMarket.reduce((a: bigint, b) => { - return a + BigInt(b.for_supply?.total ?? '0'); - }, BigInt(0)); - const supplyClaimed = tokenRewardsForMarket.reduce((a: bigint, b) => { - return a + BigInt(b.for_supply?.claimed ?? '0'); - }, BigInt(0)); + const claimable = tokenRewardsForMarket.reduce( + (sum, reward) => + sum + + BigInt(reward.for_supply?.claimable_now ?? '0') + + BigInt(reward.for_borrow?.claimable_now ?? '0') + + BigInt(reward.for_collateral?.claimable_now ?? '0'), + BigInt(0), + ); + + const pending = tokenRewardsForMarket.reduce( + (sum, reward) => + sum + + BigInt(reward.for_supply?.claimable_next ?? '0') + + BigInt(reward.for_borrow?.claimable_next ?? '0') + + BigInt(reward.for_collateral?.claimable_next ?? '0'), + BigInt(0), + ); - const borrowClaimable = tokenRewardsForMarket.reduce((a: bigint, b) => { - return a + BigInt(b.for_borrow?.claimable_now ?? '0'); - }, BigInt(0)); - const borrowPending = tokenRewardsForMarket.reduce((a: bigint, b) => { - return a + BigInt(b.for_borrow?.claimable_next ?? '0'); - }, BigInt(0)); - const borrowTotal = tokenRewardsForMarket.reduce((a: bigint, b) => { - return a + BigInt(b.for_borrow?.total ?? '0'); - }, BigInt(0)); - const borrowClaimed = tokenRewardsForMarket.reduce((a: bigint, b) => { - return a + BigInt(b.for_borrow?.claimed ?? '0'); - }, BigInt(0)); + const claimed = tokenRewardsForMarket.reduce( + (sum, reward) => + sum + + BigInt(reward.for_supply?.claimed ?? '0') + + BigInt(reward.for_borrow?.claimed ?? '0') + + BigInt(reward.for_collateral?.claimed ?? '0'), + BigInt(0), + ); - const collateralClaimable = tokenRewardsForMarket.reduce((a: bigint, b) => { - return a + BigInt(b.for_collateral?.claimable_now ?? '0'); - }, BigInt(0)); - const collateralPending = tokenRewardsForMarket.reduce((a: bigint, b) => { - return a + BigInt(b.for_collateral?.claimable_next ?? '0'); - }, BigInt(0)); - const collateralTotal = tokenRewardsForMarket.reduce((a: bigint, b) => { - return a + BigInt(b.for_collateral?.total ?? '0'); - }, BigInt(0)); - const collateralClaimed = tokenRewardsForMarket.reduce((a: bigint, b) => { - return a + BigInt(b.for_collateral?.claimed ?? '0'); - }, BigInt(0)); + const total = tokenRewardsForMarket.reduce( + (sum, reward) => + sum + + BigInt(reward.for_supply?.total ?? '0') + + BigInt(reward.for_borrow?.total ?? '0') + + BigInt(reward.for_collateral?.total ?? '0'), + BigInt(0), + ); const matchedToken = findToken(selectedToken, market.morphoBlue.chain.id); @@ -403,52 +392,16 @@ export default function MarketProgram({ {market.collateralAsset.symbol} {formatBalance(market.lltv, 16)}% - {formatReadable( - formatBalance(supplyClaimable, matchedToken?.decimals ?? 18), - )} - - - {formatReadable(formatBalance(supplyPending, matchedToken?.decimals ?? 18))} - - - {formatReadable(formatBalance(supplyClaimed, matchedToken?.decimals ?? 18))} - - - {formatReadable(formatBalance(supplyTotal, matchedToken?.decimals ?? 18))} - - - {formatReadable( - formatBalance(borrowClaimable, matchedToken?.decimals ?? 18), - )} - - - {formatReadable(formatBalance(borrowPending, matchedToken?.decimals ?? 18))} - - - {formatReadable(formatBalance(borrowClaimed, matchedToken?.decimals ?? 18))} - - - {formatReadable(formatBalance(borrowTotal, matchedToken?.decimals ?? 18))} - - - {formatReadable( - formatBalance(collateralClaimable, matchedToken?.decimals ?? 18), - )} + {formatReadable(formatBalance(claimable, matchedToken?.decimals ?? 18))} - {formatReadable( - formatBalance(collateralPending, matchedToken?.decimals ?? 18), - )} + {formatReadable(formatBalance(pending, matchedToken?.decimals ?? 18))} - {formatReadable( - formatBalance(collateralClaimed, matchedToken?.decimals ?? 18), - )} + {formatReadable(formatBalance(claimed, matchedToken?.decimals ?? 18))} - {formatReadable( - formatBalance(collateralTotal, matchedToken?.decimals ?? 18), - )} + {formatReadable(formatBalance(total, matchedToken?.decimals ?? 18))} ); From 0da84bee7abd187644cf7c370406b83c901d2563 Mon Sep 17 00:00:00 2001 From: Anton Cheng Date: Wed, 8 Jan 2025 13:11:10 +0800 Subject: [PATCH 05/17] ui: remove underlone --- app/rewards/components/MarketProgram.tsx | 2 +- app/rewards/components/UniformProgram.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/rewards/components/MarketProgram.tsx b/app/rewards/components/MarketProgram.tsx index de4b3e92..a0467525 100644 --- a/app/rewards/components/MarketProgram.tsx +++ b/app/rewards/components/MarketProgram.tsx @@ -178,7 +178,7 @@ export default function MarketProgram({ href={getAssetURL(tokenReward.token, tokenReward.chainId)} target="_blank" rel="noopener noreferrer" - className="flex items-center justify-center gap-2 hover:opacity-80" + className="flex items-center justify-center gap-2 hover:opacity-80 no-underline" onClick={(e) => e.stopPropagation()} >

{matchedToken.symbol}

diff --git a/app/rewards/components/UniformProgram.tsx b/app/rewards/components/UniformProgram.tsx index 3c0c7698..91321900 100644 --- a/app/rewards/components/UniformProgram.tsx +++ b/app/rewards/components/UniformProgram.tsx @@ -96,7 +96,8 @@ export default function UniformProgram({ href={getAssetURL(reward.asset.address, reward.asset.chain_id)} target="_blank" rel="noopener noreferrer" - className="flex items-center justify-center gap-2 hover:opacity-80" + className="flex items-center justify-center gap-2 hover:opacity-80 no-underline" + onClick={(e) => e.stopPropagation()} >

{reward.token?.symbol ?? 'Unknown'}

Date: Wed, 8 Jan 2025 18:34:12 +0800 Subject: [PATCH 06/17] chore: vaults --- app/rewards/components/MarketProgram.tsx | 2 +- app/rewards/components/RewardContent.tsx | 59 ++++- app/rewards/components/UniformProgram.tsx | 2 +- app/rewards/components/VaultProgram.tsx | 272 ++++++++++++++++++++++ src/components/TokenIcon.tsx | 1 + src/utils/tokens.ts | 6 + src/utils/types.ts | 30 ++- 7 files changed, 361 insertions(+), 11 deletions(-) create mode 100644 app/rewards/components/VaultProgram.tsx diff --git a/app/rewards/components/MarketProgram.tsx b/app/rewards/components/MarketProgram.tsx index a0467525..b85581b6 100644 --- a/app/rewards/components/MarketProgram.tsx +++ b/app/rewards/components/MarketProgram.tsx @@ -178,7 +178,7 @@ export default function MarketProgram({ href={getAssetURL(tokenReward.token, tokenReward.chainId)} target="_blank" rel="noopener noreferrer" - className="flex items-center justify-center gap-2 hover:opacity-80 no-underline" + className="flex items-center justify-center gap-2 no-underline hover:opacity-80" onClick={(e) => e.stopPropagation()} >

{matchedToken.symbol}

diff --git a/app/rewards/components/RewardContent.tsx b/app/rewards/components/RewardContent.tsx index a311c04c..33369ec8 100644 --- a/app/rewards/components/RewardContent.tsx +++ b/app/rewards/components/RewardContent.tsx @@ -15,22 +15,29 @@ import useUserRewards from '@/hooks/useRewards'; import { filterMarketRewards, filterUniformRewards } from '@/utils/rewardHelpers'; import MarketProgram from './MarketProgram'; import UniformProgram from './UniformProgram'; +import VaultProgram from './VaultProgram'; const PROGRAM_INFO = { market: { title: 'Market Program', tooltip: { - title: 'Market Program Rewards', + title: 'Market Program', detail: - 'Market Program Rewards are incentives tailored to specific markets on Morpho. These rewards encourage particular actions within each market, such as supplying, borrowing, or providing collateral. The program may include additional incentives designed to stimulate activity in targeted markets.', + 'Rewards for supplying, borrowing, or using assets as collateral in Morpho Blue markets.', }, }, uniform: { title: 'Uniform Program', tooltip: { - title: 'Uniform Program Rewards', - detail: - 'The Uniform Program is a new reward system that applies to all users who supply to Morpho, regardless of the specific market. It provides a consistent reward rate for each dollar supplied across eligible markets, promoting broader participation in the Morpho ecosystem.', + title: 'Uniform Program', + detail: 'Rewards distributed uniformly to all users based on their activity.', + }, + }, + vault: { + title: 'Vault Program', + tooltip: { + title: 'Vault Program', + detail: 'Rewards for depositing assets in Morpho vaults.', }, }, }; @@ -42,9 +49,11 @@ export default function Rewards() { const marketRewards = useMemo(() => filterMarketRewards(rewards), [rewards]); const uniformRewards = useMemo(() => filterUniformRewards(rewards), [rewards]); + const vaultRewards = useMemo(() => rewards.filter((r) => r.type === 'vault-reward'), [rewards]); const [showMarketPending, setShowMarketPending] = useState(false); const [showUniformPending, setShowUniformPending] = useState(false); + const [showVaultPending, setShowVaultPending] = useState(false); return (
@@ -52,9 +61,6 @@ export default function Rewards() {

Reward

-
- Morpho offers multiple reward programs to incentivize user participation. -
{loading || loadingRewards ? ( @@ -137,6 +143,43 @@ export default function Rewards() { showPending={showUniformPending} /> + +
+
+
+

{PROGRAM_INFO.vault.title}

+ + } + placement="right" + > +
+ +
+
+
+
+ Show Pending + +
+
+ +
)}
diff --git a/app/rewards/components/UniformProgram.tsx b/app/rewards/components/UniformProgram.tsx index 91321900..2157fa91 100644 --- a/app/rewards/components/UniformProgram.tsx +++ b/app/rewards/components/UniformProgram.tsx @@ -96,7 +96,7 @@ export default function UniformProgram({ href={getAssetURL(reward.asset.address, reward.asset.chain_id)} target="_blank" rel="noopener noreferrer" - className="flex items-center justify-center gap-2 hover:opacity-80 no-underline" + className="flex items-center justify-center gap-2 no-underline hover:opacity-80" onClick={(e) => e.stopPropagation()} >

{reward.token?.symbol ?? 'Unknown'}

diff --git a/app/rewards/components/VaultProgram.tsx b/app/rewards/components/VaultProgram.tsx new file mode 100644 index 00000000..fefee736 --- /dev/null +++ b/app/rewards/components/VaultProgram.tsx @@ -0,0 +1,272 @@ +'use client'; + +import { useMemo, useState } from 'react'; +import { Table, TableHeader, TableBody, TableColumn, TableRow, TableCell } from '@nextui-org/table'; +import Link from 'next/link'; +import { useAccount, useSwitchChain } from 'wagmi'; +import { Button } from '@/components/common'; +import { TokenIcon } from '@/components/TokenIcon'; +import { DistributionResponseType } from '@/hooks/useRewards'; +import { useTransactionWithToast } from '@/hooks/useTransactionWithToast'; +import { formatBalance, formatReadable } from '@/utils/balance'; +import { getAssetURL } from '@/utils/external'; +import { findToken } from '@/utils/tokens'; +import { VaultRewardType } from '@/utils/types'; + +type VaultProgramProps = { + account?: string; + vaultRewards: VaultRewardType[]; + showPending: boolean; + distributions: DistributionResponseType[]; +} + +export default function VaultProgram({ + distributions, + vaultRewards, + showPending, +}: VaultProgramProps) { + const { chainId, address: account } = useAccount(); + const { switchChain } = useSwitchChain(); + const [selectedVault, setSelectedVault] = useState(null); + + const { sendTransaction } = useTransactionWithToast({ + toastId: 'claim-vault', + pendingText: 'Claiming vault rewards...', + errorText: 'Something went wrong', + successText: 'Reward Claimed', + successDescription: 'Successfully claimed vault rewards', + chainId, + }); + + const allRewardTokens = useMemo(() => { + // First, group rewards by asset address and chain ID + const groupedRewards = vaultRewards.reduce( + (acc, reward) => { + const key = `${reward.asset.address.toLowerCase()}-${reward.asset.chain_id}`; + if (!acc[key]) { + acc[key] = { + rewards: [], + token: reward.asset.address, + chainId: reward.asset.chain_id, + distribution: distributions.find( + (d) => d.asset.address.toLowerCase() === reward.asset.address.toLowerCase(), + ), + }; + } + acc[key].rewards.push(reward); + return acc; + }, + {} as Record< + string, + { + rewards: VaultRewardType[]; + token: string; + chainId: number; + distribution?: DistributionResponseType; + } + >, + ); + + // Then, calculate totals for each group + return Object.values(groupedRewards).map((group) => { + const claimable = group.rewards.reduce( + (sum, reward) => sum + BigInt(reward.for_supply?.claimable_now ?? '0'), + BigInt(0), + ); + + const pending = group.rewards.reduce( + (sum, reward) => sum + BigInt(reward.for_supply?.claimable_next ?? '0'), + BigInt(0), + ); + + const claimed = group.rewards.reduce( + (sum, reward) => sum + BigInt(reward.for_supply?.claimed ?? '0'), + BigInt(0), + ); + + const total = group.rewards.reduce( + (sum, reward) => sum + BigInt(reward.for_supply?.total ?? '0'), + BigInt(0), + ); + + return { + token: group.token, + chainId: group.chainId, + distribution: group.distribution, + claimable, + pending, + total, + claimed, + rewards: group.rewards, + }; + }); + }, [vaultRewards, distributions]); + + const filteredRewardTokens = useMemo( + () => allRewardTokens.filter((tokenReward) => showPending || tokenReward.claimable > BigInt(0)), + [allRewardTokens, showPending], + ); + + const handleRowClick = (token: string) => { + setSelectedVault((prevVault) => (prevVault === token ? null : token)); + }; + + return ( +
+
+
+ + Asset + Chain + Claimable + Pending + Claimed + Total + Action + + + {filteredRewardTokens + .filter((tokenReward) => tokenReward !== null && tokenReward !== undefined) + .map((tokenReward, index) => { + const matchedToken = findToken(tokenReward.token, tokenReward.chainId) ?? { + symbol: 'Unknown', + img: undefined, + decimals: 18, + }; + + return ( + handleRowClick(tokenReward.token)} + > + + e.stopPropagation()} + > +

{matchedToken.symbol}

+ + +
+ +
+ +
+
+ +
+

+ {formatReadable( + formatBalance(tokenReward.claimable, matchedToken.decimals), + )} +

+ +
+
+ +
+

+ {formatReadable( + formatBalance(tokenReward.pending, matchedToken.decimals), + )} +

+ +
+
+ +
+

+ {formatReadable( + formatBalance(tokenReward.claimed, matchedToken.decimals), + )} +

+ +
+
+ +
+

+ {formatReadable(formatBalance(tokenReward.total, matchedToken.decimals))} +

+ +
+
+ +
+ +
+
+
+ ); + })} +
+
+
+
+ ); +} diff --git a/src/components/TokenIcon.tsx b/src/components/TokenIcon.tsx index 694ff80e..b53e45ca 100644 --- a/src/components/TokenIcon.tsx +++ b/src/components/TokenIcon.tsx @@ -19,6 +19,7 @@ export function TokenIcon({ address, chainId, width, height, opacity }: TokenIco return ( {token.symbol Date: Mon, 20 Jan 2025 13:30:19 +0800 Subject: [PATCH 07/17] chore: add address display --- app/positions/components/PositionsContent.tsx | 24 +--------- app/rewards/components/RewardContent.tsx | 8 +++- src/components/common/AddressDisplay.tsx | 46 +++++++++++++++++++ 3 files changed, 55 insertions(+), 23 deletions(-) create mode 100644 src/components/common/AddressDisplay.tsx diff --git a/app/positions/components/PositionsContent.tsx b/app/positions/components/PositionsContent.tsx index 14919508..2812ed73 100644 --- a/app/positions/components/PositionsContent.tsx +++ b/app/positions/components/PositionsContent.tsx @@ -26,6 +26,7 @@ import { MarketPosition } from '@/utils/types'; import { SetupAgentModal } from './agent/SetupAgentModal'; import { OnboardingModal } from './onboarding/Modal'; import { PositionsSummaryTable } from './PositionsSummaryTable'; +import { AddressDisplay } from '@/components/common/AddressDisplay'; export default function Positions() { const [showSupplyModal, setShowSupplyModal] = useState(false); @@ -71,28 +72,7 @@ export default function Positions() {

Portfolio

-
-
- - {isConnected && account === address && ( -
-
- -
-
- )} -
-
- -
-
+
+ ); + + if (button.tooltip) { + return ( + + } + placement="top" + > + {ButtonComponent} + + ); + } + + return ButtonComponent; + } + + if (badge) { + if (badge.tooltip) { + return ( + + } + placement="top" + > +
+ {badge.text} +
+
+ ); + } + + return {badge.text}; + } + + return null; + }; + return (
-
- {title} - {tooltip && ( - - } - placement="right" - > -
- -
-
- )} -
-
- {badge && - (badge.tooltip ? ( + {title} + {tooltip && ( } - placement="top" + placement="right" > -
- {badge.text} +
+
- ) : ( - {badge.text} - ))} + )} +
+
{children}
diff --git a/app/rewards/components/RewardContent.tsx b/app/rewards/components/RewardContent.tsx index 2ee923ba..95f2d6df 100644 --- a/app/rewards/components/RewardContent.tsx +++ b/app/rewards/components/RewardContent.tsx @@ -20,10 +20,12 @@ import { MORPHO_LEGACY, MORPHO_TOKEN_BASE, MORPHO_TOKEN_MAINNET } from '@/utils/ import { MarketRewardType, RewardAmount, AggregatedRewardType } from '@/utils/types'; import InfoCard from './InfoCard'; import RewardTable from './RewardTable'; +import { useWrapLegacyMorpho } from '@/hooks/useWrapLegacyMorpho'; +import { WrapProcessModal } from '@/components/WrapProcessModal'; export default function Rewards() { const { account } = useParams<{ account: string }>(); - const { rewards, distributions, loading: loadingRewards } = useUserRewards(account); + const { rewards, distributions, loading: loadingRewards, refresh } = useUserRewards(account); const [showPending, setShowPending] = useState(false); @@ -130,6 +132,16 @@ export default function Rewards() { const canClaim = useMemo(() => totalClaimable > 0n, [totalClaimable]); + const showLegacy = useMemo(() => morphoBalanceLegacy && morphoBalanceLegacy.value !== 0n, [morphoBalanceLegacy]) + + const { wrap, currentStep, showProcessModal, setShowProcessModal } = useWrapLegacyMorpho( + morphoBalanceLegacy?.value ?? 0n, + () => { + // Refresh rewards data after successful wrap + refresh(); + }, + ); + return (
@@ -211,26 +223,24 @@ export default function Rewards() {
- {/* only show this if legacy balance is not 0 => need wrapping */} - {morphoBalanceLegacy && morphoBalanceLegacy.value !== 0n && ( + {showLegacy && (
- {formatSimple(formatBalance(morphoBalanceLegacy?.value, 18))} + {morphoBalanceLegacy && ( + {formatSimple(formatBalance(morphoBalanceLegacy?.value, 18))} + )}
+ {showProcessModal && ( + setShowProcessModal(false)} + /> + )}
); } diff --git a/src/components/WrapProcessModal.tsx b/src/components/WrapProcessModal.tsx new file mode 100644 index 00000000..6cf048d5 --- /dev/null +++ b/src/components/WrapProcessModal.tsx @@ -0,0 +1,93 @@ +import React, { useMemo } from 'react'; +import { Cross1Icon } from '@radix-ui/react-icons'; +import { motion, AnimatePresence } from 'framer-motion'; +import { FaCheckCircle, FaCircle } from 'react-icons/fa'; +import { WrapStep } from '@/hooks/useWrapLegacyMorpho'; +import { formatBalance } from '@/utils/balance'; + +type WrapProcessModalProps = { + amount: bigint; + currentStep: WrapStep; + onClose: () => void; +}; + +export function WrapProcessModal({ + amount, + currentStep, + onClose, +}: WrapProcessModalProps): JSX.Element { + const steps = useMemo( + () => [ + { + key: 'approve', + label: 'Approve Wrapper', + detail: 'Approve the wrapper contract to spend your legacy MORPHO tokens', + }, + { + key: 'wrap', + label: 'Wrap MORPHO', + detail: 'Confirm transaction to wrap your legacy MORPHO tokens', + }, + ], + [], + ); + + return ( +
+ +
+

Wrapping {formatBalance(amount, 18)} MORPHO

+ +
+ +
+ + {steps.map((step, index) => { + const isActive = currentStep === step.key; + const isPassed = steps.findIndex((s) => s.key === currentStep) > index; + + return ( + +
+ {isPassed ? ( + + ) : ( + + )} +
+
+

{step.label}

+

{step.detail}

+
+
+ ); + })} +
+
+
+
+ ); +} diff --git a/src/hooks/useERC20Approval.ts b/src/hooks/useERC20Approval.ts index bf08b02a..9260b76a 100644 --- a/src/hooks/useERC20Approval.ts +++ b/src/hooks/useERC20Approval.ts @@ -8,14 +8,18 @@ export function useERC20Approval({ spender, amount, tokenSymbol, + chainId, }: { token: Address; spender: Address; amount: bigint; tokenSymbol: string; + chainId?: number }) { const { address: account } = useAccount(); - const chainId = useChainId(); + const currentChain = useChainId(); + + const chainIdToUse = chainId ?? currentChain; const { data: allowance, refetch: refetchAllowance } = useReadContract({ address: token, @@ -25,6 +29,7 @@ export function useERC20Approval({ query: { enabled: !!account, }, + chainId: chainIdToUse }); const isApproved = useMemo(() => { @@ -37,7 +42,7 @@ export function useERC20Approval({ pendingText: `Approving ${tokenSymbol}`, successText: `${tokenSymbol} Approved`, errorText: 'Failed to approve', - chainId, + chainId: chainIdToUse, pendingDescription: `Approving ${tokenSymbol} for spender ${spender.slice(2, 8)}...`, successDescription: `Successfully approved ${tokenSymbol} for spender ${spender.slice(2, 8)}`, }); diff --git a/src/hooks/useRewards.ts b/src/hooks/useRewards.ts index b6fd103b..6d5a1ac9 100644 --- a/src/hooks/useRewards.ts +++ b/src/hooks/useRewards.ts @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { Address } from 'viem'; import { RewardResponseType } from '@/utils/types'; import { URLS } from '@/utils/urls'; @@ -26,50 +26,59 @@ const useUserRewards = (user: string | undefined) => { const [distributions, setDistributions] = useState([]); const [error, setError] = useState(null); - useEffect(() => { - const fetchData = async () => { - try { - setLoading(true); - const [totalRewardsRes, distributionRes] = await Promise.all([ - fetch(`${URLS.MORPHO_REWARDS_API}/users/${user}/rewards`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }), - fetch(`${URLS.MORPHO_REWARDS_API}/users/${user}/distributions`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }), - ]); + const fetchData = useCallback(async () => { + if (!user) { + setLoading(false); + return; + } - const newRewards = (await totalRewardsRes.json()).data as RewardResponseType[]; - const newDistributions = (await distributionRes.json()).data as DistributionResponseType[]; + try { + setLoading(true); + const [totalRewardsRes, distributionRes] = await Promise.all([ + fetch(`${URLS.MORPHO_REWARDS_API}/users/${user}/rewards`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }), + fetch(`${URLS.MORPHO_REWARDS_API}/users/${user}/distributions`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }), + ]); - if (Array.isArray(newDistributions)) { - setDistributions(newDistributions); - } else { - setDistributions([newDistributions]); - } + const newRewards = (await totalRewardsRes.json()).data as RewardResponseType[]; + const newDistributions = (await distributionRes.json()).data as DistributionResponseType[]; + if (Array.isArray(newDistributions)) { + setDistributions(newDistributions); + } + if (Array.isArray(newRewards)) { setRewards(newRewards); - - setLoading(false); - } catch (_error) { - console.log('err', _error); - setError(_error); - setLoading(false); } - }; - - if (!user) return; - - fetchData().catch(console.error); + setError(null); + } catch (err) { + setError(err); + setRewards([]); + setDistributions([]); + } finally { + setLoading(false); + } }, [user]); - return { loading, rewards, distributions, error }; + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { + rewards, + distributions, + loading, + error, + refresh: fetchData, + }; }; export default useUserRewards; diff --git a/src/hooks/useWrapLegacyMorpho.ts b/src/hooks/useWrapLegacyMorpho.ts new file mode 100644 index 00000000..14d50d71 --- /dev/null +++ b/src/hooks/useWrapLegacyMorpho.ts @@ -0,0 +1,85 @@ +import { useCallback, useState } from 'react'; +import { toast } from 'react-toastify'; +import { Address, encodeFunctionData } from 'viem'; +import { useAccount, useSwitchChain } from 'wagmi'; + +import { useTransactionWithToast } from '@/hooks/useTransactionWithToast'; +import { SupportedNetworks } from '@/utils/networks'; +import { MORPHO_LEGACY, MORPHO_TOKEN_WRAPPER } from '@/utils/tokens'; +import { useERC20Approval } from './useERC20Approval'; +import wrapperABI from '@/abis/morpho-wrapper'; + +export type WrapStep = 'approve' | 'wrap'; + +export function useWrapLegacyMorpho(amount: bigint, onSuccess?: () => void) { + const [currentStep, setCurrentStep] = useState('approve'); + const [showProcessModal, setShowProcessModal] = useState(false); + + const { address: account, chainId } = useAccount(); + const { switchChainAsync } = useSwitchChain(); + + const { isApproved, approve } = useERC20Approval({ + token: MORPHO_LEGACY as Address, + spender: MORPHO_TOKEN_WRAPPER as Address, + amount, + tokenSymbol: 'MORPHO', + chainId: SupportedNetworks.Mainnet, + }); + + const { sendTransactionAsync } = useTransactionWithToast({ + toastId: 'wrap-morpho', + pendingText: 'Wrapping MORPHO...', + successText: 'Successfully wrapped MORPHO tokens!', + errorText: 'Failed to wrap MORPHO tokens', + onSuccess: () => { + setShowProcessModal(false); + onSuccess?.(); + }, + }); + + const wrap = useCallback(async () => { + try { + if (!account) { + toast.error('Wallet not connected'); + return; + } + + if (chainId !== SupportedNetworks.Mainnet) { + await switchChainAsync({ chainId: SupportedNetworks.Mainnet }); + toast.info('Network changed'); + } + + setShowProcessModal(true); + if (!isApproved) { + setCurrentStep('approve'); + await approve().catch((err) => { + toast.error('Failed to approve MORPHO: ' + (err.message || 'Unknown error')); + setShowProcessModal(false); + throw err; + }); + } + + setCurrentStep('wrap'); + await sendTransactionAsync({ + account: account, + to: MORPHO_TOKEN_WRAPPER, + data: encodeFunctionData({ + abi: wrapperABI, + functionName: 'depositFor', + args: [account, amount], + }), + }); + } catch (err) { + toast.error('Failed to wrap MORPHO.'); + setShowProcessModal(false); + } + }, [account, amount, chainId, isApproved, approve, sendTransactionAsync, switchChainAsync]); + + return { + wrap, + currentStep, + showProcessModal, + setShowProcessModal, + isApproved, + }; +} diff --git a/src/utils/tokens.ts b/src/utils/tokens.ts index e69e2b3f..3b85ba50 100644 --- a/src/utils/tokens.ts +++ b/src/utils/tokens.ts @@ -23,6 +23,9 @@ const MORPHO_TOKEN_BASE = '0xBAa5CC21fd487B8Fcc2F632f3F4E8D37262a0842'; const MORPHO_TOKEN_MAINNET = '0x58D97B57BB95320F9a05dC918Aef65434969c2B2'; const MORPHO_LEGACY = '0x9994E35Db50125E0DF82e4c2dde62496CE330999'; +// wrapper to convert legacy morpho tokens +const MORPHO_TOKEN_WRAPPER = '0x9d03bb2092270648d7480049d0e58d2fcf0e5123' + const supportedTokens = [ { symbol: 'USDC', @@ -508,4 +511,5 @@ export { MORPHO_TOKEN_BASE, MORPHO_TOKEN_MAINNET, MORPHO_LEGACY, + MORPHO_TOKEN_WRAPPER }; From 771d81371d453a6796664803e8d37123a782d7ea Mon Sep 17 00:00:00 2001 From: antoncoding Date: Mon, 20 Jan 2025 19:35:39 +0800 Subject: [PATCH 13/17] chore: lint --- app/rewards/components/InfoCard.tsx | 141 +++++++++++++---------- app/rewards/components/RewardContent.tsx | 25 ++-- app/rewards/components/RewardTable.tsx | 17 ++- src/components/WrapProcessModal.tsx | 5 +- src/hooks/useERC20Approval.ts | 6 +- src/hooks/useRewards.ts | 2 +- src/hooks/useWrapLegacyMorpho.ts | 8 +- src/utils/tokens.ts | 4 +- 8 files changed, 118 insertions(+), 90 deletions(-) diff --git a/app/rewards/components/InfoCard.tsx b/app/rewards/components/InfoCard.tsx index dffdbbfb..d301767f 100644 --- a/app/rewards/components/InfoCard.tsx +++ b/app/rewards/components/InfoCard.tsx @@ -28,6 +28,73 @@ type InfoCardProps = { className?: string; }; +function BadgeButton({ + text, + variant, + disabled, + handleClick, + tooltip, +}: { + text: string; + variant?: 'success' | 'warning' | 'danger' | 'primary' | 'default'; + disabled?: boolean; + handleClick: () => void; + tooltip?: TooltipInfo; +}) { + const ButtonComponent = ( + + ); + + if (tooltip) { + return ( + + } + placement="top" + > + {ButtonComponent} + + ); + } + + return ButtonComponent; +} + +function BadgeComponent({ + text, + variant, + tooltip, +}: { + text: string; + variant?: 'success' | 'warning' | 'danger' | 'primary' | 'default'; + tooltip?: TooltipInfo; +}) { + const BadgeElement = {text}; + + if (tooltip) { + return ( + + } + placement="top" + > +
{BadgeElement}
+
+ ); + } + + return BadgeElement; +} + export default function InfoCard({ title, children, @@ -36,66 +103,6 @@ export default function InfoCard({ button, className = '', }: InfoCardProps) { - const ActionComponent = () => { - if (button) { - const ButtonComponent = ( - - ); - - if (button.tooltip) { - return ( - - } - placement="top" - > - {ButtonComponent} - - ); - } - - return ButtonComponent; - } - - if (badge) { - if (badge.tooltip) { - return ( - - } - placement="top" - > -
- {badge.text} -
-
- ); - } - - return {badge.text}; - } - - return null; - }; - return (
@@ -118,7 +125,19 @@ export default function InfoCard({ )}
- +
+ {button ? ( + + ) : badge ? ( + + ) : null} +
{children}
diff --git a/app/rewards/components/RewardContent.tsx b/app/rewards/components/RewardContent.tsx index 95f2d6df..a9204411 100644 --- a/app/rewards/components/RewardContent.tsx +++ b/app/rewards/components/RewardContent.tsx @@ -12,22 +12,22 @@ import EmptyScreen from '@/components/Status/EmptyScreen'; import LoadingScreen from '@/components/Status/LoadingScreen'; import { TokenIcon } from '@/components/TokenIcon'; import { TooltipContent } from '@/components/TooltipContent'; +import { WrapProcessModal } from '@/components/WrapProcessModal'; import useUserRewards from '@/hooks/useRewards'; +import { useWrapLegacyMorpho } from '@/hooks/useWrapLegacyMorpho'; import { formatBalance, formatSimple } from '@/utils/balance'; import { SupportedNetworks } from '@/utils/networks'; import { MORPHO_LEGACY, MORPHO_TOKEN_BASE, MORPHO_TOKEN_MAINNET } from '@/utils/tokens'; import { MarketRewardType, RewardAmount, AggregatedRewardType } from '@/utils/types'; import InfoCard from './InfoCard'; import RewardTable from './RewardTable'; -import { useWrapLegacyMorpho } from '@/hooks/useWrapLegacyMorpho'; -import { WrapProcessModal } from '@/components/WrapProcessModal'; export default function Rewards() { const { account } = useParams<{ account: string }>(); const { rewards, distributions, loading: loadingRewards, refresh } = useUserRewards(account); - const [showPending, setShowPending] = useState(false); + const [showClaimed, setShowClaimed] = useState(false); const { data: morphoBalanceMainnet } = useBalance({ token: MORPHO_TOKEN_MAINNET, @@ -132,13 +132,16 @@ export default function Rewards() { const canClaim = useMemo(() => totalClaimable > 0n, [totalClaimable]); - const showLegacy = useMemo(() => morphoBalanceLegacy && morphoBalanceLegacy.value !== 0n, [morphoBalanceLegacy]) + const showLegacy = useMemo( + () => morphoBalanceLegacy && morphoBalanceLegacy.value !== 0n, + [morphoBalanceLegacy], + ); const { wrap, currentStep, showProcessModal, setShowProcessModal } = useWrapLegacyMorpho( morphoBalanceLegacy?.value ?? 0n, () => { // Refresh rewards data after successful wrap - refresh(); + void refresh(); }, ); @@ -233,7 +236,9 @@ export default function Rewards() { button={{ text: 'Wrap Now', variant: 'success', - onClick: wrap, + onClick: () => { + void wrap(); + }, disabled: showProcessModal, }} > @@ -258,11 +263,11 @@ export default function Rewards() {

All Rewards

- Show Pending + Show Claimed
@@ -276,7 +281,7 @@ export default function Rewards() { account={account} rewards={allRewards} distributions={distributions} - showPending={showPending} + showClaimed={showClaimed} /> )} diff --git a/app/rewards/components/RewardTable.tsx b/app/rewards/components/RewardTable.tsx index db09ca8c..9d089acb 100644 --- a/app/rewards/components/RewardTable.tsx +++ b/app/rewards/components/RewardTable.tsx @@ -21,14 +21,14 @@ type RewardTableProps = { account: string; rewards: AggregatedRewardType[]; distributions: DistributionResponseType[]; - showPending: boolean; + showClaimed: boolean; }; export default function RewardTable({ rewards, distributions, account, - showPending, + showClaimed, }: RewardTableProps) { const { chainId } = useAccount(); const { switchChain } = useSwitchChain(); @@ -44,8 +44,17 @@ export default function RewardTable({ }); const filteredRewardTokens = useMemo( - () => rewards.filter((tokenReward) => showPending || tokenReward.total.claimable > BigInt(0)), - [rewards, showPending], + () => + rewards.filter((tokenReward) => { + if (showClaimed) return true; + + // if showClaimed is not turned on, only show tokens that are claimable or have pending + if (tokenReward.total.claimable === 0n && tokenReward.total.pendingAmount === 0n) + return false; + + return true; + }), + [rewards, showClaimed], ); return ( diff --git a/src/components/WrapProcessModal.tsx b/src/components/WrapProcessModal.tsx index 6cf048d5..93cf3004 100644 --- a/src/components/WrapProcessModal.tsx +++ b/src/components/WrapProcessModal.tsx @@ -43,6 +43,7 @@ export function WrapProcessModal({

Wrapping {formatBalance(amount, 18)} MORPHO

diff --git a/src/hooks/useERC20Approval.ts b/src/hooks/useERC20Approval.ts index 9260b76a..0a6455b7 100644 --- a/src/hooks/useERC20Approval.ts +++ b/src/hooks/useERC20Approval.ts @@ -14,7 +14,7 @@ export function useERC20Approval({ spender: Address; amount: bigint; tokenSymbol: string; - chainId?: number + chainId?: number; }) { const { address: account } = useAccount(); const currentChain = useChainId(); @@ -29,7 +29,7 @@ export function useERC20Approval({ query: { enabled: !!account, }, - chainId: chainIdToUse + chainId: chainIdToUse, }); const isApproved = useMemo(() => { @@ -42,7 +42,7 @@ export function useERC20Approval({ pendingText: `Approving ${tokenSymbol}`, successText: `${tokenSymbol} Approved`, errorText: 'Failed to approve', - chainId: chainIdToUse, + chainId: chainIdToUse, pendingDescription: `Approving ${tokenSymbol} for spender ${spender.slice(2, 8)}...`, successDescription: `Successfully approved ${tokenSymbol} for spender ${spender.slice(2, 8)}`, }); diff --git a/src/hooks/useRewards.ts b/src/hooks/useRewards.ts index 6d5a1ac9..d150fecc 100644 --- a/src/hooks/useRewards.ts +++ b/src/hooks/useRewards.ts @@ -69,7 +69,7 @@ const useUserRewards = (user: string | undefined) => { }, [user]); useEffect(() => { - fetchData(); + void fetchData(); }, [fetchData]); return { diff --git a/src/hooks/useWrapLegacyMorpho.ts b/src/hooks/useWrapLegacyMorpho.ts index 14d50d71..5e43fefa 100644 --- a/src/hooks/useWrapLegacyMorpho.ts +++ b/src/hooks/useWrapLegacyMorpho.ts @@ -3,11 +3,11 @@ import { toast } from 'react-toastify'; import { Address, encodeFunctionData } from 'viem'; import { useAccount, useSwitchChain } from 'wagmi'; +import wrapperABI from '@/abis/morpho-wrapper'; import { useTransactionWithToast } from '@/hooks/useTransactionWithToast'; import { SupportedNetworks } from '@/utils/networks'; import { MORPHO_LEGACY, MORPHO_TOKEN_WRAPPER } from '@/utils/tokens'; import { useERC20Approval } from './useERC20Approval'; -import wrapperABI from '@/abis/morpho-wrapper'; export type WrapStep = 'approve' | 'wrap'; @@ -52,11 +52,7 @@ export function useWrapLegacyMorpho(amount: bigint, onSuccess?: () => void) { setShowProcessModal(true); if (!isApproved) { setCurrentStep('approve'); - await approve().catch((err) => { - toast.error('Failed to approve MORPHO: ' + (err.message || 'Unknown error')); - setShowProcessModal(false); - throw err; - }); + await approve(); } setCurrentStep('wrap'); diff --git a/src/utils/tokens.ts b/src/utils/tokens.ts index 3b85ba50..f26b8758 100644 --- a/src/utils/tokens.ts +++ b/src/utils/tokens.ts @@ -24,7 +24,7 @@ const MORPHO_TOKEN_MAINNET = '0x58D97B57BB95320F9a05dC918Aef65434969c2B2'; const MORPHO_LEGACY = '0x9994E35Db50125E0DF82e4c2dde62496CE330999'; // wrapper to convert legacy morpho tokens -const MORPHO_TOKEN_WRAPPER = '0x9d03bb2092270648d7480049d0e58d2fcf0e5123' +const MORPHO_TOKEN_WRAPPER = '0x9d03bb2092270648d7480049d0e58d2fcf0e5123'; const supportedTokens = [ { @@ -511,5 +511,5 @@ export { MORPHO_TOKEN_BASE, MORPHO_TOKEN_MAINNET, MORPHO_LEGACY, - MORPHO_TOKEN_WRAPPER + MORPHO_TOKEN_WRAPPER, }; From fe299e5cbc4e8c75137089ced4c44400b15a45e6 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Mon, 20 Jan 2025 19:40:46 +0800 Subject: [PATCH 14/17] chore: remove Ionic --- src/imgs/tokens/Ionic.png | Bin 341 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/imgs/tokens/Ionic.png diff --git a/src/imgs/tokens/Ionic.png b/src/imgs/tokens/Ionic.png deleted file mode 100644 index 7c506135977c065b22a605801e77eeaae7574233..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 341 zcmV-b0jmCqP)qS^QY;Zm zC=pL8FxhhrJ0B^!V+9)ySO=Zo0002UNklN& zT8^#Je+|fXIxHGUP n#|-4rGV(NrJ^%kYkN?&WvOx*qTMvF*00000NkvXXu0mjfgNA|c From 86be10f2241138e5af7609f3d8c1355199e7b109 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Mon, 20 Jan 2025 19:41:25 +0800 Subject: [PATCH 15/17] chore: add ionic --- src/imgs/tokens/ionic.png | Bin 0 -> 341 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/imgs/tokens/ionic.png diff --git a/src/imgs/tokens/ionic.png b/src/imgs/tokens/ionic.png new file mode 100644 index 0000000000000000000000000000000000000000..7c506135977c065b22a605801e77eeaae7574233 GIT binary patch literal 341 zcmV-b0jmCqP)qS^QY;Zm zC=pL8FxhhrJ0B^!V+9)ySO=Zo0002UNklN& zT8^#Je+|fXIxHGUP n#|-4rGV(NrJ^%kYkN?&WvOx*qTMvF*00000NkvXXu0mjfgNA|c literal 0 HcmV?d00001 From 6a9c6007ffa9e6b350dc66f37792219ebc187b20 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Mon, 20 Jan 2025 20:19:55 +0800 Subject: [PATCH 16/17] chore: dark --- app/rewards/components/RewardContent.tsx | 9 +++++---- src/components/WrapProcessModal.tsx | 11 ++++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/app/rewards/components/RewardContent.tsx b/app/rewards/components/RewardContent.tsx index a9204411..fac17992 100644 --- a/app/rewards/components/RewardContent.tsx +++ b/app/rewards/components/RewardContent.tsx @@ -132,10 +132,11 @@ export default function Rewards() { const canClaim = useMemo(() => totalClaimable > 0n, [totalClaimable]); - const showLegacy = useMemo( - () => morphoBalanceLegacy && morphoBalanceLegacy.value !== 0n, - [morphoBalanceLegacy], - ); + const showLegacy = true; + // useMemo( + // () => morphoBalanceLegacy && morphoBalanceLegacy.value !== 0n, + // [morphoBalanceLegacy], + // ); const { wrap, currentStep, showProcessModal, setShowProcessModal } = useWrapLegacyMorpho( morphoBalanceLegacy?.value ?? 0n, diff --git a/src/components/WrapProcessModal.tsx b/src/components/WrapProcessModal.tsx index 93cf3004..337d5466 100644 --- a/src/components/WrapProcessModal.tsx +++ b/src/components/WrapProcessModal.tsx @@ -35,17 +35,22 @@ export function WrapProcessModal({ return (
-

Wrapping {formatBalance(amount, 18)} MORPHO

+ @@ -65,7 +70,7 @@ export function WrapProcessModal({ exit={{ opacity: 0, y: -20 }} transition={{ delay: index * 0.1 }} className={`flex items-start gap-4 rounded-lg p-4 ${ - isActive ? 'bg-gray-50' : '' + isActive ? 'bg-gray-50 dark:bg-gray-800' : '' }`} >
From e44526178948712eb22204e020dafc47f129de42 Mon Sep 17 00:00:00 2001 From: antoncoding Date: Mon, 20 Jan 2025 20:45:52 +0800 Subject: [PATCH 17/17] chore: review feedback --- app/rewards/components/RewardContent.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/app/rewards/components/RewardContent.tsx b/app/rewards/components/RewardContent.tsx index fac17992..67e06451 100644 --- a/app/rewards/components/RewardContent.tsx +++ b/app/rewards/components/RewardContent.tsx @@ -48,8 +48,11 @@ export default function Rewards() { }); const morphoBalance = useMemo( - () => (morphoBalanceMainnet?.value ?? 0n) + (morphoBalanceBase?.value ?? 0n), - [morphoBalanceMainnet, morphoBalanceBase], + () => + (morphoBalanceMainnet?.value ?? 0n) + + (morphoBalanceBase?.value ?? 0n) + + (morphoBalanceLegacy?.value ?? 0n), + [morphoBalanceMainnet, morphoBalanceBase, morphoBalanceLegacy], ); const allRewards = useMemo(() => { @@ -132,11 +135,10 @@ export default function Rewards() { const canClaim = useMemo(() => totalClaimable > 0n, [totalClaimable]); - const showLegacy = true; - // useMemo( - // () => morphoBalanceLegacy && morphoBalanceLegacy.value !== 0n, - // [morphoBalanceLegacy], - // ); + const showLegacy = useMemo( + () => morphoBalanceLegacy && morphoBalanceLegacy.value !== 0n, + [morphoBalanceLegacy], + ); const { wrap, currentStep, showProcessModal, setShowProcessModal } = useWrapLegacyMorpho( morphoBalanceLegacy?.value ?? 0n, @@ -269,7 +271,7 @@ export default function Rewards() { size="sm" isSelected={showClaimed} onValueChange={setShowClaimed} - aria-label="Show pending market rewards" + aria-label="Show claimed rewards" />