diff --git a/app/markets/components/constants.ts b/app/markets/components/constants.ts index 5db27b77..67ae4cac 100644 --- a/app/markets/components/constants.ts +++ b/app/markets/components/constants.ts @@ -3,7 +3,6 @@ export enum SortColumn { LoanAsset = 1, CollateralAsset = 2, LLTV = 3, - Reward = 4, Supply = 5, Borrow = 6, SupplyAPY = 7, diff --git a/app/markets/components/utils.ts b/app/markets/components/utils.ts index c542c019..3c9860ef 100644 --- a/app/markets/components/utils.ts +++ b/app/markets/components/utils.ts @@ -11,7 +11,6 @@ export const sortProperties = { [SortColumn.LoanAsset]: 'loanAsset.name', [SortColumn.CollateralAsset]: 'collateralAsset.name', [SortColumn.LLTV]: 'lltv', - [SortColumn.Reward]: (item: Market) => Number(item.rewardPer1000USD ?? '0'), [SortColumn.Supply]: 'state.supplyAssetsUsd', [SortColumn.Borrow]: 'state.borrowAssetsUsd', [SortColumn.SupplyAPY]: 'state.supplyApy', diff --git a/app/positions/components/PositionsContent.tsx b/app/positions/components/PositionsContent.tsx index 14919508..1a4bc1f1 100644 --- a/app/positions/components/PositionsContent.tsx +++ b/app/positions/components/PositionsContent.tsx @@ -3,17 +3,16 @@ import { useMemo, useState } from 'react'; import Link from 'next/link'; import { useParams } from 'next/navigation'; -import { FaHistory, FaPlus, FaCircle } from 'react-icons/fa'; +import { FaHistory, FaPlus } from 'react-icons/fa'; import { IoRefreshOutline } from 'react-icons/io5'; import { RiRobot2Line } from 'react-icons/ri'; import { TbReport } from 'react-icons/tb'; import { toast } from 'react-toastify'; import { Address } from 'viem'; import { useAccount } from 'wagmi'; -import { Avatar } from '@/components/Avatar/Avatar'; +import { AddressDisplay } from '@/components/common/AddressDisplay'; import { Badge } from '@/components/common/Badge'; import { Button } from '@/components/common/Button'; -import { Name } from '@/components/common/Name'; import Header from '@/components/layout/header/Header'; import EmptyScreen from '@/components/Status/EmptyScreen'; import LoadingScreen from '@/components/Status/LoadingScreen'; @@ -35,7 +34,7 @@ export default function Positions() { const [selectedPosition, setSelectedPosition] = useState(null); const { account } = useParams<{ account: string }>(); - const { address, isConnected } = useAccount(); + const { address } = useAccount(); const { rebalancerInfo, refetch: refetchRebalancerInfo } = useUserRebalancerInfo(address); const isOwner = useMemo(() => { @@ -71,28 +70,7 @@ export default function Positions() {

Portfolio

-
-
- - {isConnected && account === address && ( -
-
- -
-
- )} -
-
- -
-
+
+ ); + + 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, + tooltip, + badge, + button, + className = '', +}: InfoCardProps) { + return ( +
+
+
+ {title} + {tooltip && ( + + } + placement="right" + > +
+ +
+
+ )} +
+
+ {button ? ( + + ) : badge ? ( + + ) : null} +
+
+
{children}
+
+ ); +} diff --git a/app/rewards/components/MarketProgram.tsx b/app/rewards/components/MarketProgram.tsx deleted file mode 100644 index 09aa714e..00000000 --- a/app/rewards/components/MarketProgram.tsx +++ /dev/null @@ -1,337 +0,0 @@ -'use client'; - -import { useMemo, useState } from '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'; -import { Address } from 'viem'; -import { useAccount, useSwitchChain } from 'wagmi'; -import { Button } from '@/components/common/Button'; -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'; - -type MarketProgramProps = { - account: string; - markets: Market[]; - marketRewards: MarketProgramType[]; - distributions: DistributionResponseType[]; -}; - -export default function MarketProgram({ - marketRewards, - markets, - distributions, - account, -}: MarketProgramProps) { - const { chainId } = useAccount(); - const { switchChain } = useSwitchChain(); - const [selectedToken, setSelectedToken] = useState(null); - - const { sendTransaction } = useTransactionWithToast({ - toastId: 'claim', - pendingText: 'Claiming Reward...', - successText: 'Reward Claimed!', - errorText: 'Failed to claim rewards', - chainId, - pendingDescription: `Claiming rewards`, - successDescription: `Successfully claimed 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; - - 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 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. -

- -
- - - Asset - Chain - Claimable - Pending - Claimed - Total - Action - - - {allRewardTokens - .filter((tokenReward) => tokenReward !== null && tokenReward !== undefined) - .map((tokenReward, index) => { - const matchedToken = findToken(tokenReward.token, tokenReward.chainId) ?? { - symbol: 'Unknown', - img: undefined, - decimals: 18, - }; - const distribution = distributions.find( - (d) => d.asset.address.toLowerCase() === tokenReward.token.toLowerCase(), - ); - - return ( - handleRowClick(tokenReward.token)} - > - -
-

{matchedToken.symbol}

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

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

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

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

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

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

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

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

- {matchedToken.img && ( - - )} -
-
- -
- -
-
-
- ); - })} -
-
-
- - {selectedToken && ( -
-

- {' '} - Reward Breakdown for{' '} - { - findToken( - selectedToken, - allRewardTokens.find((t) => t.token === selectedToken)?.chainId ?? 1, - )?.symbol - } -

- - - Market ID - Loan Asset - Collateral - LLTV - Claimable - Pending - Claimed - Total - - - {markets - .filter((m) => - marketRewards.find( - (r) => - r.program && - r.program.market_id.toLowerCase() === m.uniqueKey.toLowerCase() && - r.program.asset.address.toLowerCase() === selectedToken.toLowerCase(), - ), - ) - .map((market, idx) => { - const tokenRewardsForMarket = marketRewards.filter((reward) => { - if (!reward.program) return false; - return ( - reward.program.market_id === market.uniqueKey && - reward.program.asset.address.toLowerCase() === selectedToken.toLowerCase() - ); - }); - - const claimable = tokenRewardsForMarket.reduce((a: bigint, b) => { - return a + BigInt(b.for_supply?.claimable_now ?? '0'); - }, BigInt(0)); - const pending = tokenRewardsForMarket.reduce((a: bigint, b) => { - return a + BigInt(b.for_supply?.claimable_next ?? '0'); - }, BigInt(0)); - - const total = tokenRewardsForMarket.reduce((a: bigint, b) => { - return a + BigInt(b.for_supply?.total ?? '0'); - }, BigInt(0)); - - const claimed = tokenRewardsForMarket.reduce((a: bigint, b) => { - return a + BigInt(b.for_supply?.claimed ?? '0'); - }, BigInt(0)); - - const matchedToken = findToken(selectedToken, market.morphoBlue.chain.id); - - return ( - - - - {market.uniqueKey.slice(2, 8)} - - - {market.loanAsset.symbol} - {market.collateralAsset.symbol} - {formatBalance(market.lltv, 16)}% - - {formatReadable(formatBalance(claimable, matchedToken?.decimals ?? 18))} - - - {formatReadable(formatBalance(pending, matchedToken?.decimals ?? 18))} - - - {formatReadable(formatBalance(claimed, matchedToken?.decimals ?? 18))} - - - {formatReadable(formatBalance(total, matchedToken?.decimals ?? 18))} - - - ); - })} - -
-
- )} -
- ); -} diff --git a/app/rewards/components/RewardContent.tsx b/app/rewards/components/RewardContent.tsx index 80639e95..67e06451 100644 --- a/app/rewards/components/RewardContent.tsx +++ b/app/rewards/components/RewardContent.tsx @@ -1,82 +1,302 @@ 'use client'; import { useMemo, useState } from 'react'; +import { Switch, Tooltip } from '@nextui-org/react'; import { useParams } from 'next/navigation'; -import ButtonGroup from '@/components/ButtonGroup'; +import { BsQuestionCircle } from 'react-icons/bs'; +import { Address } from 'viem'; +import { useBalance } from 'wagmi'; +import { AddressDisplay } from '@/components/common/AddressDisplay'; 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 { TokenIcon } from '@/components/TokenIcon'; +import { TooltipContent } from '@/components/TooltipContent'; +import { WrapProcessModal } from '@/components/WrapProcessModal'; import useUserRewards from '@/hooks/useRewards'; -import { filterMarketRewards, filterUniformRewards } from '@/utils/rewardHelpers'; -import MarketProgram from './MarketProgram'; -import UniformProgram from './UniformProgram'; // You'll need to create this component -const programOptions = [ - { key: 'market', label: 'Market Program', value: 'market' }, - { key: 'uniform', label: 'Uniform Program', value: 'uniform' }, -]; +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'; 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 ( - - ); - } - }; + const { rewards, distributions, loading: loadingRewards, refresh } = useUserRewards(account); + + const [showClaimed, setShowClaimed] = useState(false); + + const { data: morphoBalanceMainnet } = useBalance({ + token: MORPHO_TOKEN_MAINNET, + address: account as Address, + chainId: SupportedNetworks.Mainnet, + }); + + const { data: morphoBalanceBase } = useBalance({ + token: MORPHO_TOKEN_BASE, + address: account as Address, + chainId: SupportedNetworks.Base, + }); + + const { data: morphoBalanceLegacy } = useBalance({ + token: MORPHO_LEGACY, + address: account as Address, + chainId: SupportedNetworks.Mainnet, + }); + + const morphoBalance = useMemo( + () => + (morphoBalanceMainnet?.value ?? 0n) + + (morphoBalanceBase?.value ?? 0n) + + (morphoBalanceLegacy?.value ?? 0n), + [morphoBalanceMainnet, morphoBalanceBase, morphoBalanceLegacy], + ); + + const allRewards = useMemo(() => { + // Group rewards by token address and chain + const groupedRewards = rewards.reduce( + (acc, reward) => { + const key = `${reward.asset.address}-${reward.asset.chain_id}`; + if (!acc[key]) { + acc[key] = { + asset: reward.asset, + total: { + claimable: 0n, + pendingAmount: 0n, + claimed: 0n, + }, + programs: [], + }; + } + if (reward.type === 'uniform-reward') { + acc[key].total.claimable += BigInt(reward.amount.claimable_now); + acc[key].total.pendingAmount += BigInt(reward.amount.claimable_next); + acc[key].total.claimed += BigInt(reward.amount.claimed); + acc[key].programs.push(reward.type); + } else if (reward.type === 'market-reward' || reward.type === 'vault-reward') { + // go through all posible keys of rewad object: for_supply, for_borrow, for_collateral} + + if (reward.for_supply) { + acc[key].total.claimable += BigInt(reward.for_supply.claimable_now); + acc[key].total.pendingAmount += BigInt(reward.for_supply.claimable_next); + acc[key].total.claimed += BigInt(reward.for_supply.claimed); + acc[key].programs.push(reward.type); + } + + if ((reward as MarketRewardType).for_borrow) { + acc[key].total.claimable += BigInt( + ((reward as MarketRewardType).for_borrow as RewardAmount).claimable_now, + ); + acc[key].total.pendingAmount += BigInt( + ((reward as MarketRewardType).for_borrow as RewardAmount).claimable_next, + ); + acc[key].total.claimed += BigInt( + ((reward as MarketRewardType).for_borrow as RewardAmount).claimed, + ); + acc[key].programs.push(reward.type); + } + + if ((reward as MarketRewardType).for_collateral) { + acc[key].total.claimable += BigInt( + ((reward as MarketRewardType).for_collateral as RewardAmount).claimable_now, + ); + acc[key].total.pendingAmount += BigInt( + ((reward as MarketRewardType).for_collateral as RewardAmount).claimable_next, + ); + acc[key].total.claimed += BigInt( + ((reward as MarketRewardType).for_collateral as RewardAmount).claimed, + ); + acc[key].programs.push(reward.type); + } + } + return acc; + }, + {} as Record, + ); + + return Object.values(groupedRewards); + }, [rewards]); + + const totalClaimable = useMemo(() => { + return allRewards.reduce((acc, reward) => { + if ( + reward.asset.address.toLowerCase() === MORPHO_TOKEN_MAINNET.toLowerCase() || + reward.asset.address.toLowerCase() === MORPHO_TOKEN_BASE.toLowerCase() || + reward.asset.address.toLowerCase() === MORPHO_LEGACY.toLowerCase() + ) { + return acc + reward.total.claimable; + } + return acc; + }, 0n); + }, [allRewards]); + + 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 + void refresh(); + }, + ); return (
+

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 Token

+ + } + placement="right" + > +
+ +
+
+
+
+ {/* morpho token blocks */} +
+ +
+ + {formatSimple(formatBalance(morphoBalance, 18))} + + +
+
- {loading || loadingRewards ? ( - - ) : rewards.length === 0 ? ( - - ) : ( - renderActiveProgram() - )} + +
+ {formatSimple(formatBalance(totalClaimable, 18))} + +
+
+ + {showLegacy && ( + { + void wrap(); + }, + disabled: showProcessModal, + }} + > +
+ {morphoBalanceLegacy && ( + {formatSimple(formatBalance(morphoBalanceLegacy?.value, 18))} + )} + +
+
+ )} +
+
+
+
+
+

All Rewards

+
+
+ Show Claimed + +
+
+ {loadingRewards ? ( + + ) : rewards.length === 0 ? ( + + ) : ( + + )} +
+
+ {showProcessModal && ( + setShowProcessModal(false)} + /> + )}
); } diff --git a/app/rewards/components/RewardTable.tsx b/app/rewards/components/RewardTable.tsx new file mode 100644 index 00000000..9d089acb --- /dev/null +++ b/app/rewards/components/RewardTable.tsx @@ -0,0 +1,232 @@ +'use client'; + +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'; +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 { formatBalance, formatSimple } from '@/utils/balance'; +import { getAssetURL } from '@/utils/external'; +import { getNetworkImg } from '@/utils/networks'; +import { findToken } from '@/utils/tokens'; +import { AggregatedRewardType } from '@/utils/types'; + +type RewardTableProps = { + account: string; + rewards: AggregatedRewardType[]; + distributions: DistributionResponseType[]; + showClaimed: boolean; +}; + +export default function RewardTable({ + rewards, + distributions, + account, + showClaimed, +}: RewardTableProps) { + const { chainId } = useAccount(); + const { switchChain } = useSwitchChain(); + + const { sendTransaction } = useTransactionWithToast({ + toastId: 'claim', + pendingText: 'Claiming Reward...', + successText: 'Reward Claimed!', + errorText: 'Failed to claim rewards', + chainId, + pendingDescription: `Claiming rewards`, + successDescription: `Successfully claimed rewards`, + }); + + const filteredRewardTokens = useMemo( + () => + 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 ( +
+
+ + + Asset + Chain + Claimable + Pending + Claimed + Total + Action + + + {filteredRewardTokens + .filter((tokenReward) => tokenReward !== null && tokenReward !== undefined) + .map((tokenReward, index) => { + const matchedToken = findToken( + tokenReward.asset.address, + tokenReward.asset.chain_id, + ) ?? { + symbol: 'Unknown', + img: undefined, + decimals: 18, + }; + + const total = + tokenReward.total.claimable + + tokenReward.total.pendingAmount + + tokenReward.total.claimed; + + const distribution = distributions.find( + (d) => + d.asset.address.toLowerCase() === tokenReward.asset.address.toLowerCase() && + d.asset.chain_id === tokenReward.asset.chain_id, + ); + + return ( + + + e.stopPropagation()} + > +

{matchedToken.symbol}

+ + +
+ +
+ +
+
+ +
+

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

+ +
+
+ +
+

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

+ +
+
+ +
+

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

+ +
+
+ +
+

{formatSimple(formatBalance(total, matchedToken.decimals))}

+ +
+
+ +
+ +
+
+
+ ); + })} +
+
+
+
+ ); +} diff --git a/app/rewards/components/UniformProgram.tsx b/app/rewards/components/UniformProgram.tsx deleted file mode 100644 index 3797bf76..00000000 --- a/app/rewards/components/UniformProgram.tsx +++ /dev/null @@ -1,199 +0,0 @@ -'use client'; - -import { useMemo } from 'react'; -import { Table, TableHeader, TableBody, TableColumn, TableRow, TableCell } from '@nextui-org/table'; -import Image from 'next/image'; -import { toast } from 'react-toastify'; -import { Address } from 'viem'; -import { useAccount, useSwitchChain } from 'wagmi'; -import { Button } from '@/components/common/Button'; -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 { UniformRewardType } from '@/utils/types'; - -type UniformProgramProps = { - account: string; - uniformRewards: UniformRewardType[]; - distributions: DistributionResponseType[]; -}; - -export default function UniformProgram({ - account, - uniformRewards, - distributions, -}: UniformProgramProps) { - const { chainId } = useAccount(); - const { switchChain } = useSwitchChain(); - - const { sendTransaction } = useTransactionWithToast({ - toastId: 'claim-uniform', - pendingText: 'Claiming Uniform Reward...', - successText: 'Uniform Reward Claimed!', - errorText: 'Failed to claim uniform rewards', - chainId, - pendingDescription: `Claiming uniform rewards`, - successDescription: `Successfully claimed uniform rewards`, - }); - - const rewardsData = useMemo( - () => - uniformRewards.map((reward) => { - const token = findToken(reward.asset.address, reward.asset.chain_id); - const distribution = distributions.find( - (d) => d.asset.address.toLowerCase() === reward.asset.address.toLowerCase(), - ); - return { - ...reward, - token, - distribution, - claimable: BigInt(reward.amount.claimable_now ?? '0'), - pending: BigInt(reward.amount.claimable_next ?? '0'), - total: BigInt(reward.amount.total ?? '0'), - claimed: BigInt(reward.amount.claimed ?? '0'), - }; - }), - [uniformRewards, distributions], - ); - - 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 - - . -

- -
- - - Asset - Chain - Claimable - Pending - Claimed - Total - Action - - - {rewardsData.map((reward, index) => ( - - -
-

{reward.token?.symbol}

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

- {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/abis/morpho-wrapper.ts b/src/abis/morpho-wrapper.ts new file mode 100644 index 00000000..53e2aa4b --- /dev/null +++ b/src/abis/morpho-wrapper.ts @@ -0,0 +1,50 @@ +export default [ + { + inputs: [{ internalType: 'address', name: 'morphoToken', type: 'address' }], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { inputs: [], name: 'SelfAddress', type: 'error' }, + { inputs: [], name: 'ZeroAddress', type: 'error' }, + { + inputs: [], + name: 'LEGACY_MORPHO', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'NEW_MORPHO', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'account', type: 'address' }, + { internalType: 'uint256', name: 'value', type: 'uint256' }, + ], + name: 'depositFor', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'underlying', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'pure', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'account', type: 'address' }, + { internalType: 'uint256', name: 'value', type: 'uint256' }, + ], + name: 'withdrawTo', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, +] as const; 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 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 ( +
+ +
+ + +
+ +
+ + {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/components/common/AddressDisplay.tsx b/src/components/common/AddressDisplay.tsx new file mode 100644 index 00000000..8bda20db --- /dev/null +++ b/src/components/common/AddressDisplay.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { useMemo } from 'react'; +import { FaCircle } from 'react-icons/fa'; +import { Address } from 'viem'; +import { useAccount } from 'wagmi'; +import { Avatar } from '@/components/Avatar/Avatar'; +import { Name } from '@/components/common/Name'; + +type AddressDisplayProps = { + address: Address; +}; + +export function AddressDisplay({ address }: AddressDisplayProps) { + const { address: connectedAddress, isConnected } = useAccount(); + + const isOwner = useMemo(() => { + return address === connectedAddress; + }, [address, connectedAddress]); + + return ( +
+
+ + {isOwner && isConnected && ( +
+
+ +
+
+ )} +
+
+ +
+
+ ); +} diff --git a/src/contexts/MarketsContext.tsx b/src/contexts/MarketsContext.tsx index 89864672..fcc2ba30 100644 --- a/src/contexts/MarketsContext.tsx +++ b/src/contexts/MarketsContext.tsx @@ -11,9 +11,7 @@ import { } from 'react'; import { marketsQuery } from '@/graphql/queries'; import useLiquidations from '@/hooks/useLiquidations'; -import { getRewardPer1000USD } from '@/utils/morpho'; import { isSupportedChain } from '@/utils/networks'; -import { MORPHOTokenAddress } from '@/utils/tokens'; import { Market } from '@/utils/types'; import { getMarketWarningsWithDetail } from '@/utils/warnings'; @@ -82,28 +80,11 @@ export function MarketsProvider({ children }: MarketsProviderProps) { .filter((market) => isSupportedChain(market.morphoBlue.chain.id)); const processedMarkets = filtered.map((market) => { - const entry = market.state.rewards.find( - (reward) => reward.asset.address.toLowerCase() === MORPHOTokenAddress.toLowerCase(), - ); - const warningsWithDetail = getMarketWarningsWithDetail(market); const isProtectedByLiquidationBots = liquidatedMarketIds.has(market.id); - if (!entry) { - return { - ...market, - rewardPer1000USD: undefined, - warningsWithDetail, - isProtectedByLiquidationBots, - }; - } - - const supplyAssetUSD = Number(market.state.supplyAssetsUsd); - const rewardPer1000USD = getRewardPer1000USD(entry.yearlySupplyTokens, supplyAssetUSD); - return { ...market, - rewardPer1000USD, warningsWithDetail, isProtectedByLiquidationBots, }; diff --git a/src/hooks/useERC20Approval.ts b/src/hooks/useERC20Approval.ts index bf08b02a..0a6455b7 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/useMarket.ts b/src/hooks/useMarket.ts index 1a779b70..1c4d6a8e 100644 --- a/src/hooks/useMarket.ts +++ b/src/hooks/useMarket.ts @@ -1,7 +1,5 @@ import { useQuery } from '@tanstack/react-query'; -import { getRewardPer1000USD } from '@/utils/morpho'; import { SupportedNetworks } from '@/utils/networks'; -import { MORPHOTokenAddress } from '@/utils/tokens'; import { URLS } from '@/utils/urls'; import { getMarketWarningsWithDetail } from '@/utils/warnings'; import { marketDetailQuery, marketHistoricalDataQuery } from '../graphql/queries'; @@ -38,21 +36,10 @@ const graphqlFetcher = async ( }; const processMarketData = (market: Market): MarketDetail => { - const entry = market.state.rewards.find( - (reward) => reward.asset.address.toLowerCase() === MORPHOTokenAddress?.toLowerCase(), - ); - const warningsWithDetail = getMarketWarningsWithDetail(market); - let rewardPer1000USD: string | undefined; - if (entry) { - const supplyAssetUSD = Number(market.state.supplyAssetsUsd); - rewardPer1000USD = getRewardPer1000USD(entry.yearlySupplyTokens, supplyAssetUSD); - } - return { ...market, - rewardPer1000USD, warningsWithDetail, isProtectedByLiquidationBots: false, // NOT needed for now, might implement later historicalState: { diff --git a/src/hooks/useRewards.ts b/src/hooks/useRewards.ts index b6fd103b..d150fecc 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(() => { + void 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..5e43fefa --- /dev/null +++ b/src/hooks/useWrapLegacyMorpho.ts @@ -0,0 +1,81 @@ +import { useCallback, useState } from 'react'; +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'; + +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(); + } + + 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/imgs/tokens/ionic.png b/src/imgs/tokens/ionic.png new file mode 100644 index 00000000..7c506135 Binary files /dev/null and b/src/imgs/tokens/ionic.png differ diff --git a/src/imgs/tokens/well.png b/src/imgs/tokens/well.png new file mode 100644 index 00000000..1d89ff8c Binary files /dev/null and b/src/imgs/tokens/well.png differ diff --git a/src/utils/balance.ts b/src/utils/balance.ts index 04a00cb3..781ec3d1 100644 --- a/src/utils/balance.ts +++ b/src/utils/balance.ts @@ -45,6 +45,13 @@ export function formatReadable(num: number | string, precision = 2): string { } } +export function formatSimple(num: number) { + return new Intl.NumberFormat('en-us', { + minimumFractionDigits: 2, + maximumFractionDigits: 4, + }).format(num); +} + export function min(a: bigint, b: bigint): bigint { return a < b ? a : b; } diff --git a/src/utils/ipfs.ts b/src/utils/ipfs.ts deleted file mode 100644 index d97b5f34..00000000 --- a/src/utils/ipfs.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Convert IPFS URI to HTTPS URI. - * - * @param ipfsURI An ipfs protocol URI. - * @param gateway The IPFS gateway to use. Defaults to ipfs.io, a free public gateway. - * For production use, you'll likely want a paid provider. - * @returns An HTTPS URI that points to the data represented by the cid - * embedded in the ipfs URI. - */ -export const ipfsToHTTP = function (ipfsURI: string, gateway = 'ipfs.io') { - if (ipfsURI.startsWith('http')) { - return ipfsURI.replace('http://', 'https://'); - } - // IPNS Name is a Multihash of a serialized PublicKey. - const cid = ipfsURI.replace('ipfs://', ''); - - // Addresses using a gateway use the following form, - // where is the gateway address, - // and is the content identifier. - return `https://${gateway}/ipfs/${cid}`; -}; diff --git a/src/utils/rewardHelpers.ts b/src/utils/rewardHelpers.ts deleted file mode 100644 index 57b0eeab..00000000 --- a/src/utils/rewardHelpers.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { RewardResponseType, MarketProgramType, UniformRewardType } from '@/utils/types'; - -export function isMarketReward(reward: RewardResponseType): reward is MarketProgramType { - return reward.type === 'market-reward'; -} - -export function isUniformReward(reward: RewardResponseType): reward is UniformRewardType { - return reward.type === 'uniform-reward'; -} - -export function filterMarketRewards(rewards: RewardResponseType[]): MarketProgramType[] { - return rewards.filter(isMarketReward); -} - -export function filterUniformRewards(rewards: RewardResponseType[]): UniformRewardType[] { - return rewards.filter(isUniformReward); -} diff --git a/src/utils/test/address.test.ts b/src/utils/test/address.test.ts deleted file mode 100644 index d07a802c..00000000 --- a/src/utils/test/address.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { getSlicedAddress } from '../address'; - -describe('getSlicedAddress', () => { - it('should return a string of class names', () => { - const address = getSlicedAddress('0x1234567890123456789012345678901234567890'); - expect(address).toEqual('0x123...7890'); - }); -}); diff --git a/src/utils/test/analytics.test.ts b/src/utils/test/analytics.test.ts deleted file mode 100644 index 3dc65b34..00000000 --- a/src/utils/test/analytics.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { initAnalytics, markStep } from '../analytics'; - -describe('analytics', () => { - // TODO Add real tests - it('should initAnalytics be defined', () => { - expect(initAnalytics).toBeDefined(); - }); - - // TODO Add real tests - it('should markStep be defined', () => { - expect(markStep).toBeDefined(); - }); -}); diff --git a/src/utils/test/ipfsToHTTP.test.ts b/src/utils/test/ipfsToHTTP.test.ts deleted file mode 100644 index 57ee6610..00000000 --- a/src/utils/test/ipfsToHTTP.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ipfsToHTTP } from '../ipfs'; - -describe('ipfsToHTTP', () => { - it('converts ipfs URI to HTTPS URI', () => { - const ipfsURI = 'ipfs://QmY5V6JZ5Yf7Z6p8n7Y1Z5dJLXhZU7Z7Q3mH2nX8vqfHc5'; - const expected = 'https://ipfs.io/ipfs/QmY5V6JZ5Yf7Z6p8n7Y1Z5dJLXhZU7Z7Q3mH2nX8vqfHc5'; - expect(ipfsToHTTP(ipfsURI)).toEqual(expected); - }); -}); diff --git a/src/utils/test/timestamp.test.ts b/src/utils/test/timestamp.test.ts deleted file mode 100644 index 1e93ee9e..00000000 --- a/src/utils/test/timestamp.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { convertBigIntTimestampToDate } from '../timestamp'; - -describe('convertBigIntTimestampToDate', () => { - it('should convert a big int timestamp to a date', () => { - const timestamp = 1701704606n; - const bigIntDate = convertBigIntTimestampToDate(timestamp); - expect(bigIntDate.toISOString()).toEqual('2023-12-04T15:43:26.000Z'); - }); -}); diff --git a/src/utils/timestamp.ts b/src/utils/timestamp.ts deleted file mode 100644 index 41190cc1..00000000 --- a/src/utils/timestamp.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * This function converts a timestamp in bigint to a Date object. - * @param timestamp The timestamp to convert to a Date object. - * @returns The Date object. - */ -export function convertBigIntTimestampToDate(timestamp: bigint): Date { - return new Date(Number(timestamp) * 1000); -} diff --git a/src/utils/tokens.ts b/src/utils/tokens.ts index 465eaaab..f26b8758 100644 --- a/src/utils/tokens.ts +++ b/src/utils/tokens.ts @@ -19,24 +19,14 @@ export type UnknownERC20Token = { isUnknown?: boolean; }; -const MORPHOTokenAddress = '0x9994E35Db50125E0DF82e4c2dde62496CE330999'; +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: 'MORPHO', - img: require('../imgs/tokens/morpho.svg') as string, - decimals: 18, - networks: [ - { - address: '0x9994E35Db50125E0DF82e4c2dde62496CE330999', - chain: mainnet, - }, - { - address: '0x9994E35Db50125E0DF82e4c2dde62496CE330999', - chain: base, - }, - ], - }, { symbol: 'USDC', img: require('../imgs/tokens/usdc.webp') as string, @@ -420,6 +410,51 @@ 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: 'PYTH', + img: require('../imgs/oracles/pyth.png') as string, + decimals: 18, + networks: [{ chain: base, address: '0x4c5d8A75F3762c1561D96f177694f67378705E98' }], + }, + { + symbol: 'MORPHO', + img: require('../imgs/tokens/morpho.svg') as string, + decimals: 18, + networks: [ + { + address: MORPHO_TOKEN_MAINNET, + chain: mainnet, + }, + { + address: MORPHO_TOKEN_BASE, + chain: base, + }, + ], + }, + { + symbol: 'MORPHO*', + img: require('../imgs/tokens/morpho.svg') as string, + decimals: 18, + networks: [ + { + address: MORPHO_LEGACY, + chain: mainnet, + }, + ], + }, ]; const isWhitelisted = (address: string, chainId: number) => { @@ -469,9 +504,12 @@ const getUniqueTokens = (tokenList: { address: string; chainId: number }[]) => { export { supportedTokens, isWhitelisted, - MORPHOTokenAddress, findTokenWithKey, findToken, getUniqueTokens, infoToKey, + MORPHO_TOKEN_BASE, + MORPHO_TOKEN_MAINNET, + MORPHO_LEGACY, + MORPHO_TOKEN_WRAPPER, }; diff --git a/src/utils/types.ts b/src/utils/types.ts index 441c2f6f..8748759d 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -106,7 +106,7 @@ type AssetType = { chain_id: number; }; -type RewardAmount = { +export type RewardAmount = { total: string; claimable_now: string; claimable_next: string; @@ -114,9 +114,12 @@ type RewardAmount = { }; // Market Program Type -export type MarketProgramType = { +export type MarketRewardType = { + // shared type: 'market-reward'; asset: AssetType; + user: string; + // specific for_borrow: RewardAmount | null; for_collateral: RewardAmount | null; for_supply: RewardAmount | null; @@ -129,20 +132,56 @@ export type MarketProgramType = { market_id: string; asset: AssetType; }; - user: string; }; // Uniform Reward Type export type UniformRewardType = { + // shared type: 'uniform-reward'; - amount: RewardAmount; asset: AssetType; + user: string; + // specific + amount: RewardAmount; program_id: string; +}; + +export type VaultRewardType = { + // shared + type: 'vault-reward'; + asset: AssetType; user: string; + // specific + program: VaultProgramType; + for_supply: RewardAmount | null; +}; + +export type VaultProgramType = { + type: 'vault-reward'; + asset: AssetType; + vault: string; + chain_id: number; + rate_per_year: string; + distributor: AssetType; + creator: string; + blacklist: string[]; + start: string; + end: string; + created_at: string; + id: string; }; // Combined RewardResponseType -export type RewardResponseType = MarketProgramType | UniformRewardType; +export type RewardResponseType = MarketRewardType | UniformRewardType | VaultRewardType; + +export type AggregatedRewardType = { + asset: AssetType; + total: { + claimable: bigint; + pendingAmount: bigint; + claimed: bigint; + }; + programs: ('vault-reward' | 'market-reward' | 'uniform-reward')[]; +}; export type RebalanceAction = { fromMarket: { @@ -286,7 +325,6 @@ export type Market = { }; // appended by us - rewardPer1000USD?: string; warningsWithDetail: WarningWithDetail[]; isProtectedByLiquidationBots: boolean; oracle: {