From 02d44094ef1ac491adc1b0328d33829cf7771149 Mon Sep 17 00:00:00 2001 From: Majorfi Date: Mon, 23 Oct 2023 14:56:59 +0200 Subject: [PATCH 001/111] feat: wip v3 --- apps/common/components/AppHeader.tsx | 21 +- apps/common/components/Apps.tsx | 15 + apps/common/hooks/useCurrentApp.tsx | 6 +- apps/common/schemas/yDaemonVaultsSchemas.ts | 22 - apps/vaults-v3/Mark.tsx | 118 +++++ apps/vaults-v3/Wrapper.tsx | 36 ++ .../vaults-v3/components/ImageWithOverlay.tsx | 53 ++ apps/vaults-v3/components/ListHero.tsx | 120 +++++ apps/vaults-v3/components/RewardsTab.tsx | 201 ++++++++ apps/vaults-v3/components/SettingsPopover.tsx | 196 ++++++++ .../details/VaultActionsTabsWrapper.tsx | 340 +++++++++++++ .../components/details/VaultDetailsHeader.tsx | 173 +++++++ .../details/actions/QuickActionsButtons.tsx | 251 ++++++++++ .../details/actions/QuickActionsFrom.tsx | 142 ++++++ .../details/actions/QuickActionsSwitch.tsx | 17 + .../details/actions/QuickActionsTo.tsx | 121 +++++ .../details/tabs/VaultDetailsAbout.tsx | 195 ++++++++ .../details/tabs/VaultDetailsHistorical.tsx | 111 +++++ .../details/tabs/VaultDetailsStrategies.tsx | 263 ++++++++++ .../details/tabs/VaultDetailsTabsWrapper.tsx | 261 ++++++++++ .../details/tabs/findLatestApr.test.ts | 55 +++ .../components/details/tabs/findLatestApr.ts | 19 + .../graphs/GraphForStrategyReports.tsx | 148 ++++++ .../graphs/GraphForVaultEarnings.tsx | 112 +++++ .../graphs/GraphForVaultPPSGrowth.tsx | 90 ++++ .../components/graphs/GraphForVaultTVL.tsx | 91 ++++ .../components/list/VaultListFactory.tsx | 196 ++++++++ .../components/list/VaultListOptions.tsx | 62 +++ .../components/list/VaultsListEmpty.tsx | 161 +++++++ .../list/VaultsListInternalMigrationRow.tsx | 63 +++ .../components/list/VaultsListRetired.tsx | 64 +++ .../components/list/VaultsListRow.tsx | 454 ++++++++++++++++++ apps/vaults-v3/constants/index.ts | 15 + apps/vaults-v3/constants/menu.ts | 4 + apps/vaults-v3/constants/optRewards.ts | 233 +++++++++ pages/_app.tsx | 92 +++- pages/index.tsx | 12 + pages/vaults-v3/[chainID]/[address].tsx | 130 +++++ pages/vaults-v3/about.tsx | 149 ++++++ pages/vaults-v3/index.tsx | 346 +++++++++++++ style.css | 26 + tailwind.config.js | 7 +- tsconfig.json | 1 + 43 files changed, 5132 insertions(+), 60 deletions(-) create mode 100644 apps/vaults-v3/Mark.tsx create mode 100755 apps/vaults-v3/Wrapper.tsx create mode 100644 apps/vaults-v3/components/ImageWithOverlay.tsx create mode 100644 apps/vaults-v3/components/ListHero.tsx create mode 100644 apps/vaults-v3/components/RewardsTab.tsx create mode 100755 apps/vaults-v3/components/SettingsPopover.tsx create mode 100755 apps/vaults-v3/components/details/VaultActionsTabsWrapper.tsx create mode 100755 apps/vaults-v3/components/details/VaultDetailsHeader.tsx create mode 100644 apps/vaults-v3/components/details/actions/QuickActionsButtons.tsx create mode 100644 apps/vaults-v3/components/details/actions/QuickActionsFrom.tsx create mode 100644 apps/vaults-v3/components/details/actions/QuickActionsSwitch.tsx create mode 100644 apps/vaults-v3/components/details/actions/QuickActionsTo.tsx create mode 100755 apps/vaults-v3/components/details/tabs/VaultDetailsAbout.tsx create mode 100755 apps/vaults-v3/components/details/tabs/VaultDetailsHistorical.tsx create mode 100755 apps/vaults-v3/components/details/tabs/VaultDetailsStrategies.tsx create mode 100755 apps/vaults-v3/components/details/tabs/VaultDetailsTabsWrapper.tsx create mode 100644 apps/vaults-v3/components/details/tabs/findLatestApr.test.ts create mode 100644 apps/vaults-v3/components/details/tabs/findLatestApr.ts create mode 100755 apps/vaults-v3/components/graphs/GraphForStrategyReports.tsx create mode 100755 apps/vaults-v3/components/graphs/GraphForVaultEarnings.tsx create mode 100755 apps/vaults-v3/components/graphs/GraphForVaultPPSGrowth.tsx create mode 100755 apps/vaults-v3/components/graphs/GraphForVaultTVL.tsx create mode 100644 apps/vaults-v3/components/list/VaultListFactory.tsx create mode 100644 apps/vaults-v3/components/list/VaultListOptions.tsx create mode 100755 apps/vaults-v3/components/list/VaultsListEmpty.tsx create mode 100755 apps/vaults-v3/components/list/VaultsListInternalMigrationRow.tsx create mode 100755 apps/vaults-v3/components/list/VaultsListRetired.tsx create mode 100755 apps/vaults-v3/components/list/VaultsListRow.tsx create mode 100644 apps/vaults-v3/constants/index.ts create mode 100644 apps/vaults-v3/constants/menu.ts create mode 100644 apps/vaults-v3/constants/optRewards.ts create mode 100755 pages/vaults-v3/[chainID]/[address].tsx create mode 100755 pages/vaults-v3/about.tsx create mode 100644 pages/vaults-v3/index.tsx diff --git a/apps/common/components/AppHeader.tsx b/apps/common/components/AppHeader.tsx index 70b93c461..94bc0b04b 100644 --- a/apps/common/components/AppHeader.tsx +++ b/apps/common/components/AppHeader.tsx @@ -12,6 +12,7 @@ import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; import {BalanceReminderPopover} from '@common/components/BalanceReminderPopover'; import {ImageWithFallback} from '@common/components/ImageWithFallback'; import {useMenu} from '@common/contexts/useMenu'; +import {useCurrentApp} from '@common/hooks/useCurrentApp'; import {LogoYearn} from '@common/icons/LogoYearn'; import {YBalHeader} from '@yBal/components/header/YBalHeader'; import {YBribeHeader} from '@yBribe/components/header/YBribeHeader'; @@ -48,6 +49,12 @@ function Logo(): ReactElement { function LogoPopover(): ReactElement { const [isShowing, set_isShowing] = useState(false); + const router = useRouter(); + const {name} = useCurrentApp(router); + + if (name === 'V3') { + return ; + } const YETH = { name: 'yETH', @@ -136,6 +143,10 @@ export function AppHeader(): ReactElement { return [HOME_MENU, ...APPS[AppName.YBAL].menu]; } + if (pathname.startsWith('/vaults-v3')) { + return [HOME_MENU, ...APPS[AppName.VAULTSV3].menu]; + } + if (pathname.startsWith('/vaults')) { return [HOME_MENU, ...APPS[AppName.VAULTS].menu]; } @@ -159,15 +170,6 @@ export function AppHeader(): ReactElement { ]; }, [pathname]); - const supportedNetworks = useMemo((): number[] => { - const ethereumOnlyPaths = ['/ycrv', '/ybal', '/veyfi', '/ybribe']; - if (ethereumOnlyPaths.some((path): boolean => pathname.startsWith(path))) { - return [1]; - } - - return [1, 10, 250, 42161]; - }, [pathname]); - return (
diff --git a/apps/common/components/Apps.tsx b/apps/common/components/Apps.tsx index b26d4193e..2e5381fe8 100644 --- a/apps/common/components/Apps.tsx +++ b/apps/common/components/Apps.tsx @@ -4,6 +4,7 @@ import yBalManifest from 'public/apps/ybal-manifest.json'; import ybribeManifest from 'public/apps/ybribe-manifest.json'; import ycrvManifest from 'public/apps/ycrv-manifest.json'; import {VAULTS_MENU} from '@vaults/constants/menu'; +import {VAULTS_V3_MENU} from '@vaults-v3/constants/menu'; import {VEYFI_MENU} from '@veYFI/constants/menu'; import {YBAL_TOKEN_ADDRESS, YCRV_TOKEN_ADDRESS} from '@yearn-finance/web-lib/utils/constants'; import {ImageWithFallback} from '@common/components/ImageWithFallback'; @@ -17,6 +18,7 @@ import type {TMenu} from '@yearn-finance/web-lib/components/Header'; import type {TMetaFile} from './Meta'; export enum AppName { + VAULTSV3 = 'V3', VAULTS = 'Vaults', YCRV = 'yCRV', YBAL = 'yBal', @@ -34,6 +36,19 @@ type TApp = { }; export const APPS: {[key in AppName]: TApp} = { + V3: { + name: AppName.VAULTSV3, + href: '/vaults-v3', + menu: VAULTS_V3_MENU, + manifest: vaultsManifest, + icon: ( + + ) + }, Vaults: { name: AppName.VAULTS, href: '/vaults', diff --git a/apps/common/hooks/useCurrentApp.tsx b/apps/common/hooks/useCurrentApp.tsx index 19362db18..c6124b25b 100644 --- a/apps/common/hooks/useCurrentApp.tsx +++ b/apps/common/hooks/useCurrentApp.tsx @@ -1,4 +1,4 @@ -import {useMemo} from 'react'; +import {Fragment, useMemo} from 'react'; import {VeYfiHeader} from 'apps/veyfi/components/header/VeYfiHeader'; import homeManifest from 'public/manifest.json'; import {VaultsHeader} from '@vaults/components/header/VaultsHeader'; @@ -23,6 +23,10 @@ type TCurrentApp = { export function useCurrentApp({pathname}: NextRouter): TCurrentApp { return useMemo((): TCurrentApp => { const appMapping: TDict = { + '/vaults-v3': { + ...APPS[AppName.VAULTSV3], + header: + }, '/vaults': { ...APPS[AppName.VAULTS], header: diff --git a/apps/common/schemas/yDaemonVaultsSchemas.ts b/apps/common/schemas/yDaemonVaultsSchemas.ts index 46ee69a33..f39bd55c9 100644 --- a/apps/common/schemas/yDaemonVaultsSchemas.ts +++ b/apps/common/schemas/yDaemonVaultsSchemas.ts @@ -9,33 +9,11 @@ const yDaemonVaultStrategySchema = z.object({ description: z.string(), details: z .object({ - keeper: addressSchema, - strategist: addressSchema, - rewards: addressSchema, - healthCheck: addressSchema, totalDebt: z.string(), totalLoss: z.string(), totalGain: z.string(), - minDebtPerHarvest: z.string(), - maxDebtPerHarvest: z.string(), - estimatedTotalAssets: z.string(), - creditAvailable: z.string(), - debtOutstanding: z.string(), - expectedReturn: z.string(), - delegatedAssets: z.string(), - delegatedValue: z.string(), - version: z.string(), - protocols: z.array(z.string()).or(z.null()), - apr: z.number(), performanceFee: z.number(), lastReport: z.number(), - activation: z.number(), - keepCRV: z.number(), - debtLimit: z.number(), - doHealthCheck: z.boolean(), - inQueue: z.boolean(), - emergencyExit: z.boolean(), - isActive: z.boolean(), debtRatio: z.number().optional() }) .optional(), // Optional for migratable diff --git a/apps/vaults-v3/Mark.tsx b/apps/vaults-v3/Mark.tsx new file mode 100644 index 000000000..8cfec93ad --- /dev/null +++ b/apps/vaults-v3/Mark.tsx @@ -0,0 +1,118 @@ +import type {ReactElement} from 'react'; + +export function V3Mask(props: React.SVGProps): ReactElement { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/apps/vaults-v3/Wrapper.tsx b/apps/vaults-v3/Wrapper.tsx new file mode 100755 index 000000000..30d27b01c --- /dev/null +++ b/apps/vaults-v3/Wrapper.tsx @@ -0,0 +1,36 @@ +import {type ReactElement} from 'react'; +import {type NextRouter} from 'next/router'; +import {AnimatePresence, motion} from 'framer-motion'; +import {AppSettingsContextApp} from '@vaults/contexts/useAppSettings'; +import {StakingRewardsContextApp} from '@vaults/contexts/useStakingRewards'; +import {WalletForZapAppContextApp} from '@vaults/contexts/useWalletForZaps'; +import Meta from '@common/components/Meta'; +import {useCurrentApp} from '@common/hooks/useCurrentApp'; +import {variants} from '@common/utils/animations'; + +export function Wrapper({children, router}: {children: ReactElement; router: NextRouter}): ReactElement { + const {manifest} = useCurrentApp(router); + + return ( + <> + + + + + + + {children} + + + + + + + ); +} diff --git a/apps/vaults-v3/components/ImageWithOverlay.tsx b/apps/vaults-v3/components/ImageWithOverlay.tsx new file mode 100644 index 000000000..8279682f9 --- /dev/null +++ b/apps/vaults-v3/components/ImageWithOverlay.tsx @@ -0,0 +1,53 @@ +import {IconCross} from '@yearn-finance/web-lib/icons/IconCross'; +import {ImageWithFallback} from '@common/components/ImageWithFallback'; + +import type {ReactElement} from 'react'; + +type TImageWithOverlayProps = { + imageSrc: string; + imageAlt: string; + imageWidth: number; + imageHeight: number; + overlayText: string; + buttonText: string; + href: string; + onCloseClick: () => void; +}; + +export const ImageWithOverlay: React.FC = ({ + imageSrc, + imageAlt, + imageWidth, + imageHeight, + overlayText, + buttonText, + href, + onCloseClick +}): ReactElement => { + return ( +
+ +
+ +

{overlayText}

+ + + +
+
+ ); +}; diff --git a/apps/vaults-v3/components/ListHero.tsx b/apps/vaults-v3/components/ListHero.tsx new file mode 100644 index 000000000..223af0024 --- /dev/null +++ b/apps/vaults-v3/components/ListHero.tsx @@ -0,0 +1,120 @@ +import {useMemo} from 'react'; +import {ALL_CATEGORIES} from '@vaults/constants'; +import {IconArbitrumChain} from '@yearn-finance/web-lib/icons/chains/IconArbitrumChain'; +import {IconBaseChain} from '@yearn-finance/web-lib/icons/chains/IconBaseChain'; +import {IconEtherumChain} from '@yearn-finance/web-lib/icons/chains/IconEtherumChain'; +import {IconFantomChain} from '@yearn-finance/web-lib/icons/chains/IconFantomChain'; +import {IconOptimismChain} from '@yearn-finance/web-lib/icons/chains/IconOptimismChain'; +import {MultiSelectDropdown} from '@common/components/MultiSelectDropdown'; +import {SearchBar} from '@common/components/SearchBar'; + +import type {ReactElement} from 'react'; +import type {TMultiSelectOptionProps} from '@common/components/MultiSelectDropdown'; + +type TListHero = { + categories: string[]; + chains: number[]; + searchValue: string; + onChangeCategories: (categories: string[]) => void; + onChangeChains: (chains: number[]) => void; + onSearch: (searchValue: string) => void; +}; + +export function ListHero({ + categories, + onChangeCategories, + searchValue, + chains, + onSearch, + onChangeChains +}: TListHero): ReactElement { + const chainOptions = useMemo((): TMultiSelectOptionProps[] => { + return [ + { + label: 'Ethereum', + value: 1, + isSelected: chains.includes(1), + icon: + }, + { + label: 'OP Mainnet', + value: 10, + isSelected: chains.includes(10), + icon: + }, + { + label: 'Fantom', + value: 250, + isSelected: chains.includes(250), + icon: + }, + { + label: 'Base', + value: 8453, + isSelected: chains.includes(8453), + icon: + }, + { + label: 'Arbitrum One', + value: 42161, + isSelected: chains.includes(42161), + icon: + } + ]; + }, [chains]); + + const categoryOptions = useMemo((): TMultiSelectOptionProps[] => { + const options: TMultiSelectOptionProps[] = Object.entries(ALL_CATEGORIES).map( + ([key, value]): TMultiSelectOptionProps => ({ + value: key, + label: value, + isSelected: categories.includes(key) + }) + ); + return options; + }, [categories]); + + return ( +
+
+
+ {'Select Blockchain'} + { + const selectedChains = options + .filter((o): boolean => o.isSelected) + .map((option): number => Number(option.value)); + onChangeChains(selectedChains); + }} + /> +
+ +
+ {'Filter'} + { + const selectedCategories = options + .filter((o): boolean => o.isSelected) + .map((option): string => String(option.value)); + onChangeCategories(selectedCategories); + }} + /> +
+ +
+ {'Search'} + +
+
+
+ ); +} diff --git a/apps/vaults-v3/components/RewardsTab.tsx b/apps/vaults-v3/components/RewardsTab.tsx new file mode 100644 index 000000000..0c3f1322c --- /dev/null +++ b/apps/vaults-v3/components/RewardsTab.tsx @@ -0,0 +1,201 @@ +import {useCallback, useState} from 'react'; +import {useContractRead} from 'wagmi'; +import {useStakingRewards} from '@vaults/contexts/useStakingRewards'; +import {claim as claimAction, stake as stakeAction, unstake as unstakeAction} from '@vaults/utils/actions'; +import {Button} from '@yearn-finance/web-lib/components/Button'; +import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; +import {VAULT_ABI} from '@yearn-finance/web-lib/utils/abi/vault.abi'; +import {toAddress} from '@yearn-finance/web-lib/utils/address'; +import {ZERO_ADDRESS} from '@yearn-finance/web-lib/utils/constants'; +import {toBigInt, toNormalizedBN} from '@yearn-finance/web-lib/utils/format.bigNumber'; +import {formatCounterValue} from '@yearn-finance/web-lib/utils/format.value'; +import {isZero} from '@yearn-finance/web-lib/utils/isZero'; +import {defaultTxStatus} from '@yearn-finance/web-lib/utils/web3/transaction'; +import {Input} from '@common/components/Input'; +import {useWallet} from '@common/contexts/useWallet'; +import {useToken} from '@common/hooks/useToken'; +import {approveERC20} from '@common/utils/actions'; + +import type {ReactElement} from 'react'; +import type {TYDaemonVault} from '@common/schemas/yDaemonVaultsSchemas'; + +const DISPLAY_DECIMALS = 10; +const trimAmount = (amount: string | number): string => Number(Number(amount).toFixed(DISPLAY_DECIMALS)).toString(); + +export function RewardsTab({currentVault}: {currentVault: TYDaemonVault}): ReactElement { + const {provider, address, isActive} = useWeb3(); + const {refresh: refreshBalances} = useWallet(); + const { + stakingRewardsByVault, + stakingRewardsMap, + positionsMap, + refresh: refreshStakingRewards + } = useStakingRewards(); + const stakingRewardsAddress = stakingRewardsByVault[currentVault.address]; + const stakingRewards = stakingRewardsAddress ? stakingRewardsMap[stakingRewardsAddress] : undefined; + const stakingRewardsPosition = stakingRewardsAddress ? positionsMap[stakingRewardsAddress] : undefined; + const vaultToken = useToken({address: currentVault.address, chainID: currentVault.chainID}); + const rewardTokenBalance = useToken({ + address: toAddress(stakingRewards?.rewardsToken), + chainID: currentVault.chainID + }); + const [approveStakeStatus, set_approveStakeStatus] = useState(defaultTxStatus); + const [stakeStatus, set_stakeStatus] = useState(defaultTxStatus); + const [claimStatus, set_claimStatus] = useState(defaultTxStatus); + const [unstakeStatus, set_unstakeStatus] = useState(defaultTxStatus); + const stakeBalance = toNormalizedBN(toBigInt(stakingRewardsPosition?.stake), currentVault.decimals); + const rewardBalance = toNormalizedBN(toBigInt(stakingRewardsPosition?.reward), rewardTokenBalance.decimals); + + const { + data: allowance, + isLoading, + refetch + } = useContractRead({ + address: currentVault.address, + abi: VAULT_ABI, + chainId: currentVault.chainID, + functionName: 'allowance', + args: [toAddress(address), toAddress(stakingRewards?.address)], + enabled: toAddress(stakingRewards?.address) !== ZERO_ADDRESS + }); + const isApproved = toBigInt(allowance) >= vaultToken.balance.raw; + + const refreshData = useCallback(async (): Promise => { + await Promise.all([refreshBalances(), refreshStakingRewards()]); + }, [refreshBalances, refreshStakingRewards]); + + const onApprove = useCallback(async (): Promise => { + const result = await approveERC20({ + connector: provider, + chainID: currentVault.chainID, + contractAddress: currentVault.address, + spenderAddress: toAddress(stakingRewards?.address), + amount: vaultToken.balance.raw, + statusHandler: set_approveStakeStatus + }); + if (result.isSuccessful) { + refetch(); + } + }, [currentVault.address, provider, refetch, stakingRewards?.address, vaultToken.balance.raw]); + + const onStake = useCallback(async (): Promise => { + const result = await stakeAction({ + connector: provider, + chainID: currentVault.chainID, + contractAddress: toAddress(stakingRewards?.address), + amount: vaultToken.balance.raw, + statusHandler: set_stakeStatus + }); + if (result.isSuccessful) { + refreshData(); + } + }, [provider, refreshData, stakingRewards?.address, vaultToken.balance.raw]); + + const onUnstake = useCallback(async (): Promise => { + const result = await unstakeAction({ + connector: provider, + chainID: currentVault.chainID, + contractAddress: toAddress(stakingRewards?.address), + statusHandler: set_unstakeStatus + }); + if (result.isSuccessful) { + refreshData(); + } + }, [provider, refreshData, stakingRewards?.address]); + + const onClaim = useCallback(async (): Promise => { + const result = await claimAction({ + connector: provider, + chainID: currentVault.chainID, + contractAddress: toAddress(stakingRewards?.address), + statusHandler: set_claimStatus + }); + if (result.isSuccessful) { + refreshData(); + } + }, [provider, refreshData, stakingRewards?.address]); + + return ( +
+
+
+
{'Stake'}
+
+

{'Stake your yVault tokens for additional $OP rewards.'}

+
+
+
+ + +
+
+
+
+
{'Claim'}
+
+

{"Claim your staking rewards here. You've earned it anon."}

+
+
+
+ + +
+
+
+
+
{'Unstake'}
+
+

+ { + 'Unstake your yVault tokens and your remaining $OP rewards will be claimed automatically. Boom.' + } +

+
+
+
+ + +
+
+
+ ); +} diff --git a/apps/vaults-v3/components/SettingsPopover.tsx b/apps/vaults-v3/components/SettingsPopover.tsx new file mode 100755 index 000000000..9f8048bab --- /dev/null +++ b/apps/vaults-v3/components/SettingsPopover.tsx @@ -0,0 +1,196 @@ +import {Fragment, useMemo} from 'react'; +import {Popover, Transition} from '@headlessui/react'; +import {isSolverDisabled} from '@vaults/contexts/useSolver'; +import {useStakingRewards} from '@vaults/contexts/useStakingRewards'; +import {Renderable} from '@yearn-finance/web-lib/components/Renderable'; +import {IconSettings} from '@yearn-finance/web-lib/icons/IconSettings'; +import {Switch} from '@common/components/Switch'; +import {useYearn} from '@common/contexts/useYearn'; +import {Solver} from '@common/schemas/yDaemonTokenListBalances'; + +import type {ReactElement} from 'react'; +import type {TSolver} from '@common/schemas/yDaemonTokenListBalances'; +import type {TYDaemonVault} from '@common/schemas/yDaemonVaultsSchemas'; + +type TSettingPopover = { + vault: TYDaemonVault; +}; + +function Label({children}: {children: string}): ReactElement { + return ( + + ); +} + +export function SettingsPopover({vault}: TSettingPopover): ReactElement { + const { + zapProvider, + set_zapProvider, + zapSlippage, + set_zapSlippage, + isStakingOpBoostedVaults, + set_isStakingOpBoostedVaults + } = useYearn(); + const {stakingRewardsByVault} = useStakingRewards(); + + const {address, chainID} = vault; + const hasStakingRewards = !!stakingRewardsByVault?.[address]; + + const currentZapProvider = useMemo((): TSolver => { + if (chainID !== 1 && zapProvider === 'Cowswap') { + return 'Wido'; + } + return zapProvider; + }, [chainID, zapProvider]); + + return ( + + {(): ReactElement => ( + <> + + {'Settings'} + + + + +
+
+
+ + + + + {'Submit a'}  + + {'gasless order'} + +  {'using CoW Swap.'} + + + + + {'Submit an order via'}  + + {'Wido'} + +  {'(0.3% fee).'} + + + +   + +
+
+ +
+ + +
+ { + set_zapSlippage(parseFloat(e.target.value) || 0); + }} + /> +

{'%'}

+
+
+
+ {hasStakingRewards ? ( +
+ +
+
+

{'Stake automatically'}

+ + set_isStakingOpBoostedVaults(!isStakingOpBoostedVaults) + } + /> +
+
+
+ ) : null} +
+
+
+
+ + )} +
+ ); +} diff --git a/apps/vaults-v3/components/details/VaultActionsTabsWrapper.tsx b/apps/vaults-v3/components/details/VaultActionsTabsWrapper.tsx new file mode 100755 index 000000000..c0387e186 --- /dev/null +++ b/apps/vaults-v3/components/details/VaultActionsTabsWrapper.tsx @@ -0,0 +1,340 @@ +import {Fragment, useEffect, useState} from 'react'; +import Link from 'next/link'; +import {useRouter} from 'next/router'; +import {Listbox, Transition} from '@headlessui/react'; +import {useUpdateEffect} from '@react-hookz/web'; +import {VaultDetailsQuickActionsButtons} from '@vaults/components/details/actions/QuickActionsButtons'; +import {VaultDetailsQuickActionsFrom} from '@vaults/components/details/actions/QuickActionsFrom'; +import {VaultDetailsQuickActionsSwitch} from '@vaults/components/details/actions/QuickActionsSwitch'; +import {VaultDetailsQuickActionsTo} from '@vaults/components/details/actions/QuickActionsTo'; +import {ImageWithOverlay} from '@vaults/components/ImageWithOverlay'; +import {RewardsTab} from '@vaults/components/RewardsTab'; +import {SettingsPopover} from '@vaults/components/SettingsPopover'; +import {Flow, useActionFlow} from '@vaults/contexts/useActionFlow'; +import {useStakingRewards} from '@vaults/contexts/useStakingRewards'; +import {Banner} from '@yearn-finance/web-lib/components/Banner'; +import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; +import {useLocalStorage} from '@yearn-finance/web-lib/hooks/useLocalStorage'; +import {toAddress} from '@yearn-finance/web-lib/utils/address'; +import {toBigInt, toNormalizedBN} from '@yearn-finance/web-lib/utils/format.bigNumber'; +import {isZero} from '@yearn-finance/web-lib/utils/isZero'; +import {performBatchedUpdates} from '@yearn-finance/web-lib/utils/performBatchedUpdates'; +import {useToken} from '@common/hooks/useToken'; +import {IconChevron} from '@common/icons/IconChevron'; +import {Solver} from '@common/schemas/yDaemonTokenListBalances'; + +import type {ReactElement} from 'react'; +import type {TYDaemonVault} from '@common/schemas/yDaemonVaultsSchemas'; + +type TTabsOptions = { + value: number; + label: string; + flowAction: Flow; + slug?: string; +}; + +const tabs: TTabsOptions[] = [ + {value: 0, label: 'Deposit', flowAction: Flow.Deposit, slug: 'deposit'}, + {value: 1, label: 'Withdraw', flowAction: Flow.Withdraw, slug: 'withdraw'}, + {value: 2, label: 'Migrate', flowAction: Flow.Migrate, slug: 'migrate'}, + {value: 3, label: '$OP BOOST', flowAction: Flow.None, slug: 'boost'} +]; + +const DISPLAY_DECIMALS = 10; + +function getCurrentTab({ + isDepositing, + hasMigration, + isRetired +}: { + isDepositing: boolean; + hasMigration: boolean; + isRetired: boolean; +}): TTabsOptions { + if (hasMigration || isRetired) { + return tabs[1]; + } + return tabs.find((tab): boolean => tab.value === (isDepositing ? 0 : 1)) as TTabsOptions; +} + +export function VaultActionsTabsWrapper({currentVault}: {currentVault: TYDaemonVault}): ReactElement { + const {onSwitchSelectedOptions, isDepositing, actionParams, currentSolver} = useActionFlow(); + const [possibleTabs, set_possibleTabs] = useState([tabs[0], tabs[1]]); + const {stakingRewardsMap, positionsMap, stakingRewardsByVault} = useStakingRewards(); + const willDepositAndStake = currentSolver === Solver.enum.OptimismBooster; + const stakingRewardsAddress = stakingRewardsByVault[currentVault.address]; + const stakingRewards = stakingRewardsAddress ? stakingRewardsMap[stakingRewardsAddress] : undefined; + const stakingRewardsPosition = stakingRewardsAddress ? positionsMap[stakingRewardsAddress] : undefined; + const rewardTokenBalance = useToken({ + address: toAddress(stakingRewards?.rewardsToken), + chainID: currentVault.chainID + }); + const hasStakingRewards = !!stakingRewardsByVault[currentVault.address]; + const [currentTab, set_currentTab] = useState( + getCurrentTab({ + isDepositing, + hasMigration: currentVault?.migration?.available, + isRetired: currentVault?.retired + }) + ); + const [shouldShowLedgerPluginBanner, set_shouldShowLedgerPluginBanner] = useLocalStorage( + 'yearn.fi/ledger-plugin-banner', + true + ); + const [shouldShowOpBoostInfo, set_shouldShowOpBoostInfo] = useLocalStorage( + 'yearn.fi/op-boost-banner', + true + ); + const router = useRouter(); + const {isWalletLedger} = useWeb3(); + const rewardBalance = toNormalizedBN(toBigInt(stakingRewardsPosition?.reward), rewardTokenBalance.decimals); + + useEffect((): void => { + const tab = tabs.find((tab): boolean => tab.slug === router.query.action); + if (tab?.value) { + set_currentTab(tab); + } + }, [router.query.action, set_currentTab]); + + useUpdateEffect((): void => { + if (currentVault?.migration?.available && actionParams.isReady) { + performBatchedUpdates((): void => { + set_possibleTabs([tabs[1], tabs[2]]); + set_currentTab(tabs[2]); + onSwitchSelectedOptions(Flow.Migrate); + }); + } else if (currentVault?.retired && actionParams.isReady) { + performBatchedUpdates((): void => { + set_possibleTabs([tabs[1]]); + set_currentTab(tabs[1]); + onSwitchSelectedOptions(Flow.Withdraw); + }); + } + + if (currentVault.chainID === 10 && hasStakingRewards) { + performBatchedUpdates((): void => { + set_possibleTabs([tabs[0], tabs[1], tabs[3]]); + }); + } + }, [currentVault?.migration?.available, currentVault?.retired, actionParams.isReady, hasStakingRewards]); + + const isLedgerPluginVisible = isWalletLedger && shouldShowLedgerPluginBanner; + + return ( + <> + {isLedgerPluginVisible && ( +
+ set_shouldShowLedgerPluginBanner(false)} + overlayText={'SIGN IN WITH LEDGER'} + buttonText={'DOWNLOAD LEDGER PLUGIN'} + /> +
+ )} + + {currentVault?.migration?.available && ( +
+
+ {'Looks like this is an old vault.'} +

+ { + 'This Vault is no longer earning yield, but good news, there’s a shiny up to date version just waiting for you to deposit your tokens into. Click migrate, and your tokens will be migrated to the current Vault, which will be mi-great!' + } +

+
+
+ )} + + {!currentVault?.migration.available && currentVault?.retired && ( +
+
+ {'This Vault is no longer supported (oh no).'} +

+ { + 'They say all good things must come to an end, and sadly this vault is deprecated and will no longer earn yield or be supported by Yearn. Please withdraw your funds (which you could deposit into another Vault. Just saying…)' + } +

+
+
+ )} + + +
+
+ +
+ { + const newTab = tabs.find((tab): boolean => tab.value === Number(value)); + if (!newTab) { + return; + } + set_currentTab(newTab); + onSwitchSelectedOptions(newTab.flowAction); + }}> + {({open}): ReactElement => ( + <> + +
+ {currentTab?.label || 'Menu'} +
+
+ +
+
+ + + {possibleTabs.map( + (tab): ReactElement => ( + + {tab.label} + + ) + )} + + + + )} +
+
+ +
+ +
+
+
+ + {shouldShowOpBoostInfo && !isZero(rewardBalance.normalized) && ( +
+ set_shouldShowOpBoostInfo(false)} + /> +
+ )} + {currentTab.value === 3 ? ( + + ) : ( +
+ + + +
+ +
+ +
+   +
+
+ )} + + {isZero(currentTab.value) && + currentVault.apr?.forwardAPR?.composite?.boost && + hasStakingRewards && + willDepositAndStake ? ( +
+
+ + { + 'Great news! This Vault is receiving an Optimism Boost. Deposit and stake your tokens to receive OP rewards. Nice!' + } + +
+
+ ) : ( + isZero(currentTab.value) && + hasStakingRewards && + !willDepositAndStake && ( +
+
+ + { + "This Vault is receiving an Optimism Boost. To zap into it for additional OP rewards, you'll have to stake your yVault tokens manually on the $OP BOOST tab after you deposit. Sorry anon, it's just how it works." + } + +
+
+ ) + )} +
+ + ); +} diff --git a/apps/vaults-v3/components/details/VaultDetailsHeader.tsx b/apps/vaults-v3/components/details/VaultDetailsHeader.tsx new file mode 100755 index 000000000..ab1660ac9 --- /dev/null +++ b/apps/vaults-v3/components/details/VaultDetailsHeader.tsx @@ -0,0 +1,173 @@ +import {useMemo} from 'react'; +import {useStakingRewards} from '@vaults/contexts/useStakingRewards'; +import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; +import {toAddress} from '@yearn-finance/web-lib/utils/address'; +import {toBigInt, toNormalizedBN} from '@yearn-finance/web-lib/utils/format.bigNumber'; +import {formatUSD} from '@yearn-finance/web-lib/utils/format.number'; +import {formatCounterValue} from '@yearn-finance/web-lib/utils/format.value'; +import {copyToClipboard} from '@yearn-finance/web-lib/utils/helpers'; +import {RenderAmount} from '@common/components/RenderAmount'; +import {useBalance} from '@common/hooks/useBalance'; +import {useFetch} from '@common/hooks/useFetch'; +import {useTokenPrice} from '@common/hooks/useTokenPrice'; +import {IconQuestion} from '@common/icons/IconQuestion'; +import {yDaemonSingleEarnedSchema} from '@common/schemas/yDaemonEarnedSchema'; +import {getVaultName} from '@common/utils'; +import {useYDaemonBaseURI} from '@common/utils/getYDaemonBaseURI'; + +import type {ReactElement} from 'react'; +import type {TYDaemonEarnedSingle} from '@common/schemas/yDaemonEarnedSchema'; +import type {TYDaemonVault} from '@common/schemas/yDaemonVaultsSchemas'; +import type {TNormalizedBN} from '@common/types/types'; + +type TVaultHeaderLineItemProps = { + label: string; + children: ReactElement | string; + legend?: ReactElement | string; +}; + +function VaultHeaderLineItem({label, children, legend}: TVaultHeaderLineItemProps): ReactElement { + return ( +
+

{label}

+ + {children} + + + {legend ? legend : '\u00A0'} + +
+ ); +} + +function VaultAPR({apr}: {apr: TYDaemonVault['apr']}): ReactElement { + if (apr.forwardAPR.type === '' && apr.extra.stakingRewardsAPR === 0) { + return ( + + + + ); + } + return ( + +
+
+ {'Est. APR - '} + +
+ +
+ +
+

+ {'Estimated APR for the next period based on current data.'} +

+
+
+ + }> + +
+ ); +} + +export function VaultDetailsHeader({currentVault}: {currentVault: TYDaemonVault}): ReactElement { + const {address: userAddress} = useWeb3(); + const {yDaemonBaseUri} = useYDaemonBaseURI({chainID: currentVault.chainID}); + const {address, apr, tvl, decimals, symbol = 'token', token} = currentVault; + + const {data: earned} = useFetch({ + endpoint: address && userAddress ? `${yDaemonBaseUri}/earned/${userAddress}/${currentVault.address}` : null, + schema: yDaemonSingleEarnedSchema + }); + + const normalizedVaultEarned = useMemo((): TNormalizedBN => { + const {unrealizedGains} = earned?.earned?.[toAddress(currentVault.address)] || {}; + const value = toBigInt(unrealizedGains); + return toNormalizedBN(value < 0n ? 0n : value); + }, [earned?.earned, address]); + + const vaultBalance = useBalance({address, chainID: currentVault.chainID}); + const vaultPrice = useTokenPrice(address) || currentVault?.tvl?.price || 0; + const vaultName = useMemo((): string => getVaultName(currentVault), [currentVault]); + const {stakingRewardsByVault, positionsMap} = useStakingRewards(); + const stakedBalance = toBigInt(positionsMap[toAddress(stakingRewardsByVault[address])]?.stake); + const depositedAndStaked = toNormalizedBN(vaultBalance.raw + stakedBalance, decimals); + + return ( +
+ +  {vaultName}  + +
+ {address ? ( + + ) : ( +

 

+ )} +
+
+ + + + + + + + + + + + + +
+
+ ); +} diff --git a/apps/vaults-v3/components/details/actions/QuickActionsButtons.tsx b/apps/vaults-v3/components/details/actions/QuickActionsButtons.tsx new file mode 100644 index 000000000..9d23f4c98 --- /dev/null +++ b/apps/vaults-v3/components/details/actions/QuickActionsButtons.tsx @@ -0,0 +1,251 @@ +import {useCallback, useState} from 'react'; +import {useActionFlow} from '@vaults/contexts/useActionFlow'; +import {useSolver} from '@vaults/contexts/useSolver'; +import {useStakingRewards} from '@vaults/contexts/useStakingRewards'; +import {useWalletForZap} from '@vaults/contexts/useWalletForZaps'; +import {Button} from '@yearn-finance/web-lib/components/Button'; +import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; +import {toAddress} from '@yearn-finance/web-lib/utils/address'; +import {MAX_UINT_256} from '@yearn-finance/web-lib/utils/constants'; +import {toBigInt, toNormalizedBN} from '@yearn-finance/web-lib/utils/format.bigNumber'; +import {isEth} from '@yearn-finance/web-lib/utils/isEth'; +import {isZero} from '@yearn-finance/web-lib/utils/isZero'; +import {defaultTxStatus} from '@yearn-finance/web-lib/utils/web3/transaction'; +import {useWallet} from '@common/contexts/useWallet'; +import {useYearn} from '@common/contexts/useYearn'; +import {useAsyncTrigger} from '@common/hooks/useAsyncEffect'; +import {Solver} from '@common/schemas/yDaemonTokenListBalances'; + +import type {ReactElement} from 'react'; +import type {TNormalizedBN} from '@yearn-finance/web-lib/utils/format.bigNumber'; +import type {TYDaemonVault} from '@common/schemas/yDaemonVaultsSchemas'; + +export function VaultDetailsQuickActionsButtons({currentVault}: {currentVault: TYDaemonVault}): ReactElement { + const {refresh} = useWallet(); + const {refresh: refreshZapBalances} = useWalletForZap(); + const {refresh: refreshStakingRewards} = useStakingRewards(); + const {address, provider} = useWeb3(); + const {isStakingOpBoostedVaults} = useYearn(); + const [txStatusApprove, set_txStatusApprove] = useState(defaultTxStatus); + const [txStatusExecuteDeposit, set_txStatusExecuteDeposit] = useState(defaultTxStatus); + const [txStatusExecuteWithdraw, set_txStatusExecuteWithdraw] = useState(defaultTxStatus); + const [allowanceFrom, set_allowanceFrom] = useState(toNormalizedBN(0)); + const {actionParams, onChangeAmount, maxDepositPossible, isDepositing} = useActionFlow(); + const { + onApprove, + onExecuteDeposit, + onExecuteWithdraw, + onRetrieveAllowance, + currentSolver, + expectedOut, + isLoadingExpectedOut, + hash + } = useSolver(); + const isWithdrawing = !isDepositing; + + /* 🔵 - Yearn Finance ************************************************************************** + ** SWR hook to get the expected out for a given in/out pair with a specific amount. This hook is + ** called when amount/in or out changes. Calls the allowanceFetcher callback. + **********************************************************************************************/ + const triggerRetrieveAllowance = useAsyncTrigger(async (): Promise => { + set_allowanceFrom(await onRetrieveAllowance(true)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [address, onRetrieveAllowance, hash]); + + const onSuccess = useCallback(async (): Promise => { + onChangeAmount(toNormalizedBN(0)); + if ( + Solver.enum.Vanilla === currentSolver || + Solver.enum.ChainCoin === currentSolver || + Solver.enum.PartnerContract === currentSolver || + Solver.enum.OptimismBooster === currentSolver || + Solver.enum.InternalMigration === currentSolver + ) { + await refresh([ + { + address: toAddress(actionParams?.selectedOptionFrom?.value), + chainID: currentVault.chainID + }, + { + address: toAddress(actionParams?.selectedOptionTo?.value), + chainID: currentVault.chainID + } + ]); + if (Solver.enum.OptimismBooster === currentSolver) { + await refreshStakingRewards(); + } + } else if ( + Solver.enum.Cowswap === currentSolver || + Solver.enum.Portals === currentSolver || + Solver.enum.Wido === currentSolver + ) { + if (isDepositing) { + //refresh input from zap wallet, refresh output from default + await Promise.all([ + refreshZapBalances([ + { + address: toAddress(actionParams?.selectedOptionFrom?.value), + chainID: currentVault.chainID + } + ]), + refresh([ + { + address: toAddress(actionParams?.selectedOptionTo?.value), + chainID: currentVault.chainID + } + ]) + ]); + } else { + await Promise.all([ + refreshZapBalances([ + { + address: toAddress(actionParams?.selectedOptionTo?.value), + chainID: currentVault.chainID + } + ]), + refresh([ + { + address: toAddress(actionParams?.selectedOptionFrom?.value), + chainID: currentVault.chainID + } + ]) + ]); + } + } + }, [ + onChangeAmount, + currentSolver, + refresh, + actionParams?.selectedOptionFrom?.value, + actionParams?.selectedOptionTo?.value, + currentVault.chainID, + refreshStakingRewards, + isDepositing, + refreshZapBalances + ]); + + /* 🔵 - Yearn Finance ****************************************************** + ** Trigger an approve web3 action, simply trying to approve `amount` tokens + ** to be used by the Partner contract or the final vault, in charge of + ** depositing the tokens. + ** This approve can not be triggered if the wallet is not active + ** (not connected) or if the tx is still pending. + **************************************************************************/ + const onApproveFrom = useCallback(async (): Promise => { + const shouldApproveInfinite = + currentSolver === Solver.enum.PartnerContract || + currentSolver === Solver.enum.Vanilla || + currentSolver === Solver.enum.InternalMigration; + onApprove( + shouldApproveInfinite ? MAX_UINT_256 : actionParams?.amount.raw, + set_txStatusApprove, + async (): Promise => { + await triggerRetrieveAllowance(); + } + ); + }, [actionParams?.amount.raw, triggerRetrieveAllowance, currentSolver, onApprove]); + + const isButtonDisabled = + (!address && !provider) || + isZero(actionParams.amount.raw) || + toBigInt(actionParams.amount.raw) > toBigInt(maxDepositPossible.raw) || + isLoadingExpectedOut; + + /* 🔵 - Yearn Finance ****************************************************** + ** Wrapper to decide if we should use the partner contract or not + **************************************************************************/ + const isAboveAllowance = toBigInt(actionParams.amount.raw) > toBigInt(allowanceFrom?.raw); + const isButtonBusy = txStatusApprove.pending || status !== 'success'; + + if ( + isWithdrawing && //If user is withdrawing ... + currentSolver === Solver.enum.ChainCoin && // ... and the solver is ChainCoin ... + isEth(actionParams?.selectedOptionTo?.value) && // ... and the output is ETH ... + isAboveAllowance // ... and the amount is above the allowance + ) { + // ... then we need to approve the ChainCoin contract + return ( + + ); + } + + if ( + isDepositing && //If user is depositing ... + currentSolver === Solver.enum.ChainCoin // ... and the solver is ChainCoin ... + ) { + // ... then we can deposit without approval + return ( + + ); + } + + if ( + (isButtonBusy || isAboveAllowance) && //If the button is busy or the amount is above the allowance ... + ((isDepositing && currentSolver === Solver.enum.Vanilla) || // ... and the user is depositing with Vanilla ... + currentSolver === Solver.enum.InternalMigration || // ... or the user is migrating ... + currentSolver === Solver.enum.Cowswap || // ... or the user is using Cowswap ... + currentSolver === Solver.enum.Wido || // ... or the user is using Wido ... + currentSolver === Solver.enum.Portals || // ... or the user is using Portals ... + currentSolver === Solver.enum.PartnerContract || // ... or the user is using the Partner contract ... + currentSolver === Solver.enum.OptimismBooster) // ... or the user is using the Optimism Booster ... // ... then we need to approve the from token + ) { + return ( + + ); + } + + if (isDepositing || currentSolver === Solver.enum.InternalMigration) { + if (currentSolver === Solver.enum.OptimismBooster && isStakingOpBoostedVaults) { + return ( + + ); + } + return ( + + ); + } + + return ( + + ); +} diff --git a/apps/vaults-v3/components/details/actions/QuickActionsFrom.tsx b/apps/vaults-v3/components/details/actions/QuickActionsFrom.tsx new file mode 100644 index 000000000..3ee8ad76d --- /dev/null +++ b/apps/vaults-v3/components/details/actions/QuickActionsFrom.tsx @@ -0,0 +1,142 @@ +import {useCallback} from 'react'; +import {useActionFlow} from '@vaults/contexts/useActionFlow'; +import {Renderable} from '@yearn-finance/web-lib/components/Renderable'; +import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; +import {toAddress} from '@yearn-finance/web-lib/utils/address'; +import {formatAmount} from '@yearn-finance/web-lib/utils/format.number'; +import {formatCounterValue} from '@yearn-finance/web-lib/utils/format.value'; +import {handleInputChangeEventValue} from '@yearn-finance/web-lib/utils/handlers/handleInputChangeEventValue'; +import {Dropdown} from '@common/components/TokenDropdown'; +import {useWallet} from '@common/contexts/useWallet'; +import {useBalance} from '@common/hooks/useBalance'; +import {useTokenPrice} from '@common/hooks/useTokenPrice'; + +import type {ChangeEvent, ReactElement} from 'react'; + +export function VaultDetailsQuickActionsFrom(): ReactElement { + const {isActive} = useWeb3(); + const {getToken} = useWallet(); + const { + possibleOptionsFrom, + actionParams, + onUpdateSelectedOptionFrom, + onChangeAmount, + maxDepositPossible, + isDepositing + } = useActionFlow(); + const selectedFromBalance = useBalance({ + address: toAddress(actionParams?.selectedOptionFrom?.value), + chainID: Number(actionParams?.selectedOptionFrom?.chainID) + }); + const selectedOptionFromPricePerToken = useTokenPrice(toAddress(actionParams?.selectedOptionFrom?.value)); + const hasMultipleInputsToChooseFrom = isActive && isDepositing && possibleOptionsFrom.length > 1; + const selectedFromSymbol = actionParams?.selectedOptionFrom?.symbol || 'tokens'; + const selectedFromIcon = actionParams?.selectedOptionFrom?.icon; + + function renderMultipleOptionsFallback(): ReactElement { + return ( + + ); + } + + const onChangeInput = useCallback( + (e: ChangeEvent): void => { + onChangeAmount( + handleInputChangeEventValue( + e.target.value, + getToken({ + address: toAddress(actionParams?.selectedOptionFrom?.value), + chainID: Number(actionParams?.selectedOptionFrom?.chainID) + }).decimals + ) + ); + }, + [actionParams?.selectedOptionFrom?.value, getToken, onChangeAmount] + ); + + return ( +
+
+
+ + + {`You have ${formatAmount(selectedFromBalance.normalized)} ${ + actionParams?.selectedOptionFrom?.symbol || 'tokens' + }`} + +
+ +
+
+
{selectedFromIcon}
+

+ {selectedFromSymbol} +

+
+
+
+ + +
+
+ +
+
+ + +
+
+ + {formatCounterValue(actionParams?.amount?.normalized || 0, selectedOptionFromPricePerToken)} + +
+
+ ); +} diff --git a/apps/vaults-v3/components/details/actions/QuickActionsSwitch.tsx b/apps/vaults-v3/components/details/actions/QuickActionsSwitch.tsx new file mode 100644 index 000000000..e5ea3a79c --- /dev/null +++ b/apps/vaults-v3/components/details/actions/QuickActionsSwitch.tsx @@ -0,0 +1,17 @@ +import {IconArrowRight} from '@common/icons/IconArrowRight'; + +import type {ReactElement} from 'react'; + +export function VaultDetailsQuickActionsSwitch(): ReactElement { + return ( +
+ + +
+ {'Deposit / Withdraw'} + +
+   +
+ ); +} diff --git a/apps/vaults-v3/components/details/actions/QuickActionsTo.tsx b/apps/vaults-v3/components/details/actions/QuickActionsTo.tsx new file mode 100644 index 000000000..835eb1dc5 --- /dev/null +++ b/apps/vaults-v3/components/details/actions/QuickActionsTo.tsx @@ -0,0 +1,121 @@ +import {useActionFlow} from '@vaults/contexts/useActionFlow'; +import {useSolver} from '@vaults/contexts/useSolver'; +import {Renderable} from '@yearn-finance/web-lib/components/Renderable'; +import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; +import {toAddress} from '@yearn-finance/web-lib/utils/address'; +import {formatPercent} from '@yearn-finance/web-lib/utils/format.number'; +import {formatCounterValue} from '@yearn-finance/web-lib/utils/format.value'; +import {Dropdown} from '@common/components/TokenDropdown'; +import {useTokenPrice} from '@common/hooks/useTokenPrice'; + +import type {ReactElement} from 'react'; + +export function VaultDetailsQuickActionsTo(): ReactElement { + const {isActive} = useWeb3(); + const {currentVault, possibleOptionsTo, actionParams, onUpdateSelectedOptionTo, isDepositing} = useActionFlow(); + const {expectedOut, isLoadingExpectedOut} = useSolver(); + const selectedOptionToPricePerToken = useTokenPrice(toAddress(actionParams?.selectedOptionTo?.value)); + const isMigrationAvailable = currentVault?.migration?.available; + + function renderMultipleOptionsFallback(): ReactElement { + return ( + + ); + } + + return ( +
+
+
+ + + {`APR ${formatPercent( + (currentVault.apr.netAPR + currentVault.apr.extra.stakingRewardsAPR) * 100, + 2, + 2, + 500 + )}`} + +
+ +
+
+
+ {actionParams?.selectedOptionTo?.icon} +
+

+ {actionParams?.selectedOptionTo?.symbol} +

+
+
+
+ +
+ +
+ +
+
+ {isLoadingExpectedOut ? ( +
+
+ +
+
+ ) : ( + + )} +
+
+ + {formatCounterValue(expectedOut?.normalized || 0, selectedOptionToPricePerToken)} + +
+
+ ); +} diff --git a/apps/vaults-v3/components/details/tabs/VaultDetailsAbout.tsx b/apps/vaults-v3/components/details/tabs/VaultDetailsAbout.tsx new file mode 100755 index 000000000..1998703b5 --- /dev/null +++ b/apps/vaults-v3/components/details/tabs/VaultDetailsAbout.tsx @@ -0,0 +1,195 @@ +import {useIsMounted} from '@react-hookz/web'; +import {GraphForVaultEarnings} from '@vaults/components/graphs/GraphForVaultEarnings'; +import {Renderable} from '@yearn-finance/web-lib/components/Renderable'; +import {cl} from '@yearn-finance/web-lib/utils/cl'; +import {formatPercent} from '@yearn-finance/web-lib/utils/format.number'; +import {parseMarkdown} from '@yearn-finance/web-lib/utils/helpers'; +import {isZero} from '@yearn-finance/web-lib/utils/isZero'; + +import type {ReactElement} from 'react'; +import type {TYDaemonVault} from '@common/schemas/yDaemonVaultsSchemas'; +import type {TGraphData} from '@common/types/types'; + +type TAPRLineItemProps = { + label: string; + value: number | string; + apyType: string; + hasUpperLimit?: boolean; +}; + +type TYearnFeesLineItem = { + children: ReactElement; + label: string; + tooltip?: string; +}; + +function APRLineItem({value, label, apyType, hasUpperLimit}: TAPRLineItemProps): ReactElement { + const safeValue = Number(value) || 0; + const isNew = apyType === 'new' && isZero(safeValue); + + return ( +
+

{label}

+

+ {isNew + ? 'New' + : hasUpperLimit + ? formatPercent(safeValue * 100) + : formatPercent(safeValue * 100, 2, 2, 500)} +

+
+ ); +} + +function YearnFeesLineItem({children, label, tooltip}: TYearnFeesLineItem): ReactElement { + return ( +
+

{label}

+
+ {tooltip ? ( + +
+ {tooltip} +
+
+ ) : null} + {children} +
+
+ ); +} + +export function VaultDetailsAbout({ + currentVault, + harvestData +}: { + currentVault: TYDaemonVault; + harvestData: TGraphData[]; +}): ReactElement { + const isMounted = useIsMounted(); + const {token, apr} = currentVault; + + function getVaultDescription(): string { + if (token.description) { + return parseMarkdown(token.description); + } + return 'Sorry, we don\'t have a description for this asset right now. But did you know the correct word for a blob of toothpaste is a "nurdle". Fascinating! We\'ll work on updating the asset description, but at least you learnt something interesting. Catch ya later nurdles.'; + } + + return ( +
+
+
+ {'Description'} +

+

+
+ {'APR'} +
+
+ + + +
+
+ + {apr.extra.stakingRewardsAPR > 0 && ( +
+ + +
+ )} +
+
+
+
+
+
+ {'Yearn Fees'} +
+ + {formatPercent(0, 0, 0)} + + + + {formatPercent((apr.fees.management || 0) / 100, 0)} + + + + + {formatPercent((apr.fees.performance || 0) / 100, 0)} + + + {currentVault.category === 'Velodrome' || currentVault.category === 'Aerodrome' ? ( + + + {formatPercent(currentVault.apr.fees.keepVelo * 100, 0)} + + + ) : null} +
+
+
+ {'Cumulative Earnings'} +
+ + + +
+
+
+
+ ); +} diff --git a/apps/vaults-v3/components/details/tabs/VaultDetailsHistorical.tsx b/apps/vaults-v3/components/details/tabs/VaultDetailsHistorical.tsx new file mode 100755 index 000000000..4a24d7967 --- /dev/null +++ b/apps/vaults-v3/components/details/tabs/VaultDetailsHistorical.tsx @@ -0,0 +1,111 @@ +import {useMemo, useState} from 'react'; +import useSWR from 'swr'; +import {useIsMounted} from '@react-hookz/web'; +import {GraphForVaultEarnings} from '@vaults/components/graphs/GraphForVaultEarnings'; +import {GraphForVaultPPSGrowth} from '@vaults/components/graphs/GraphForVaultPPSGrowth'; +import {GraphForVaultTVL} from '@vaults/components/graphs/GraphForVaultTVL'; +import {getMessariSubgraphEndpoint} from '@vaults/utils'; +import {Button} from '@yearn-finance/web-lib/components/Button'; +import {Renderable} from '@yearn-finance/web-lib/components/Renderable'; +import {formatToNormalizedValue, toBigInt} from '@yearn-finance/web-lib/utils/format.bigNumber'; +import {formatDate} from '@yearn-finance/web-lib/utils/format.time'; +import {isZero} from '@yearn-finance/web-lib/utils/isZero'; +import {graphFetcher} from '@common/utils'; + +import type {ReactElement} from 'react'; +import type {TYDaemonVault} from '@common/schemas/yDaemonVaultsSchemas'; +import type {TGraphData, TMessariGraphData} from '@common/types/types'; + +export function VaultDetailsHistorical({ + currentVault, + harvestData +}: { + currentVault: TYDaemonVault; + harvestData: TGraphData[]; +}): ReactElement { + const isMounted = useIsMounted(); + const [selectedViewIndex, set_selectedViewIndex] = useState(0); + + const {data: messariMixedData} = useSWR( + currentVault.address + ? [ + getMessariSubgraphEndpoint(currentVault.chainID), + `{ + vaultDailySnapshots( + where: {vault: "${currentVault.address.toLowerCase()}"} + orderBy: timestamp + orderDirection: asc + first: 1000 + ) { + pricePerShare + totalValueLockedUSD + timestamp + } + }` + ] + : null, + graphFetcher + ); + + const messariData = useMemo((): TMessariGraphData[] => { + const _messariMixedData = [ + ...((messariMixedData?.vaultDailySnapshots as { + timestamp: string; + totalValueLockedUSD: string; + pricePerShare: string; + }[]) || []) + ]; + return _messariMixedData?.map( + (elem): TMessariGraphData => ({ + name: formatDate(Number(elem.timestamp) * 1000), + tvl: Number(elem.totalValueLockedUSD), + pps: formatToNormalizedValue(toBigInt(elem.pricePerShare), currentVault.decimals) + }) + ); + }, [currentVault.decimals, messariMixedData?.vaultDailySnapshots]); + + return ( +
+
+
+ + + +
+
+
+ + + + + + + + + + + +
+
+ ); +} diff --git a/apps/vaults-v3/components/details/tabs/VaultDetailsStrategies.tsx b/apps/vaults-v3/components/details/tabs/VaultDetailsStrategies.tsx new file mode 100755 index 000000000..201b0e005 --- /dev/null +++ b/apps/vaults-v3/components/details/tabs/VaultDetailsStrategies.tsx @@ -0,0 +1,263 @@ +import {useMemo, useState} from 'react'; +import {useIsMounted} from '@react-hookz/web'; +import {findLatestApr} from '@vaults/components/details/tabs/findLatestApr'; +import {GraphForStrategyReports} from '@vaults/components/graphs/GraphForStrategyReports'; +import {yDaemonReportsSchema} from '@vaults/schemas/reportsSchema'; +import {Renderable} from '@yearn-finance/web-lib/components/Renderable'; +import {IconCopy} from '@yearn-finance/web-lib/icons/IconCopy'; +import {toAddress} from '@yearn-finance/web-lib/utils/address'; +import {formatToNormalizedValue, toBigInt} from '@yearn-finance/web-lib/utils/format.bigNumber'; +import {formatAmount, formatPercent} from '@yearn-finance/web-lib/utils/format.number'; +import {formatDuration} from '@yearn-finance/web-lib/utils/format.time'; +import {copyToClipboard, parseMarkdown} from '@yearn-finance/web-lib/utils/helpers'; +import {SearchBar} from '@common/components/SearchBar'; +import {Switch} from '@common/components/Switch'; +import {useFetch} from '@common/hooks/useFetch'; +import {IconChevron} from '@common/icons/IconChevron'; +import {useYDaemonBaseURI} from '@common/utils/getYDaemonBaseURI'; + +import type {ReactElement} from 'react'; +import type {TYDaemonVault, TYDaemonVaultStrategy} from '@common/schemas/yDaemonVaultsSchemas'; +import type {TYDaemonReports} from '@vaults/schemas/reportsSchema'; + +type TProps = { + currentVault: TYDaemonVault; + strategy: TYDaemonVaultStrategy; +}; + +type TRiskScoreElementProps = { + label: string; + value?: number; +}; + +function RiskScoreElement({label, value}: TRiskScoreElementProps): ReactElement { + return ( +
+

{label}

+

{value}

+
+ ); +} + +function VaultDetailsStrategy({currentVault, strategy}: TProps): ReactElement { + const {yDaemonBaseUri} = useYDaemonBaseURI({chainID: currentVault.chainID}); + const isMounted = useIsMounted(); + + const riskScoreElementsMap = useMemo((): TRiskScoreElementProps[] => { + const {riskDetails} = strategy.risk || {}; + + return [ + {label: 'TVL Impact', value: riskDetails?.TVLImpact}, + {label: 'Audit Score', value: riskDetails?.auditScore}, + {label: 'Code Review Score', value: riskDetails?.codeReviewScore}, + {label: 'Complexity Score', value: riskDetails?.complexityScore}, + {label: 'Longevity Impact', value: riskDetails?.longevityImpact}, + { + label: 'Protocol Safety Score', + value: riskDetails?.protocolSafetyScore + }, + { + label: 'Team Knowledge Score', + value: riskDetails?.teamKnowledgeScore + }, + {label: 'Testing Score', value: riskDetails?.testingScore} + ]; + }, [strategy]); + + const {data: reports} = useFetch({ + endpoint: `${yDaemonBaseUri}/reports/${strategy.address}`, + schema: yDaemonReportsSchema + }); + + const latestApr = useMemo((): number => findLatestApr(reports), [reports]); + const {lastReport} = strategy.details || {}; + const lastReportTime = lastReport ? formatDuration(lastReport * 1000 - new Date().valueOf(), true) : 'N/A'; + + return ( +
+ +
+ {strategy.displayName || strategy.name} +
+
+ +
+
+ +
+
+
+
+

{toAddress(strategy.address)}

+ +
+

+

{`Last report ${lastReportTime}.`}

+
+
+ +
+
+
+
+

{'Capital Allocation'}

+ + {`${formatAmount( + formatToNormalizedValue( + toBigInt(strategy.details?.totalDebt), + currentVault?.decimals + ), + 0, + 0 + )} ${currentVault.token.symbol}`} + +
+ +
+

{'Total Gain'}

+ + {`${formatAmount( + formatToNormalizedValue( + toBigInt(strategy.details?.totalGain) - + toBigInt(strategy.details?.totalLoss), + currentVault?.decimals + ), + 0, + 0 + )} ${currentVault.token.symbol}`} + +
+
+ +
+

{'Risk score'}

+
+ {riskScoreElementsMap.map( + ({label, value}): ReactElement => ( + + ) + )} +
+
+
+
+
+
+

{'APR'}

+ {formatPercent(latestApr, 0)} +
+ +
+

{'Allocation'}

+ + {formatPercent((strategy.details?.debtRatio || 0) / 100, 0)} + +
+ +
+

{'Perfomance fee'}

+ + {formatPercent((strategy.details?.performanceFee || 0) * 100, 0)} + +
+
+ +
+

{'Historical APR'}

+
+ + + +
+
+
+
+
+
+ ); +} + +function isExceptionStrategy(strategy: TYDaemonVaultStrategy): boolean { + // Curve DAO Fee and Bribes Reinvest + return strategy.address.toString() === '0x23724D764d8b3d26852BA20d3Bc2578093d2B022'; +} + +export function VaultDetailsStrategies({currentVault}: {currentVault: TYDaemonVault}): ReactElement { + const [searchValue, set_searchValue] = useState(''); + const [shouldHide0DebtStrategies, set_shouldHide0DebtStrategies] = useState(true); + + const hide0DebtStrategyFilter = (strategy: TYDaemonVaultStrategy): boolean => { + return !shouldHide0DebtStrategies || Number(strategy.details?.totalDebt) > 0 || isExceptionStrategy(strategy); + }; + + const nameSearchFilter = ({name, displayName}: TYDaemonVaultStrategy): boolean => { + return !searchValue || `${name} ${displayName}`.toLowerCase().includes(searchValue); + }; + + const sortedStrategies = useMemo((): TYDaemonVault['strategies'] => { + return (currentVault.strategies || []).sort( + (a, b): number => (b.details?.debtRatio || 0) - (a.details?.debtRatio || 0) + ); + }, [currentVault.strategies]); + + const filteredStrategies = (sortedStrategies || []).filter(hide0DebtStrategyFilter).filter(nameSearchFilter); + + return ( +
+
+
+ { + set_searchValue(value.toLowerCase()); + }} + /> + +
+ {'Hide 0 debt strategies'} + { + set_shouldHide0DebtStrategies((prev): boolean => !prev); + }} + /> +
+
+
+
+ {filteredStrategies.map( + (strategy): ReactElement => ( + + ) + )} +
+
+ ); +} diff --git a/apps/vaults-v3/components/details/tabs/VaultDetailsTabsWrapper.tsx b/apps/vaults-v3/components/details/tabs/VaultDetailsTabsWrapper.tsx new file mode 100755 index 000000000..51ff2efb5 --- /dev/null +++ b/apps/vaults-v3/components/details/tabs/VaultDetailsTabsWrapper.tsx @@ -0,0 +1,261 @@ +import {Fragment, useEffect, useMemo, useState} from 'react'; +import {useRouter} from 'next/router'; +import {Listbox, Transition} from '@headlessui/react'; +import {useIsMounted} from '@react-hookz/web'; +import * as Sentry from '@sentry/nextjs'; +import {VaultDetailsAbout} from '@vaults/components/details/tabs/VaultDetailsAbout'; +import {VaultDetailsHistorical} from '@vaults/components/details/tabs/VaultDetailsHistorical'; +import {VaultDetailsStrategies} from '@vaults/components/details/tabs/VaultDetailsStrategies'; +import {Renderable} from '@yearn-finance/web-lib/components/Renderable'; +import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; +import {IconAddToMetamask} from '@yearn-finance/web-lib/icons/IconAddToMetamask'; +import {IconLinkOut} from '@yearn-finance/web-lib/icons/IconLinkOut'; +import {toAddress} from '@yearn-finance/web-lib/utils/address'; +import {formatToNormalizedValue, toBigInt} from '@yearn-finance/web-lib/utils/format.bigNumber'; +import {formatDate} from '@yearn-finance/web-lib/utils/format.time'; +import {isZero} from '@yearn-finance/web-lib/utils/isZero'; +import {getNetwork} from '@yearn-finance/web-lib/utils/wagmi/utils'; +import {useFetch} from '@common/hooks/useFetch'; +import {IconChevron} from '@common/icons/IconChevron'; +import {yDaemonVaultHarvestsSchema} from '@common/schemas/yDaemonVaultsSchemas'; +import {assert} from '@common/utils/assert'; +import {useYDaemonBaseURI} from '@common/utils/getYDaemonBaseURI'; + +import type {ReactElement} from 'react'; +import type {TYDaemonVault, TYDaemonVaultHarvests} from '@common/schemas/yDaemonVaultsSchemas'; + +type TTabsOptions = { + value: number; + label: string; + slug?: string; +}; +type TTabs = { + selectedAboutTabIndex: number; + set_selectedAboutTabIndex: (arg0: number) => void; +}; + +type TExplorerLinkProps = { + explorerBaseURI?: string; + currentVaultAddress: string; +}; + +function Tabs({selectedAboutTabIndex, set_selectedAboutTabIndex}: TTabs): ReactElement { + const router = useRouter(); + + const tabs: TTabsOptions[] = useMemo( + (): TTabsOptions[] => [ + {value: 0, label: 'About', slug: 'about'}, + {value: 1, label: 'Strategies', slug: 'strategies'}, + {value: 2, label: 'Historical rates', slug: 'historical-rates'} + ], + [] + ); + + useEffect((): void => { + const tab = tabs.find((tab): boolean => tab.slug === router.query.tab); + if (tab?.value) { + set_selectedAboutTabIndex(tab?.value); + } + }, [router.query.tab, set_selectedAboutTabIndex, tabs]); + + return ( + <> + +
+ set_selectedAboutTabIndex(value)}> + {({open}): ReactElement => ( + <> + +
+ {tabs[selectedAboutTabIndex]?.label || 'Menu'} +
+
+ +
+
+ + + {tabs.map( + (tab): ReactElement => ( + + {tab.label} + + ) + )} + + + + )} +
+
+ + ); +} + +function ExplorerLink({explorerBaseURI, currentVaultAddress}: TExplorerLinkProps): ReactElement | null { + const isMounted = useIsMounted(); + + if (!explorerBaseURI || !isMounted()) { + return null; + } + + return ( + + {'Open in explorer'} + + + ); +} + +export function VaultDetailsTabsWrapper({currentVault}: {currentVault: TYDaemonVault}): ReactElement { + const {provider} = useWeb3(); + const {yDaemonBaseUri} = useYDaemonBaseURI({chainID: currentVault.chainID}); + const [selectedAboutTabIndex, set_selectedAboutTabIndex] = useState(0); + + async function onAddTokenToMetamask( + address: string, + symbol: string, + decimals: number, + image: string + ): Promise { + try { + assert(provider, 'Provider is not set'); + const walletClient = await provider.getWalletClient(); + await walletClient.watchAsset({ + type: 'ERC20', + options: { + address: toAddress(address), + decimals: decimals, + symbol: symbol, + image: image + } + }); + } catch (error) { + Sentry.captureException(error); + // Token has not been added to MetaMask. + } + } + + const {data: yDaemonHarvestsData} = useFetch({ + endpoint: `${yDaemonBaseUri}/vaults/harvests/${currentVault.address}`, + schema: yDaemonVaultHarvestsSchema + }); + + const harvestData = useMemo((): {name: string; value: number}[] => { + const _yDaemonHarvestsData = [...(yDaemonHarvestsData || [])].reverse(); + return _yDaemonHarvestsData.map((harvest): {name: string; value: number} => ({ + name: formatDate(Number(harvest.timestamp) * 1000), + value: formatToNormalizedValue(toBigInt(harvest.profit) - toBigInt(harvest.loss), currentVault.decimals) + })); + }, [currentVault.decimals, yDaemonHarvestsData]); + + return ( +
+
+ + +
+ + +
+
+ +
+ + + + + + + + + + + + +
+ ); +} diff --git a/apps/vaults-v3/components/details/tabs/findLatestApr.test.ts b/apps/vaults-v3/components/details/tabs/findLatestApr.test.ts new file mode 100644 index 000000000..0217b4340 --- /dev/null +++ b/apps/vaults-v3/components/details/tabs/findLatestApr.test.ts @@ -0,0 +1,55 @@ +import {describe, expect, it} from 'vitest'; + +import {findLatestApr} from './findLatestApr'; + +describe('findLatestApr', (): void => { + it('should return 0 when an empty array is provided', (): void => { + expect(findLatestApr([])).toBe(0); + }); + + it('should return the correct APR for a single report', (): void => { + const reports = [ + { + timestamp: 1000, + results: [ + { + APR: 0.05 + } + ] + } + ]; + + expect(findLatestApr(reports)).toBe(5); + }); + + it('should return the correct APR for multiple reports', (): void => { + const reports = [ + { + timestamp: 1000, + results: [ + { + APR: 0.05 + } + ] + }, + { + timestamp: 3000, + results: [ + { + APR: 0.1 + } + ] + }, + { + timestamp: 2000, + results: [ + { + APR: 0.07 + } + ] + } + ]; + + expect(findLatestApr(reports)).toBe(10); + }); +}); diff --git a/apps/vaults-v3/components/details/tabs/findLatestApr.ts b/apps/vaults-v3/components/details/tabs/findLatestApr.ts new file mode 100644 index 000000000..a7f07b992 --- /dev/null +++ b/apps/vaults-v3/components/details/tabs/findLatestApr.ts @@ -0,0 +1,19 @@ +import {isZero} from '@yearn-finance/web-lib/utils/isZero'; + +import type {TYDaemonReport, TYDaemonReports} from '@vaults/schemas/reportsSchema'; + +export function findLatestApr(reports?: TYDaemonReports): number { + if (!reports?.length) { + return 0; + } + + const latestReport = reports.reduce((prev, curr): TYDaemonReport => { + return prev.timestamp > curr.timestamp ? prev : curr; + }); + + if (!latestReport.results || isZero(latestReport.results.length)) { + return 0; + } + + return latestReport.results[0].APR * 100; +} diff --git a/apps/vaults-v3/components/graphs/GraphForStrategyReports.tsx b/apps/vaults-v3/components/graphs/GraphForStrategyReports.tsx new file mode 100755 index 000000000..2da946334 --- /dev/null +++ b/apps/vaults-v3/components/graphs/GraphForStrategyReports.tsx @@ -0,0 +1,148 @@ +import {Fragment, useMemo} from 'react'; +import {Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis} from 'recharts'; +import {yDaemonReportsSchema} from '@vaults/schemas/reportsSchema'; +import {formatToNormalizedValue, toBigInt} from '@yearn-finance/web-lib/utils/format.bigNumber'; +import {formatAmount, formatPercent} from '@yearn-finance/web-lib/utils/format.number'; +import {formatDate} from '@yearn-finance/web-lib/utils/format.time'; +import {isZero} from '@yearn-finance/web-lib/utils/isZero'; +import {useFetch} from '@common/hooks/useFetch'; +import {useYDaemonBaseURI} from '@common/utils/getYDaemonBaseURI'; + +import type {ReactElement} from 'react'; +import type {TYDaemonVaultStrategy} from '@common/schemas/yDaemonVaultsSchemas'; +import type {TYDaemonReport, TYDaemonReports} from '@vaults/schemas/reportsSchema'; + +export type TGraphForStrategyReportsProps = { + strategy: TYDaemonVaultStrategy; + vaultChainID: number; + vaultDecimals: number; + vaultTicker: string; + height?: number; +}; + +export function GraphForStrategyReports({ + strategy, + vaultChainID, + vaultDecimals, + vaultTicker, + height = 127 +}: TGraphForStrategyReportsProps): ReactElement { + const {yDaemonBaseUri} = useYDaemonBaseURI({chainID: vaultChainID}); + + const {data: reports} = useFetch({ + endpoint: `${yDaemonBaseUri}/reports/${strategy.address}`, + schema: yDaemonReportsSchema + }); + + const strategyData = useMemo((): { + name: number; + value: number; + gain: string; + loss: string; + }[] => { + const _reports = [...(reports || [])]; + const reportsForGraph = _reports.reverse()?.map( + ( + reports: TYDaemonReport + ): { + name: number; + value: number; + gain: string; + loss: string; + } => ({ + name: Number(reports.timestamp), + value: Number(reports.results?.[0]?.APR || 0) * 100, + gain: reports?.gain || '0', + loss: reports?.loss || '0' + }) + ); + return reportsForGraph; + }, [reports]); + + if (!strategyData || isZero(strategyData?.length)) { + return ; + } + + return ( + + + { + e.className = `${e.className} activeDot`; + delete e.dataKey; + return ; + }} + /> + + { + const { + payload: {value} + } = e; + e.fill = '#5B5B5B'; + e.className = 'text-xxs md:text-xs font-number z-10 '; + e.alignmentBaseline = 'middle'; + delete e.verticalAnchor; + delete e.visibleTicksCount; + delete e.tickFormatter; + const formatedValue = formatPercent(value); + return {formatedValue}; + }} + /> + { + const {active: isTooltipActive, payload, label} = e; + if (!isTooltipActive || !payload) { + return <>; + } + if (payload.length > 0) { + const [{value, payload: innerPayload}] = payload; + const {gain, loss} = innerPayload; + const diff = toBigInt(gain) - toBigInt(loss); + const normalizedDiff = formatToNormalizedValue(diff, vaultDecimals); + + return ( +
+
+

{formatDate(label)}

+
+
+

{'APR'}

+ + {formatPercent(Number(value))} + +
+
+

+ {normalizedDiff > 0 ? 'Gain' : 'Loss'} +

+ {`${formatAmount(normalizedDiff)} ${vaultTicker}`} +
+
+ ); + } + return
; + }} + /> + + + ); +} diff --git a/apps/vaults-v3/components/graphs/GraphForVaultEarnings.tsx b/apps/vaults-v3/components/graphs/GraphForVaultEarnings.tsx new file mode 100755 index 000000000..a8b1ae1de --- /dev/null +++ b/apps/vaults-v3/components/graphs/GraphForVaultEarnings.tsx @@ -0,0 +1,112 @@ +import {Fragment, useMemo} from 'react'; +import {Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis} from 'recharts'; +import {formatAmount, formatWithUnit} from '@yearn-finance/web-lib/utils/format.number'; +import {isZero} from '@yearn-finance/web-lib/utils/isZero'; + +import type {ReactElement} from 'react'; +import type {TYDaemonVault} from '@common/schemas/yDaemonVaultsSchemas'; +import type {TGraphData} from '@common/types/types'; + +export type TGraphForVaultEarningsProps = { + currentVault: TYDaemonVault; + harvestData: TGraphData[]; + height?: number; + isCumulative?: boolean; +}; + +export function GraphForVaultEarnings({ + currentVault, + harvestData, + height = 312, + isCumulative = true +}: TGraphForVaultEarningsProps): ReactElement { + const cumulativeData = useMemo((): {name: string; value: number}[] => { + let cumulativeValue = 0; + return harvestData.map((item: {name: string; value: number}): {name: string; value: number} => { + cumulativeValue += item.value; + return { + name: item.name, + value: cumulativeValue + }; + }); + }, [harvestData]); + + if (isCumulative && isZero(cumulativeData?.length)) { + return ; + } + if (!isCumulative && isZero(harvestData?.length)) { + return ; + } + return ( + + + { + e.className = `${e.className} activeDot`; + delete e.dataKey; + return ; + }} + strokeWidth={2} + dataKey={'value'} + stroke={'currentcolor'} + /> + + { + const { + payload: {value} + } = e; + e.fill = '#5B5B5B'; + e.className = 'text-xxs md:text-xs font-number'; + e.alignmentBaseline = 'middle'; + delete e.verticalAnchor; + delete e.visibleTicksCount; + delete e.tickFormatter; + const formatedValue = formatWithUnit(value, 0, 0); + return {formatedValue}; + }} + /> + { + const {active: isTooltipActive, payload, label} = e; + if (!isTooltipActive || !payload) { + return <>; + } + if (payload.length > 0) { + const [{value}] = payload; + + return ( +
+
+

{label}

+
+
+

{'Earnings'}

+ {`${formatAmount(Number(value))} ${currentVault.token.symbol}`} +
+
+ ); + } + return
; + }} + /> + + + ); +} diff --git a/apps/vaults-v3/components/graphs/GraphForVaultPPSGrowth.tsx b/apps/vaults-v3/components/graphs/GraphForVaultPPSGrowth.tsx new file mode 100755 index 000000000..ed0e7e833 --- /dev/null +++ b/apps/vaults-v3/components/graphs/GraphForVaultPPSGrowth.tsx @@ -0,0 +1,90 @@ +import {Fragment} from 'react'; +import {Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis} from 'recharts'; +import {formatAmount, formatPercent} from '@yearn-finance/web-lib/utils/format.number'; +import {isZero} from '@yearn-finance/web-lib/utils/isZero'; + +import type {ReactElement} from 'react'; +import type {TMessariGraphData} from '@common/types/types'; + +export type TGraphForVaultPPSGrowthProps = { + messariData: TMessariGraphData[]; + height?: number; +}; + +export function GraphForVaultPPSGrowth({messariData, height = 312}: TGraphForVaultPPSGrowthProps): ReactElement { + if (isZero(messariData?.length)) { + return ; + } + + return ( + + + { + e.className = `${e.className} activeDot`; + delete e.dataKey; + return ; + }} + /> + + { + const { + payload: {value} + } = e; + e.fill = '#5B5B5B'; + e.className = 'text-xxs md:text-xs font-number'; + e.alignmentBaseline = 'middle'; + delete e.verticalAnchor; + delete e.visibleTicksCount; + delete e.tickFormatter; + const formatedValue = formatAmount(value, 3, 3); + return {formatedValue}; + }} + /> + { + const {active: isTooltipActive, payload, label} = e; + if (!isTooltipActive || !payload) { + return <>; + } + if (payload.length > 0) { + const [{value}] = payload; + + return ( +
+
+

{label}

+
+
+

{'Growth'}

+ + {formatPercent((Number(value) - 1) * 100, 4, 4)} + +
+
+ ); + } + return
; + }} + /> + + + ); +} diff --git a/apps/vaults-v3/components/graphs/GraphForVaultTVL.tsx b/apps/vaults-v3/components/graphs/GraphForVaultTVL.tsx new file mode 100755 index 000000000..6a8af85df --- /dev/null +++ b/apps/vaults-v3/components/graphs/GraphForVaultTVL.tsx @@ -0,0 +1,91 @@ +import {Fragment} from 'react'; +import {Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis} from 'recharts'; +import {formatAmount, formatWithUnit} from '@yearn-finance/web-lib/utils/format.number'; +import {isZero} from '@yearn-finance/web-lib/utils/isZero'; + +import type {ReactElement} from 'react'; +import type {TMessariGraphData} from '@common/types/types'; + +export type TGraphForVaultTVLProps = { + messariData: TMessariGraphData[]; + height?: number; +}; + +export function GraphForVaultTVL({messariData, height = 312}: TGraphForVaultTVLProps): ReactElement { + if (isZero(messariData?.length)) { + return ; + } + + return ( + + + { + e.className = `${e.className} activeDot`; + delete e.dataKey; + return ; + }} + /> + + { + const { + payload: {value} + } = e; + e.fill = '#5B5B5B'; + e.className = 'text-xxs md:text-xs font-number'; + e.alignmentBaseline = 'middle'; + delete e.verticalAnchor; + delete e.visibleTicksCount; + delete e.tickFormatter; + const formatedValue = formatWithUnit(value, 0, 0); + return {formatedValue}; + }} + /> + { + const {active: isTooltipActive, payload, label} = e; + if (!isTooltipActive || !payload) { + return <>; + } + if (payload.length > 0) { + const [{value}] = payload; + + return ( +
+
+

{label}

+
+
+

{'TVL'}

+ {`${formatAmount(Number(value))} $`} +
+
+ ); + } + return
; + }} + /> + + + ); +} diff --git a/apps/vaults-v3/components/list/VaultListFactory.tsx b/apps/vaults-v3/components/list/VaultListFactory.tsx new file mode 100644 index 000000000..8f0878f05 --- /dev/null +++ b/apps/vaults-v3/components/list/VaultListFactory.tsx @@ -0,0 +1,196 @@ +import {useCallback, useMemo, useState} from 'react'; +import {VaultListOptions} from '@vaults/components/list/VaultListOptions'; +import {VaultsListEmptyFactory} from '@vaults/components/list/VaultsListEmpty'; +import {VaultsListRow} from '@vaults/components/list/VaultsListRow'; +import {useAppSettings} from '@vaults/contexts/useAppSettings'; +import {useFilteredVaults} from '@vaults/hooks/useFilteredVaults'; +import {useSortVaults} from '@vaults/hooks/useSortVaults'; +import {isZero} from '@yearn-finance/web-lib/utils/isZero'; +import {performBatchedUpdates} from '@yearn-finance/web-lib/utils/performBatchedUpdates'; +import {ListHead} from '@common/components/ListHead'; +import {ListHero} from '@common/components/ListHero'; +import {useWallet} from '@common/contexts/useWallet'; +import {useYearn} from '@common/contexts/useYearn'; +import {isAutomatedVault, type TYDaemonVaults} from '@common/schemas/yDaemonVaultsSchemas'; +import {getVaultName} from '@common/utils'; + +import type {ReactElement, ReactNode} from 'react'; +import type {TSortDirection} from '@common/types/types'; +import type {TPossibleSortBy} from '@vaults/hooks/useSortVaults'; + +export function VaultListFactory(): ReactElement { + const {getToken} = useWallet(); + const {vaults, isLoadingVaultList} = useYearn(); + const [sortBy, set_sortBy] = useState('apr'); + const [sortDirection, set_sortDirection] = useState(''); + const {shouldHideLowTVLVaults, shouldHideDust, searchValue, set_searchValue} = useAppSettings(); + const [category, set_category] = useState('Curve Factory Vaults'); + + /* 🔵 - Yearn Finance ************************************************************************** + ** It's best to memorize the filtered vaults, which saves a lot of processing time by only + ** performing the filtering once. + **********************************************************************************************/ + const curveVaults = useFilteredVaults( + vaults, + (vault): boolean => vault.category === 'Curve' && isAutomatedVault(vault) + ); + const holdingsVaults = useFilteredVaults(vaults, (vault): boolean => { + const {category, address, chainID} = vault; + const holding = getToken({address, chainID}); + const hasValidBalance = holding.balance.raw > 0n; + const balanceValue = holding.value || 0; + if (shouldHideDust && balanceValue < 0.01) { + return false; + } + if (hasValidBalance && category === 'Curve' && isAutomatedVault(vault)) { + return true; + } + return false; + }); + + /* 🔵 - Yearn Finance ************************************************************************** + ** First, we need to determine in which category we are. The vaultsToDisplay function will + ** decide which vaults to display based on the category. No extra filters are applied. + ** The possible lists are memoized to avoid unnecessary re-renders. + **********************************************************************************************/ + const vaultsToDisplay = useMemo((): TYDaemonVaults => { + let _vaultList: TYDaemonVaults = [...Object.values(vaults || {})]; + + if (category === 'Curve Factory Vaults') { + _vaultList = curveVaults; + } else if (category === 'Holdings') { + _vaultList = holdingsVaults; + } + + if (shouldHideLowTVLVaults && category !== 'Holdings') { + _vaultList = _vaultList.filter((vault): boolean => vault.tvl.tvl > 10_000); + } + + return _vaultList; + }, [category, curveVaults, holdingsVaults, shouldHideLowTVLVaults, vaults]); + + /* 🔵 - Yearn Finance ************************************************************************** + ** Then, on the vaultsToDisplay list, we apply the search filter. The search filter is + ** implemented as a simple string.includes() on the vault name. + **********************************************************************************************/ + const searchedVaults = useMemo((): TYDaemonVaults => { + const vaultsToUse = [...vaultsToDisplay]; + + if (searchValue === '') { + return vaultsToUse; + } + return vaultsToUse.filter((vault): boolean => { + const searchString = getVaultName(vault); + return searchString.toLowerCase().includes(searchValue.toLowerCase()); + }); + }, [vaultsToDisplay, searchValue]); + + /* 🔵 - Yearn Finance ************************************************************************** + ** Then, once we have reduced the list of vaults to display, we can sort them. The sorting + ** is done via a custom method that will sort the vaults based on the sortBy and + ** sortDirection values. + **********************************************************************************************/ + const sortedVaultsToDisplay = useSortVaults([...searchedVaults], sortBy, sortDirection); + + /* 🔵 - Yearn Finance ************************************************************************** + ** Callback method used to sort the vaults list. + ** The use of useCallback() is to prevent the method from being re-created on every render. + **********************************************************************************************/ + const onSort = useCallback((newSortBy: string, newSortDirection: string): void => { + performBatchedUpdates((): void => { + set_sortBy(newSortBy as TPossibleSortBy); + set_sortDirection(newSortDirection as TSortDirection); + }); + }, []); + + /* 🔵 - Yearn Finance ************************************************************************** + ** The VaultList component is memoized to prevent it from being re-created on every render. + ** It contains either the list of vaults, is some are available, or a message to the user. + **********************************************************************************************/ + const VaultList = useMemo((): ReactNode => { + if (isLoadingVaultList || isZero(sortedVaultsToDisplay.length)) { + return ( + + ); + } + return sortedVaultsToDisplay.map((vault): ReactNode => { + if (!vault) { + return null; + } + return ( + + ); + }); + }, [category, isLoadingVaultList, sortedVaultsToDisplay]); + + return ( +
+
+ +
+ + + + + {VaultList} +
+ ); +} diff --git a/apps/vaults-v3/components/list/VaultListOptions.tsx b/apps/vaults-v3/components/list/VaultListOptions.tsx new file mode 100644 index 000000000..ab49b5463 --- /dev/null +++ b/apps/vaults-v3/components/list/VaultListOptions.tsx @@ -0,0 +1,62 @@ +import {Fragment} from 'react'; +import {Popover, Transition} from '@headlessui/react'; +import {useAppSettings} from '@vaults/contexts/useAppSettings'; +import {IconSettings} from '@yearn-finance/web-lib/icons/IconSettings'; +import {Switch} from '@common/components/Switch'; + +import type {ReactElement} from 'react'; + +export function VaultListOptions(): ReactElement { + const {shouldHideDust, onSwitchHideDust, shouldHideLowTVLVaults, onSwitchHideLowTVLVaults} = useAppSettings(); + + return ( + + {(): ReactElement => ( + <> + + + + + +
+
+ + + +
+
+
+
+ + )} +
+ ); +} diff --git a/apps/vaults-v3/components/list/VaultsListEmpty.tsx b/apps/vaults-v3/components/list/VaultsListEmpty.tsx new file mode 100755 index 000000000..979ce0e64 --- /dev/null +++ b/apps/vaults-v3/components/list/VaultsListEmpty.tsx @@ -0,0 +1,161 @@ +import {ALL_CATEGORIES_KEYS, ALL_CHAINS} from '@vaults/constants'; +import {Button} from '@yearn-finance/web-lib/components/Button'; +import {isZero} from '@yearn-finance/web-lib/utils/isZero'; + +import type {ReactElement} from 'react'; +import type {TYDaemonVaults} from '@common/schemas/yDaemonVaultsSchemas'; + +type TVaultListEmpty = { + sortedVaultsToDisplay: TYDaemonVaults; + currentSearch: string; + currentCategories: string[]; + currentChains: number[]; + onChangeCategories: (value: string[]) => void; + onChangeChains: (value: number[]) => void; + isLoading: boolean; +}; +export function VaultsListEmpty({ + sortedVaultsToDisplay, + currentSearch, + currentCategories, + currentChains, + onChangeCategories, + onChangeChains, + isLoading +}: TVaultListEmpty): ReactElement { + if (isLoading && isZero(sortedVaultsToDisplay.length)) { + return ( +
+ {'Fetching Vaults'} +

{'Vaults will appear soon. Please wait. Beep boop.'}

+
+ +
+
+ ); + } + + if ( + !isLoading && + isZero(sortedVaultsToDisplay.length) && + currentCategories.length === 1 && + currentCategories.includes('holdings') + ) { + return ( +
+ {'Well this is awkward...'} +

+ {"You don't appear to have any deposits in our Vaults. There's an easy way to change that 😏"} +

+
+ ); + } + + if (!isLoading && isZero(sortedVaultsToDisplay.length)) { + return ( +
+ {'No data, reeeeeeeeeeee'} + {currentCategories.length === ALL_CATEGORIES_KEYS.length ? ( +

{`The vault "${currentSearch}" does not exist`}

+ ) : ( + <> +

+ {`There doesn’t seem to be anything here. It might be because you of your filters, or because there’s a rodent infestation in our server room. You check the filters, we’ll check the rodents. Deal?`} +

+ + + )} +
+ ); + } + if (!isLoading && currentChains.length === 0) { + return ( +
+ {'No data, reeeeeeeeeeee'} + <> +

{`Please, select a chain. At least one, just one.`}

+ + +
+ ); + } + return
; +} + +export function VaultListEmptyExternalMigration(): ReactElement { + return ( +
+ {'We looked under the cushions...'} +

+ { + "Looks like you don't have any tokens to migrate. That could mean that you're already earning the best risk-adjusted yields in DeFi (go you), or you don't have any vault tokens at all. In which case... you know what to do." + } +

+
+ ); +} + +export function VaultsListEmptyFactory({ + sortedVaultsToDisplay, + currentCategories, + isLoading +}: { + sortedVaultsToDisplay: TYDaemonVaults; + currentCategories: string; + isLoading: boolean; +}): ReactElement { + if (isLoading && isZero(sortedVaultsToDisplay.length)) { + return ( +
+ {'Fetching Vaults'} +

{'Vaults will appear soon. Please wait. Beep boop.'}

+
+ +
+
+ ); + } + + if (!isLoading && isZero(sortedVaultsToDisplay.length) && currentCategories === 'Holdings') { + return ( +
+ {'Well this is awkward...'} +

+ { + "You don't appear to have any deposits in our Factory Vaults. There's an easy way to change that 😏" + } +

+
+ ); + } + if (!isLoading && isZero(sortedVaultsToDisplay.length)) { + return ( +
+ {'No data, reeeeeeeeeeee'} +

+ { + 'There doesn’t seem to be anything here. It might be because you searched for a token in the wrong category, or because there’s a rodent infestation in our server room. You check the search box, we’ll check the rodents. Deal?' + } +

+
+ ); + } + return
; +} diff --git a/apps/vaults-v3/components/list/VaultsListInternalMigrationRow.tsx b/apps/vaults-v3/components/list/VaultsListInternalMigrationRow.tsx new file mode 100755 index 000000000..5175a03a8 --- /dev/null +++ b/apps/vaults-v3/components/list/VaultsListInternalMigrationRow.tsx @@ -0,0 +1,63 @@ +import {useMemo} from 'react'; +import Link from 'next/link'; +import {toAddress} from '@yearn-finance/web-lib/utils/address'; +import {formatAmount} from '@yearn-finance/web-lib/utils/format.number'; +import TokenIcon from '@common/components/TokenIcon'; +import {useBalance} from '@common/hooks/useBalance'; +import {getVaultName} from '@common/utils'; + +import type {ReactElement} from 'react'; +import type {TYDaemonVault} from '@common/schemas/yDaemonVaultsSchemas'; + +export function VaultsListInternalMigrationRow({currentVault}: {currentVault: TYDaemonVault}): ReactElement { + const vaultName = useMemo((): string => getVaultName(currentVault), [currentVault]); + const balanceToMigrate = useBalance({address: currentVault.address, chainID: currentVault.chainID}); + + return ( + +
+
+
+
+ +
+
+

{vaultName}

+

{`${formatAmount(balanceToMigrate.normalized)} ${ + currentVault.token.symbol + }`}

+
+
+
+ +
+
+ { + "Looks like you're holding tokens from a previous version of this vault. To keep earning yield on your assets, migrate to the current vault." + } +
+ +
+ +
+
+
+ + ); +} diff --git a/apps/vaults-v3/components/list/VaultsListRetired.tsx b/apps/vaults-v3/components/list/VaultsListRetired.tsx new file mode 100755 index 000000000..2f650f8d4 --- /dev/null +++ b/apps/vaults-v3/components/list/VaultsListRetired.tsx @@ -0,0 +1,64 @@ +import {useMemo} from 'react'; +import Link from 'next/link'; +import {toAddress} from '@yearn-finance/web-lib/utils/address'; +import {formatAmount} from '@yearn-finance/web-lib/utils/format.number'; +import TokenIcon from '@common/components/TokenIcon'; +import {useBalance} from '@common/hooks/useBalance'; +import {getVaultName} from '@common/utils'; + +import type {ReactElement} from 'react'; +import type {TYDaemonVault} from '@common/schemas/yDaemonVaultsSchemas'; + +export function VaultsListRetired({currentVault}: {currentVault: TYDaemonVault}): ReactElement { + const vaultName = useMemo((): string => getVaultName(currentVault), [currentVault]); + const balanceToMigrate = useBalance({address: currentVault.address, chainID: currentVault.chainID}); + + return ( + +
+
+
+
+ +
+
+

{vaultName}

+

{`${formatAmount(balanceToMigrate.normalized)} ${ + currentVault.token.symbol + }`}

+
+
+
+ +
+
+ {'This Vault is no longer supported. '} + { + 'Sadly this vault is deprecated and will no longer earn yield. Please withdraw your funds (many other Vaults await you anon).' + } +
+ +
+ +
+
+
+ + ); +} diff --git a/apps/vaults-v3/components/list/VaultsListRow.tsx b/apps/vaults-v3/components/list/VaultsListRow.tsx new file mode 100755 index 000000000..4928d3fe5 --- /dev/null +++ b/apps/vaults-v3/components/list/VaultsListRow.tsx @@ -0,0 +1,454 @@ +import {useMemo} from 'react'; +import Link from 'next/link'; +import {Renderable} from '@yearn-finance/web-lib/components/Renderable'; +import {toAddress} from '@yearn-finance/web-lib/utils/address'; +import {ETH_TOKEN_ADDRESS, WETH_TOKEN_ADDRESS, WFTM_TOKEN_ADDRESS} from '@yearn-finance/web-lib/utils/constants'; +import {formatAmount} from '@yearn-finance/web-lib/utils/format.number'; +import {isZero} from '@yearn-finance/web-lib/utils/isZero'; +import {ImageWithFallback} from '@common/components/ImageWithFallback'; +import {RenderAmount} from '@common/components/RenderAmount'; +import {useWallet} from '@common/contexts/useWallet'; +import {useBalance} from '@common/hooks/useBalance'; +import {getVaultName} from '@common/utils'; + +import type {ReactElement} from 'react'; +import type {TYDaemonVault} from '@common/schemas/yDaemonVaultsSchemas'; + +export function VaultForwardAPR({currentVault}: {currentVault: TYDaemonVault}): ReactElement { + const isEthMainnet = currentVault.chainID === 1; + if (currentVault.apr.forwardAPR.type === '') { + const hasZeroAPR = isZero(currentVault.apr?.netAPR) || Number(currentVault.apr?.netAPR.toFixed(2)) === 0; + const boostedAPR = currentVault.apr.extra.stakingRewardsAPR + currentVault.apr.netAPR; + const hasZeroBoostedAPR = isZero(boostedAPR) || Number(boostedAPR.toFixed(2)) === 0; + + if (currentVault.apr?.extra.stakingRewardsAPR > 0) { + return ( +
+ + + + {'⚡️ '} + + + + + + +
+
+
+

{'• Base APR '}

+ +
+ +
+

{'• Rewards APR '}

+ +
+
+
+
+
+
+ ); + } + return ( +
+ + + + + +
+ ); + } + + if (isEthMainnet && currentVault.apr.forwardAPR.composite?.boost > 0 && !currentVault.apr.extra.stakingRewardsAPR) { + const unBoostedAPR = currentVault.apr.forwardAPR.netAPR / currentVault.apr.forwardAPR.composite.boost; + return ( + +
+ + + + + + + 0 && + !currentVault.apr.extra.stakingRewardsAPR + }> + {`BOOST ${formatAmount(currentVault.apr.forwardAPR.composite?.boost, 2, 2)}x`} + + + +
+
+
+

{'• Base APR '}

+ +
+ +
+

{'• Boost '}

+

{`${formatAmount(currentVault.apr.forwardAPR.composite.boost, 2, 2)} x`}

+
+
+
+
+
+
+ ); + } + + if (currentVault.apr?.extra.stakingRewardsAPR > 0) { + const boostedAPR = currentVault.apr.extra.stakingRewardsAPR + currentVault.apr.forwardAPR.netAPR; + const hasZeroBoostedAPR = isZero(boostedAPR) || Number(boostedAPR.toFixed(2)) === 0; + return ( +
+ + + + {'⚡️ '} + + + + + + +
+
+
+

{'• Base APR '}

+ +
+ +
+

{'• Rewards APR '}

+ +
+
+
+
+
+
+ ); + } + + const hasZeroAPR = isZero(currentVault.apr?.netAPR) || Number(currentVault.apr?.netAPR.toFixed(2)) === 0; + return ( +
+ + + + + +
+ ); +} + +export function VaultHistoricalAPR({currentVault}: {currentVault: TYDaemonVault}): ReactElement { + const hasZeroAPR = isZero(currentVault.apr?.netAPR) || Number(currentVault.apr?.netAPR.toFixed(2)) === 0; + + if (currentVault.apr?.extra.stakingRewardsAPR > 0) { + return ( +
+ + + + {'⚡️ '} + + + + + + +
+
+
+

{'• Base APR '}

+ +
+ +
+

{'• Rewards APR '}

+

{'N/A'}

+
+
+
+
+
+
+ ); + } + + return ( +
+ + + + + +
+ ); +} + +export function VaultsListRow({currentVault}: {currentVault: TYDaemonVault}): ReactElement { + const {getToken} = useWallet(); + const balanceOfWant = useBalance({chainID: currentVault.chainID, address: currentVault.token.address}); + const balanceOfCoin = useBalance({chainID: currentVault.chainID, address: ETH_TOKEN_ADDRESS}); + const balanceOfWrappedCoin = useBalance({ + chainID: currentVault.chainID, + address: toAddress(currentVault.token.address) === WFTM_TOKEN_ADDRESS ? WFTM_TOKEN_ADDRESS : WETH_TOKEN_ADDRESS //TODO: Create a wagmi Chain upgrade to add the chain wrapper token address + }); + const vaultName = useMemo((): string => getVaultName(currentVault), [currentVault]); + + const availableToDeposit = useMemo((): bigint => { + if (toAddress(currentVault.token.address) === WETH_TOKEN_ADDRESS) { + // Handle ETH native coin + return balanceOfWrappedCoin.raw + balanceOfCoin.raw; + } + if (toAddress(currentVault.token.address) === WFTM_TOKEN_ADDRESS) { + // Handle FTM native coin + return balanceOfWrappedCoin.raw + balanceOfCoin.raw; + } + return balanceOfWant.raw; + }, [balanceOfCoin.raw, balanceOfWant.raw, balanceOfWrappedCoin.raw, currentVault.token.address]); + + const staked = useMemo((): bigint => { + const token = getToken({chainID: currentVault.chainID, address: currentVault.address}); + const depositedAndStaked = token.balance.raw + token.stakingBalance.raw; + return depositedAndStaked; + }, [currentVault.address, currentVault.chainID, getToken]); + + return ( + +
+
+ +
+
+
+
+ +
+

{vaultName}

+
+
+ +
+
+ + +
+ +
+ + +
+ +
+ +

+ +

+
+ +
+ +

+ +

+
+ +
+ +

+ +

+
+
+
+ + ); +} diff --git a/apps/vaults-v3/constants/index.ts b/apps/vaults-v3/constants/index.ts new file mode 100644 index 000000000..40ddf9485 --- /dev/null +++ b/apps/vaults-v3/constants/index.ts @@ -0,0 +1,15 @@ +export const YFACTORY_SUPPORTED_NETWORK = 1; +export const OPT_STAKING_REWARD_SUPPORTED_NETWORK = 10; + +export const ALL_CATEGORIES = { + holdings: 'Holdings', + crypto: 'Crypto Vaults', + stables: 'Stables Vaults', + curve: 'Curve Vaults', + // balancer: 'Balancer Vaults', + boosted: 'Boosted Vaults', + velodrome: 'Velodrome Vaults', + aerodrome: 'Aerodrome Vaults' +}; +export const ALL_CATEGORIES_KEYS = Object.keys(ALL_CATEGORIES); +export const ALL_CHAINS = [1, 10, 250, 8453, 42161]; diff --git a/apps/vaults-v3/constants/menu.ts b/apps/vaults-v3/constants/menu.ts new file mode 100644 index 000000000..b6820faaa --- /dev/null +++ b/apps/vaults-v3/constants/menu.ts @@ -0,0 +1,4 @@ +export const VAULTS_V3_MENU = [ + {path: '/vaults', label: 'Vaults'}, + {path: '/vaults/about', label: 'About'} +]; diff --git a/apps/vaults-v3/constants/optRewards.ts b/apps/vaults-v3/constants/optRewards.ts new file mode 100644 index 000000000..2a48fb9c1 --- /dev/null +++ b/apps/vaults-v3/constants/optRewards.ts @@ -0,0 +1,233 @@ +import {toAddress} from '@yearn-finance/web-lib/utils/address'; + +import type {TAddress, TDict} from '@yearn-finance/web-lib/types'; + +export const OPT_YVETH_WITH_REWARDS = toAddress('0x5B977577Eb8a480f63e11FC615D6753adB8652Ae'); +export const OPT_YVDAI_WITH_REWARDS = toAddress('0x65343F414FFD6c97b0f6add33d16F6845Ac22BAc'); +export const OPT_YVUSDT_WITH_REWARDS = toAddress('0xFaee21D0f0Af88EE72BB6d68E54a90E6EC2616de'); +export const OPT_YVUSDC_WITH_REWARDS = toAddress('0xaD17A225074191d5c8a37B50FdA1AE278a2EE6A2'); +export const OPT_YVSUSCUSDC_WITH_REWARDS = toAddress('0x161E7411A291ccaf377279fCe6a8C8D689dBeBe8'); +export const OPT_YVDOLAUSDC_WITH_REWARDS = toAddress('0x22687Ca792A8Cb5E169a77E0949e71Fe37147604'); +export const OPT_YVMAIUSDC_WITH_REWARDS = toAddress('0x682dF61222A3FB64F3e88fEf41cE21499aDcc647'); +export const OPT_YVMAI_WITH_REWARDS = toAddress('0x6759574667896a453304897993D61F00FdC7214d'); +export const OPT_YVMAI_USDC_WITH_REWARDS = toAddress('0x1d49ab5b03268E329fc7d2111E5364e63b36a3cE'); +export const OPT_YVMAI_DOLA_WITH_REWARDS = toAddress('0x68f2B1db2DA6204489279a8fC681f83728bB9622'); +export const OPT_YVLDO_WSTETH_WITH_REWARDS = toAddress('0x507343bAD3a1ee0B67C3B1712133290DED43980D'); +export const OPT_YVWUSDR_USDC_WITH_REWARDS = toAddress('0x531fDb1C8429F404116932226D487db94471C4b7'); +export const OPT_YVVELO_USDC_WITH_REWARDS = toAddress('0x88B429359665B8685b7431f5A0f6e8C719Bf49Aa'); +export const OPT_YVMAI_ALUSD_WITH_REWARDS = toAddress('0xD02Ce9E211279ab3f414ff7A661518f9173780CB'); +export const OPT_YVALUSD_FRAX_WITH_REWARDS = toAddress('0x9056b19Fde674B5Ad8397e7Ed3E25a1BCCEf0C27'); +export const OPT_YVALETH_FRXETH_WITH_REWARDS = toAddress('0x7e1d451799af57EA5570D1015c342C12a918a30b'); +export const OPT_YVALETH_WETH_WITH_REWARDS = toAddress('0xf7D66b41Cd4241eae450fd9D2d6995754634D9f3'); +export const OPT_YVERN_DOLA_WITH_REWARDS = toAddress('0x50032bE2b2c7e8A6a6bc241e3aEbef21497A3cF0'); +export const OPT_YVERN_LUSD_WITH_REWARDS = toAddress('0x7bDEA31F968089f93A548ddec8BB74036E8ac6e0'); +export const OPT_YVLUSD_WETH_WITH_REWARDS = toAddress('0xbC61B71562b01a3a4808D3B9291A3Bf743AB3361'); +export const OPT_YVAGEUR_USDC_WITH_REWARDS = toAddress('0x03894Ca9b0CABfC53dDE7C2DCfcC2A58D6192687'); +export const OPT_YVMIM_USDC_WITH_REWARDS = toAddress('0x950f384f566b82CCA6380f75AE42E2BBE9B357B2'); +export const OPT_YVDOLA_USDC_WITH_REWARDS = toAddress('0x39F60710a27a87332d10E3333c2eFF41EeC911Ee'); +export const OPT_YVOP_USDC_WITH_REWARDS = toAddress('0x3AD9566b15AACDd26d8a220cA8635F925EA7a3f6'); +export const OPT_YVOP_VELO_WITH_REWARDS = toAddress('0xC86B70e2C16C5fB5315e4663737E0E077ABFC395'); +export const OPT_YVSNX_USDC_WITH_REWARDS = toAddress('0x01c77E8a099e97B7b0Dca2F987d9bd1911CdDE50'); +export const OPT_YVFRAX_DOLA_WITH_REWARDS = toAddress('0x3f14Df340498F3EAA2894BFc19789dD592Dc062D'); +export const OPT_YVALUSD_USDC_WITH_REWARDS = toAddress('0x3B141BD7d6E1d67e2101A08E4dd849a8408d91aa'); +export const OPT_YVMTA_USDC_WITH_REWARDS = toAddress('0x906122aC1b55B71248ABFA552fDc6ABd3356884A'); +export const OPT_YVIB_WETH_WITH_REWARDS = toAddress('0x8e8D635E28BB8774FD23107cC1D9b6D30AD5D0B9'); +export const OPT_YVEXA_WETH_WITH_REWARDS = toAddress('0xc3439Ba7db7566ed0deF55c179ED9b3bA273A67F'); +export const OPT_YVTBTC_WETH_WITH_REWARDS = toAddress('0x00a2faF97CC2b985B29a0a0eAa6E0D8033Dc0eA1'); +export const OPT_YVTBTC_WBTC_WITH_REWARDS = toAddress('0x00Cb8E36A9C40491A39e8bF2864Ed30C1B579860'); +export const OPT_YVOP_WETH_WITH_REWARDS = toAddress('0xDdDCAeE873f2D9Df0E18a80709ef2B396d4a6EA5'); +export const OPT_YVWUSDRV2_USDC_WITH_REWARDS = toAddress('0x43360FDd9546e3e77ac2e6793f53219729743293'); +export const OPT_YVSTERN_ERN_WITH_REWARDS = toAddress('0xa7B550B3A80361d8e47E07616dC42f04c655881b'); + +export const OPT_YVETH_STAKING_CONTRACT = toAddress('0xE35Fec3895Dcecc7d2a91e8ae4fF3c0d43ebfFE0'); +export const OPT_YVDAI_STAKING_CONTRACT = toAddress('0xf8126EF025651E1B313a6893Fcf4034F4F4bD2aA'); +export const OPT_YVUSDT_STAKING_CONTRACT = toAddress('0xf66932f225cA48856B7f97b6F060f4c0D244Af8E'); +export const OPT_YVUSDC_STAKING_CONTRACT = toAddress('0xB2c04C55979B6CA7EB10e666933DE5ED84E6876b'); +export const OPT_YVSUSCUSDC_STAKING_CONTRACT = toAddress('0x7f2073aF8F422a94A41df58ed6d9da0ed92c378D'); +export const OPT_YVDOLAUSDC_STAKING_CONTRACT = toAddress('0x692287540111A8a9B3323427e729073d9aaeEe83'); +export const OPT_YVMAIUSDC_STAKING_CONTRACT = toAddress('0xF81Ad33A29c7A85cd9fBE4F3E96dFDe50C7565fF'); +export const OPT_YVMAI_STAKING_CONTRACT = toAddress('0x56D5241A333aa1B3c203d7604dE23c7dD5D4d943'); +export const OPT_YVMAI_USDC_STAKING_CONTRACT = toAddress('0xD7fdB47441721aC006958906E26a3d6384777aE3'); +export const OPT_YVMAI_DOLA_STAKING_CONTRACT = toAddress('0x57c41fd87046ae9ae17fD3BF50FDA1A8b8Ab5CA3'); +export const OPT_YVLDO_WSTETH_STAKING_CONTRACT = toAddress('0x05aEc4BD3446c60eba98F6D8EB5443F25d7f15F6'); +export const OPT_YVWUSDR_USDC_STAKING_CONTRACT = toAddress('0xc7c145e5EC1e811B1149FD4527a23c9Bcab330d0'); +export const OPT_YVVELO_USDC_STAKING_CONTRACT = toAddress('0x7Ca5B3b707475fA977B10Fb4FA217983fc8E32cF'); +export const OPT_YVMAI_ALUSD_STAKING_CONTRACT = toAddress('0x1f895d9E8B0830cB3bbdA739Fa8aFb186b7FBDb4'); +export const OPT_YVALUSD_FRAX_STAKING_CONTRACT = toAddress('0xb666abf97f0a2ea624a6fa0d3fb92121f3fb591b'); +export const OPT_YVALETH_FRXETH_STAKING_CONTRACT = toAddress('0x77A0914310dD8C5ec2813A0deab498AEab2C8f42'); +export const OPT_YVALETH_WETH_STAKING_CONTRACT = toAddress('0xB1494DCaDE9B7678692dEf8Da0129e28A209B026'); +export const OPT_YVERN_DOLA_STAKING_CONTRACT = toAddress('0x1530E5540132Bd8dA96eA6EB5aCe73b98973B0C1'); +export const OPT_YVERN_LUSD_STAKING_CONTRACT = toAddress('0x54fcb3b792e19d9ba07ee61a71e93deb124cb957'); +export const OPT_YVLUSD_WETH_STAKING_CONTRACT = toAddress('0x0E4e9914Ecf0F7177EF999774d46218614555159'); +export const OPT_YVAGEUR_USDC_STAKING_CONTRACT = toAddress('0x3D8a5eB67a7841E00fFaE8f0764a58322ca02c66'); +export const OPT_YVMIM_USDC_STAKING_CONTRACT = toAddress('0x0e54e555714c7EEabC54E7c270fbCa8295BA1fE8'); +export const OPT_YVDOLA_USDC_STAKING_CONTRACT = toAddress('0xb62F6B72d974a5e3253c6d29630F591251CE6B10'); +export const OPT_YVOP_USDC_STAKING_CONTRACT = toAddress('0x182F09A75A8190f111C1762EcC904e8727d6d6d5'); +export const OPT_YVOP_VELO_STAKING_CONTRACT = toAddress('0x454f953ca4A4C6aB3f2dbB158158De55E4195AbC'); +export const OPT_YVSNX_USDC_STAKING_CONTRACT = toAddress('0xd3434Df0cDf7Bb81a94e499895f93dE57288eE9e'); +export const OPT_YVFRAX_DOLA_STAKING_CONTRACT = toAddress('0x8E7aa69D6cBbF34C8F9443BdEe2601052C015529'); +export const OPT_YVALUSD_USDC_STAKING_CONTRACT = toAddress('0x3E9c848f5B352083470e8906a4ec78c601f98cCc'); +export const OPT_YVMTA_USDC_STAKING_CONTRACT = toAddress('0xb9835152A8a0b56094A9C83A2849002a325DAfc1'); +export const OPT_YVIB_WETH_STAKING_CONTRACT = toAddress('0xaF19C8404b5fB377e97F3c1B9eb595b61a42D621'); +export const OPT_YVEXA_WETH_STAKING_CONTRACT = toAddress('0xDa345746Cd1BfCEB2B5e5e35581e2519e0003578'); +export const OPT_YVTBTC_WETH_STAKING_CONTRACT = toAddress('0x3181E64B7d83Ec4240C0dd5aCAB65C0078c3cb3C'); +export const OPT_YVTBTC_WBTC_STAKING_CONTRACT = toAddress('0x9b26e8BD7EBc0177B06e3168410947A5dB6FDb12'); +export const OPT_YVOP_WETH_STAKING_CONTRACT = toAddress('0x885FeDaB0182699eC2f2663F776a04150ed6f7af'); +export const OPT_YVWUSDRV2_USDC_STAKING_CONTRACT = toAddress('0xE00bdf935e4FeB3cB5e0601D88a999d30994605c'); +export const OPT_YVSTERN_ERN_STAKING_CONTRACT = toAddress('0x80C3806ADF50EfAc542DD4B3657F4BE2C30E24b8'); + +export const OPT_VAULTS_WITH_REWARDS = [ + OPT_YVETH_WITH_REWARDS, + OPT_YVDAI_WITH_REWARDS, + OPT_YVUSDT_WITH_REWARDS, + OPT_YVUSDC_WITH_REWARDS, + OPT_YVSUSCUSDC_WITH_REWARDS, + OPT_YVDOLAUSDC_WITH_REWARDS, + OPT_YVMAIUSDC_WITH_REWARDS, + OPT_YVMAI_WITH_REWARDS, + OPT_YVMAI_USDC_WITH_REWARDS, + OPT_YVMAI_DOLA_WITH_REWARDS, + OPT_YVLDO_WSTETH_WITH_REWARDS, + OPT_YVWUSDR_USDC_WITH_REWARDS, + OPT_YVVELO_USDC_WITH_REWARDS, + OPT_YVMAI_ALUSD_WITH_REWARDS, + OPT_YVALUSD_FRAX_WITH_REWARDS, + OPT_YVALETH_FRXETH_WITH_REWARDS, + OPT_YVALETH_WETH_WITH_REWARDS, + OPT_YVERN_DOLA_WITH_REWARDS, + OPT_YVERN_LUSD_WITH_REWARDS, + OPT_YVLUSD_WETH_WITH_REWARDS, + OPT_YVAGEUR_USDC_WITH_REWARDS, + OPT_YVMIM_USDC_WITH_REWARDS, + OPT_YVDOLA_USDC_WITH_REWARDS, + OPT_YVOP_USDC_WITH_REWARDS, + OPT_YVOP_VELO_WITH_REWARDS, + OPT_YVSNX_USDC_WITH_REWARDS, + OPT_YVFRAX_DOLA_WITH_REWARDS, + OPT_YVALUSD_USDC_WITH_REWARDS, + OPT_YVMTA_USDC_WITH_REWARDS, + OPT_YVIB_WETH_WITH_REWARDS, + OPT_YVEXA_WETH_WITH_REWARDS, + OPT_YVTBTC_WETH_WITH_REWARDS, + OPT_YVTBTC_WBTC_WITH_REWARDS, + OPT_YVOP_WETH_WITH_REWARDS, + OPT_YVWUSDRV2_USDC_WITH_REWARDS, + OPT_YVSTERN_ERN_WITH_REWARDS +]; + +export const OPT_REWARDS_TOKENS = [ + OPT_YVETH_STAKING_CONTRACT, + OPT_YVDAI_STAKING_CONTRACT, + OPT_YVUSDT_STAKING_CONTRACT, + OPT_YVUSDC_STAKING_CONTRACT, + OPT_YVSUSCUSDC_STAKING_CONTRACT, + OPT_YVDOLAUSDC_STAKING_CONTRACT, + OPT_YVMAIUSDC_STAKING_CONTRACT, + OPT_YVMAI_STAKING_CONTRACT, + OPT_YVMAI_USDC_STAKING_CONTRACT, + OPT_YVMAI_DOLA_STAKING_CONTRACT, + OPT_YVLDO_WSTETH_STAKING_CONTRACT, + OPT_YVWUSDR_USDC_STAKING_CONTRACT, + OPT_YVVELO_USDC_STAKING_CONTRACT, + OPT_YVMAI_ALUSD_STAKING_CONTRACT, + OPT_YVALUSD_FRAX_STAKING_CONTRACT, + OPT_YVALETH_FRXETH_STAKING_CONTRACT, + OPT_YVALETH_WETH_STAKING_CONTRACT, + OPT_YVERN_DOLA_STAKING_CONTRACT, + OPT_YVERN_LUSD_STAKING_CONTRACT, + OPT_YVLUSD_WETH_STAKING_CONTRACT, + OPT_YVAGEUR_USDC_STAKING_CONTRACT, + OPT_YVMIM_USDC_STAKING_CONTRACT, + OPT_YVDOLA_USDC_STAKING_CONTRACT, + OPT_YVOP_USDC_STAKING_CONTRACT, + OPT_YVOP_VELO_STAKING_CONTRACT, + OPT_YVSNX_USDC_STAKING_CONTRACT, + OPT_YVFRAX_DOLA_STAKING_CONTRACT, + OPT_YVALUSD_USDC_STAKING_CONTRACT, + OPT_YVMTA_USDC_STAKING_CONTRACT, + OPT_YVIB_WETH_STAKING_CONTRACT, + OPT_YVEXA_WETH_STAKING_CONTRACT, + OPT_YVTBTC_WETH_STAKING_CONTRACT, + OPT_YVTBTC_WBTC_STAKING_CONTRACT, + OPT_YVOP_WETH_STAKING_CONTRACT, + OPT_YVWUSDRV2_USDC_STAKING_CONTRACT, + OPT_YVSTERN_ERN_STAKING_CONTRACT +]; + +export const VAULT_TO_STACKING: TDict = { + [OPT_YVETH_STAKING_CONTRACT]: OPT_YVETH_WITH_REWARDS, + [OPT_YVDAI_STAKING_CONTRACT]: OPT_YVDAI_WITH_REWARDS, + [OPT_YVUSDT_STAKING_CONTRACT]: OPT_YVUSDT_WITH_REWARDS, + [OPT_YVUSDC_STAKING_CONTRACT]: OPT_YVUSDC_WITH_REWARDS, + [OPT_YVSUSCUSDC_STAKING_CONTRACT]: OPT_YVSUSCUSDC_WITH_REWARDS, + [OPT_YVDOLAUSDC_STAKING_CONTRACT]: OPT_YVDOLAUSDC_WITH_REWARDS, + [OPT_YVMAIUSDC_STAKING_CONTRACT]: OPT_YVMAIUSDC_WITH_REWARDS, + [OPT_YVMAI_STAKING_CONTRACT]: OPT_YVMAI_WITH_REWARDS, + [OPT_YVMAI_USDC_STAKING_CONTRACT]: OPT_YVMAI_USDC_WITH_REWARDS, + [OPT_YVMAI_DOLA_STAKING_CONTRACT]: OPT_YVMAI_DOLA_WITH_REWARDS, + [OPT_YVLDO_WSTETH_STAKING_CONTRACT]: OPT_YVLDO_WSTETH_WITH_REWARDS, + [OPT_YVWUSDR_USDC_STAKING_CONTRACT]: OPT_YVWUSDR_USDC_WITH_REWARDS, + [OPT_YVVELO_USDC_STAKING_CONTRACT]: OPT_YVVELO_USDC_WITH_REWARDS, + [OPT_YVMAI_ALUSD_STAKING_CONTRACT]: OPT_YVMAI_ALUSD_WITH_REWARDS, + [OPT_YVALUSD_FRAX_STAKING_CONTRACT]: OPT_YVALUSD_FRAX_WITH_REWARDS, + [OPT_YVALETH_FRXETH_STAKING_CONTRACT]: OPT_YVALETH_FRXETH_WITH_REWARDS, + [OPT_YVALETH_WETH_STAKING_CONTRACT]: OPT_YVALETH_WETH_WITH_REWARDS, + [OPT_YVERN_DOLA_STAKING_CONTRACT]: OPT_YVERN_DOLA_WITH_REWARDS, + [OPT_YVERN_LUSD_STAKING_CONTRACT]: OPT_YVERN_LUSD_WITH_REWARDS, + [OPT_YVLUSD_WETH_STAKING_CONTRACT]: OPT_YVLUSD_WETH_WITH_REWARDS, + [OPT_YVAGEUR_USDC_STAKING_CONTRACT]: OPT_YVAGEUR_USDC_WITH_REWARDS, + [OPT_YVMIM_USDC_STAKING_CONTRACT]: OPT_YVMIM_USDC_WITH_REWARDS, + [OPT_YVDOLA_USDC_STAKING_CONTRACT]: OPT_YVDOLA_USDC_WITH_REWARDS, + [OPT_YVOP_USDC_STAKING_CONTRACT]: OPT_YVOP_USDC_WITH_REWARDS, + [OPT_YVOP_VELO_STAKING_CONTRACT]: OPT_YVOP_VELO_WITH_REWARDS, + [OPT_YVSNX_USDC_STAKING_CONTRACT]: OPT_YVSNX_USDC_WITH_REWARDS, + [OPT_YVFRAX_DOLA_STAKING_CONTRACT]: OPT_YVFRAX_DOLA_WITH_REWARDS, + [OPT_YVALUSD_USDC_STAKING_CONTRACT]: OPT_YVALUSD_USDC_WITH_REWARDS, + [OPT_YVMTA_USDC_STAKING_CONTRACT]: OPT_YVMTA_USDC_WITH_REWARDS, + [OPT_YVIB_WETH_STAKING_CONTRACT]: OPT_YVIB_WETH_WITH_REWARDS, + [OPT_YVEXA_WETH_STAKING_CONTRACT]: OPT_YVEXA_WETH_WITH_REWARDS, + [OPT_YVTBTC_WETH_STAKING_CONTRACT]: OPT_YVTBTC_WETH_WITH_REWARDS, + [OPT_YVTBTC_WBTC_STAKING_CONTRACT]: OPT_YVTBTC_WBTC_WITH_REWARDS, + [OPT_YVOP_WETH_STAKING_CONTRACT]: OPT_YVOP_WETH_WITH_REWARDS, + [OPT_YVWUSDRV2_USDC_STAKING_CONTRACT]: OPT_YVWUSDRV2_USDC_WITH_REWARDS, + [OPT_YVSTERN_ERN_STAKING_CONTRACT]: OPT_YVSTERN_ERN_WITH_REWARDS +}; + +export const STACKING_TO_VAULT: TDict = { + [OPT_YVETH_WITH_REWARDS]: OPT_YVETH_STAKING_CONTRACT, + [OPT_YVDAI_WITH_REWARDS]: OPT_YVDAI_STAKING_CONTRACT, + [OPT_YVUSDT_WITH_REWARDS]: OPT_YVUSDT_STAKING_CONTRACT, + [OPT_YVUSDC_WITH_REWARDS]: OPT_YVUSDC_STAKING_CONTRACT, + [OPT_YVSUSCUSDC_WITH_REWARDS]: OPT_YVSUSCUSDC_STAKING_CONTRACT, + [OPT_YVDOLAUSDC_WITH_REWARDS]: OPT_YVDOLAUSDC_STAKING_CONTRACT, + [OPT_YVMAIUSDC_WITH_REWARDS]: OPT_YVMAIUSDC_STAKING_CONTRACT, + [OPT_YVMAI_WITH_REWARDS]: OPT_YVMAI_STAKING_CONTRACT, + [OPT_YVMAI_USDC_WITH_REWARDS]: OPT_YVMAI_USDC_STAKING_CONTRACT, + [OPT_YVMAI_DOLA_WITH_REWARDS]: OPT_YVMAI_DOLA_STAKING_CONTRACT, + [OPT_YVLDO_WSTETH_WITH_REWARDS]: OPT_YVLDO_WSTETH_STAKING_CONTRACT, + [OPT_YVWUSDR_USDC_WITH_REWARDS]: OPT_YVWUSDR_USDC_STAKING_CONTRACT, + [OPT_YVVELO_USDC_WITH_REWARDS]: OPT_YVVELO_USDC_STAKING_CONTRACT, + [OPT_YVMAI_ALUSD_WITH_REWARDS]: OPT_YVMAI_ALUSD_STAKING_CONTRACT, + [OPT_YVALUSD_FRAX_WITH_REWARDS]: OPT_YVALUSD_FRAX_STAKING_CONTRACT, + [OPT_YVALETH_FRXETH_WITH_REWARDS]: OPT_YVALETH_FRXETH_STAKING_CONTRACT, + [OPT_YVALETH_WETH_WITH_REWARDS]: OPT_YVALETH_WETH_STAKING_CONTRACT, + [OPT_YVERN_DOLA_WITH_REWARDS]: OPT_YVERN_DOLA_STAKING_CONTRACT, + [OPT_YVERN_LUSD_WITH_REWARDS]: OPT_YVERN_LUSD_STAKING_CONTRACT, + [OPT_YVLUSD_WETH_WITH_REWARDS]: OPT_YVLUSD_WETH_STAKING_CONTRACT, + [OPT_YVAGEUR_USDC_WITH_REWARDS]: OPT_YVAGEUR_USDC_STAKING_CONTRACT, + [OPT_YVMIM_USDC_WITH_REWARDS]: OPT_YVMIM_USDC_STAKING_CONTRACT, + [OPT_YVDOLA_USDC_WITH_REWARDS]: OPT_YVDOLA_USDC_STAKING_CONTRACT, + [OPT_YVOP_USDC_WITH_REWARDS]: OPT_YVOP_USDC_STAKING_CONTRACT, + [OPT_YVOP_VELO_WITH_REWARDS]: OPT_YVOP_VELO_STAKING_CONTRACT, + [OPT_YVSNX_USDC_WITH_REWARDS]: OPT_YVSNX_USDC_STAKING_CONTRACT, + [OPT_YVFRAX_DOLA_WITH_REWARDS]: OPT_YVFRAX_DOLA_STAKING_CONTRACT, + [OPT_YVALUSD_USDC_WITH_REWARDS]: OPT_YVALUSD_USDC_STAKING_CONTRACT, + [OPT_YVMTA_USDC_WITH_REWARDS]: OPT_YVMTA_USDC_STAKING_CONTRACT, + [OPT_YVIB_WETH_WITH_REWARDS]: OPT_YVIB_WETH_STAKING_CONTRACT, + [OPT_YVEXA_WETH_WITH_REWARDS]: OPT_YVEXA_WETH_STAKING_CONTRACT, + [OPT_YVTBTC_WETH_WITH_REWARDS]: OPT_YVTBTC_WETH_STAKING_CONTRACT, + [OPT_YVTBTC_WBTC_WITH_REWARDS]: OPT_YVTBTC_WBTC_STAKING_CONTRACT, + [OPT_YVOP_WETH_WITH_REWARDS]: OPT_YVOP_WETH_STAKING_CONTRACT, + [OPT_YVWUSDRV2_USDC_WITH_REWARDS]: OPT_YVWUSDRV2_USDC_STAKING_CONTRACT, + [OPT_YVSTERN_ERN_WITH_REWARDS]: OPT_YVSTERN_ERN_STAKING_CONTRACT +}; diff --git a/pages/_app.tsx b/pages/_app.tsx index 0153cfb8a..1efbd8517 100755 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,4 +1,4 @@ -import React, {Fragment, memo, useEffect} from 'react'; +import React, {Fragment, memo, useCallback, useEffect} from 'react'; import localFont from 'next/font/local'; import useSWR from 'swr'; import {AnimatePresence, domAnimation, LazyMotion, motion} from 'framer-motion'; @@ -6,6 +6,7 @@ import {useIntervalEffect, useIsMounted, useLocalStorageValue} from '@react-hook import {arbitrum, base, fantom, mainnet, optimism} from '@wagmi/chains'; import {WithYearn} from '@yearn-finance/web-lib/contexts/WithYearn'; import {useChainID} from '@yearn-finance/web-lib/hooks/useChainID'; +import {cl} from '@yearn-finance/web-lib/utils/cl'; import {baseFetcher} from '@yearn-finance/web-lib/utils/fetchers'; import {localhost} from '@yearn-finance/web-lib/utils/wagmi/networks'; import {AppHeader} from '@common/components/AppHeader'; @@ -48,6 +49,39 @@ const aeonik = localFont({ ] }); +function useCurrentTheme({name}: {name: string}): void { + const switchToPreferedColorScheme = useCallback((): void => { + const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + const isSystemDarkMode = darkModeMediaQuery.matches; + const isDarkMode = + window.localStorage.isDarkMode === 'true' || (!('isDarkMode' in window.localStorage) && isSystemDarkMode); + + if (isDarkMode) { + document.documentElement.classList.add('dark'); + document.documentElement.classList.remove('v3'); + } else { + document.documentElement.classList.remove('dark'); + document.documentElement.classList.remove('v3'); + } + + if (isDarkMode === isSystemDarkMode) { + delete window.localStorage.isDarkMode; + } + }, []); + + useEffect((): void => { + //whevener we reach this page and `name === v3`, add the `.v3` css tag to the root element (html). + // otherwise, remove it. + if (name === 'V3') { + document.documentElement.classList.remove('dark'); + document.documentElement.classList.add('v3'); + } else { + switchToPreferedColorScheme(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [name === 'V3', switchToPreferedColorScheme]); +} + /** 🔵 - Yearn Finance *************************************************************************** ** The 'WithLayout' function is a React functional component that returns a ReactElement. It is used ** to wrap the current page component and provide layout for the page. @@ -71,34 +105,40 @@ const WithLayout = memo(function WithLayout(props: AppProps): ReactElement { const {value: shouldHidePopover} = useLocalStorageValue('yearn.fi/feedback-popover'); const {name} = useCurrentApp(router); + useCurrentTheme({name}); + return ( -
-
+ <> +
- - - - {getLayout( - , - router - )} - {!shouldHidePopover && } - - -
-
+
+
+ + + + {getLayout( + , + router + )} + {!shouldHidePopover && } + + + +
+
+ ); }); diff --git a/pages/index.tsx b/pages/index.tsx index 7a791487d..f8bb5da78 100755 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -8,6 +8,18 @@ import {LogoYearn} from '@common/icons/LogoYearn'; import type {ReactElement} from 'react'; const apps = [ + { + href: '/vaults-v3', + title: 'V3', + description: 'deposit tokens and receive yield.', + icon: ( + + ) + }, { href: '/vaults', title: 'Vaults', diff --git a/pages/vaults-v3/[chainID]/[address].tsx b/pages/vaults-v3/[chainID]/[address].tsx new file mode 100755 index 000000000..7a97010b8 --- /dev/null +++ b/pages/vaults-v3/[chainID]/[address].tsx @@ -0,0 +1,130 @@ +import {useEffect, useState} from 'react'; +import {useRouter} from 'next/router'; +import {motion} from 'framer-motion'; +import {VaultDetailsTabsWrapper} from '@vaults/components/details/tabs/VaultDetailsTabsWrapper'; +import {VaultActionsTabsWrapper} from '@vaults/components/details/VaultActionsTabsWrapper'; +import {VaultDetailsHeader} from '@vaults/components/details/VaultDetailsHeader'; +import {ActionFlowContextApp} from '@vaults/contexts/useActionFlow'; +import {WithSolverContextApp} from '@vaults/contexts/useSolver'; +import {Wrapper} from '@vaults/Wrapper'; +import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; +import {toAddress} from '@yearn-finance/web-lib/utils/address'; +import TokenIcon from '@common/components/TokenIcon'; +import {useWallet} from '@common/contexts/useWallet'; +import {useFetch} from '@common/hooks/useFetch'; +import {type TYDaemonVault, yDaemonVaultSchema} from '@common/schemas/yDaemonVaultsSchemas'; +import {variants} from '@common/utils/animations'; +import {useYDaemonBaseURI} from '@common/utils/getYDaemonBaseURI'; + +import type {GetStaticPaths, GetStaticProps} from 'next'; +import type {NextRouter} from 'next/router'; +import type {ReactElement} from 'react'; +import type {TUseBalancesTokens} from '@common/hooks/useMultichainBalances'; + +function Index(): ReactElement | null { + const {address, isActive} = useWeb3(); + const router = useRouter(); + const {refresh} = useWallet(); + const {yDaemonBaseUri} = useYDaemonBaseURI({chainID: Number(router.query.chainID)}); + const [currentVault, set_currentVault] = useState(undefined); + const {data: vault, isLoading: isLoadingVault} = useFetch({ + endpoint: router.query.address + ? `${yDaemonBaseUri}/vaults/${toAddress(router.query.address as string)}?${new URLSearchParams({ + strategiesDetails: 'withDetails', + strategiesRisk: 'withRisk', + strategiesCondition: 'inQueue' + })}` + : null, + schema: yDaemonVaultSchema + }); + + useEffect((): void => { + if (vault && !currentVault) { + set_currentVault(vault); + } + }, [currentVault, vault]); + + useEffect((): void => { + if (address && isActive) { + const tokensToRefresh: TUseBalancesTokens[] = []; + if (currentVault?.address) { + tokensToRefresh.push({address: currentVault.address, chainID: currentVault.chainID}); + } + if (currentVault?.token?.address) { + tokensToRefresh.push({address: currentVault.token.address, chainID: currentVault.chainID}); + } + refresh(tokensToRefresh); + } + }, [currentVault?.address, currentVault?.token.address, address, isActive, refresh, currentVault?.chainID]); + + if (isLoadingVault || !router.query.address) { + return ( +
+
+ +
+
+ ); + } + + if (!currentVault) { + return ( +
+
+

+ {"We couln't find this vault on the connected network."} +

+
+
+ ); + } + + return ( + <> +
+ + + +
+ +
+ + + + + + + +
+ + ); +} + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export const getStaticPaths = (async () => { + return { + paths: [], + fallback: true + }; +}) satisfies GetStaticPaths; + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export const getStaticProps: GetStaticProps = async () => { + return { + props: {} + }; +}; + +Index.getLayout = function getLayout(page: ReactElement, router: NextRouter): ReactElement { + return {page}; +}; + +export default Index; diff --git a/pages/vaults-v3/about.tsx b/pages/vaults-v3/about.tsx new file mode 100755 index 000000000..e5e89d5f6 --- /dev/null +++ b/pages/vaults-v3/about.tsx @@ -0,0 +1,149 @@ +import {Balancer} from 'react-wrap-balancer'; +import {Wrapper} from '@vaults/Wrapper'; +import SettingsOverwrite from '@common/components/SettingsOverwrite'; + +import type {NextRouter} from 'next/router'; +import type {ReactElement} from 'react'; + +function About(): ReactElement { + return ( +
+
+
+

{'Wtf is a Vault?'}

+
+
+ +

+ { + 'In ‘traditional finance’ (boo, hiss) you can earn yield on your savings by depositing them in a bank - who use the capital for loans and other productive money growing means.' + } +

+

+ { + 'Yearn Vaults are like crypto savings accounts floating in cyberspace. You deposit your assets, and Yearn puts them to work within the DeFi ecosystem, returning the earned yield back to you.' + } +

+

+ { + 'However, unlike a bank account - none of this takes place behind closed doors (no offence to doors). Decentralised Finance uses public blockchains, meaning you are in control of your assets and can see where they are at all times. Nothing is hidden and everything is auditable by anyone, at any time.' + } +

+
+
+
+ +
+
+

{'Risk Score'}

+
+
+ +

+ { + 'In order to give users the best risk-adjusted yields in DeFi, Yearn uses a comprehensive risk assessment framework for each strategy within a Vault. This framework combines to give each Vault a holistic Risk Score.' + } +

+

+ { + 'Strategies are assessed against eight different factors; Audit, Code Review, Complexity, Longevity, Protocol Safety, Team Knowledge, Testing Score, TVL Impact. Since Vaults use multiple strategies, riskier strategies can be paired with more conservative ones to ensure the Vault has a robust and balanced Risk Score.' + } +

+

+ {'For a full breakdown read more about our '} + + {'Risk Scores'} + + {'.'} +

+
+
+
+ +
+
+

{'Fees'}

+
+
+

+ + { + 'Yearn vaults never have a deposit or withdrawal fee (yay), and most have no management fee and a mere 10% performance fee. Because we use smart contracts (rather than human money managers with expensive designer drug habits) we’re able to be highly capital efficient and pass almost all earned yield on to you.' + } + +

+
+
+ +
+
+

{'APY'}

+
+
+

+ + { + 'Vaults display a Net APY (or Annual Percentage Yield), which is the average APY of the past month’s harvest. For more detailed information on how APYs are calculated, visit our docs.' + } + +

+
+
+ +
+
+

{'Yearn? DeFi? I think I’m lost…'}

+
+
+ +

+ { + 'Searching for ‘words that rhyme with turn’ and accidentally ended up here? Welcome! You’re at the frontier of Decentralised Finance - a new type of financial system built on blockchains and designed to give users better access, transparency and control of their assets.' + } +

+

+ { + 'DeFi offers many opportunities to put your digital assets to work, and earn yield in return - and Yearn was designed to automate this process for you. Less sharp suits and slicked back hair, more cyberspace yield ninjas wielding razor sharp battle tested code katanas.' + } +

+

+ { + 'We can’t offer you a phone number with ambient jazz hold music to listen to - but please feel free to hop into our ' + } + + {'discord'} + + {' if you have any questions, we’d love to chat.'} +

+
+
+
+ + +
+ ); +} + +About.getLayout = function getLayout(page: ReactElement, router: NextRouter): ReactElement { + return {page}; +}; + +export default About; diff --git a/pages/vaults-v3/index.tsx b/pages/vaults-v3/index.tsx new file mode 100644 index 000000000..8dee6f3b0 --- /dev/null +++ b/pages/vaults-v3/index.tsx @@ -0,0 +1,346 @@ +import {Fragment, useEffect, useMemo} from 'react'; +import {QueryParamProvider} from 'use-query-params'; +import {motion, useSpring, useTransform} from 'framer-motion'; +import {VaultListOptions} from '@vaults/components/list/VaultListOptions'; +import {VaultsListEmpty} from '@vaults/components/list/VaultsListEmpty'; +import {VaultsListInternalMigrationRow} from '@vaults/components/list/VaultsListInternalMigrationRow'; +import {VaultsListRetired} from '@vaults/components/list/VaultsListRetired'; +import {VaultsListRow} from '@vaults/components/list/VaultsListRow'; +import {ListHero} from '@vaults/components/ListHero'; +import {useVaultFilter} from '@vaults/hooks/useFilteredVaults'; +import {useSortVaults} from '@vaults/hooks/useSortVaults'; +import {useQueryArguments} from '@vaults/hooks/useVaultsQueryArgs'; +import {Wrapper} from '@vaults/Wrapper'; +import {V3Mask} from '@vaults-v3/Mark'; +import {Button} from '@yearn-finance/web-lib/components/Button'; +import {Renderable} from '@yearn-finance/web-lib/components/Renderable'; +import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; +import {IconChain} from '@yearn-finance/web-lib/icons/IconChain'; +import {formatAmount} from '@yearn-finance/web-lib/utils/format.number'; +import {isZero} from '@yearn-finance/web-lib/utils/isZero'; +import {ListHead} from '@common/components/ListHead'; +import {useWallet} from '@common/contexts/useWallet'; +import {useYearn} from '@common/contexts/useYearn'; +import {NextQueryParamAdapter} from '@common/utils/QueryParamsProvider'; + +import type {NextRouter} from 'next/router'; +import type {ReactElement, ReactNode} from 'react'; +import type {TYDaemonVault} from '@common/schemas/yDaemonVaultsSchemas'; +import type {TSortDirection} from '@common/types/types'; +import type {TPossibleSortBy} from '@vaults/hooks/useSortVaults'; + +function Counter({value}: {value: number}): ReactElement { + const v = useSpring(value, {mass: 1, stiffness: 75, damping: 15}); + const display = useTransform(v, (current): string => `$${formatAmount(current)}`); + + useEffect((): void => { + v.set(value); + }, [v, value]); + + return {display}; +} + +function HeaderUserPosition(): ReactElement { + const {cumulatedValueInVaults} = useWallet(); + const {earned} = useYearn(); + const {options, isActive, address, openLoginModal, onSwitchChain} = useWeb3(); + + const formatedYouEarned = useMemo((): string => { + const amount = (earned?.totalUnrealizedGainsUSD || 0) > 0 ? earned?.totalUnrealizedGainsUSD || 0 : 0; + return formatAmount(amount) ?? ''; + }, [earned?.totalUnrealizedGainsUSD]); + + const formatedYouHave = useMemo((): string => { + return formatAmount(cumulatedValueInVaults || 0) ?? ''; + }, [cumulatedValueInVaults]); + + if (!isActive) { + return ( + +
+

{'Wallet not connected'}

+ +
+
+ ); + } + return ( + +
+ + {'Portfolio'} + +
+
+

{'Deposited'}

+ + + +
+
+

{'Earnings'}

+ + + +
+
+
+
+

{'Earnings'}

+ + + +
+
+ ); +} + +function ListOfVaults(): ReactElement { + const {isLoadingVaultList} = useYearn(); + const { + search, + categories, + chains, + sortDirection, + sortBy, + onSearch, + onChangeCategories, + onChangeChains, + onChangeSortDirection, + onChangeSortBy + } = useQueryArguments(); + const {activeVaults, migratableVaults, retiredVaults} = useVaultFilter(categories, chains); + + /* 🔵 - Yearn Finance ************************************************************************** + ** Then, on the activeVaults list, we apply the search filter. The search filter is + ** implemented as a simple string.includes() on the vault name. + **********************************************************************************************/ + const searchedVaultsToDisplay = useMemo((): TYDaemonVault[] => { + if (!search) { + return activeVaults; + } + return activeVaults.filter((vault: TYDaemonVault): boolean => { + const lowercaseSearch = search.toLowerCase(); + const splitted = + `${vault.name} ${vault.symbol} ${vault.token.name} ${vault.token.symbol} ${vault.address} ${vault.token.address}` + .toLowerCase() + .split(' '); + return splitted.some((word): boolean => word.startsWith(lowercaseSearch)); + }); + }, [activeVaults, search]); + + /* 🔵 - Yearn Finance ************************************************************************** + ** Then, once we have reduced the list of vaults to display, we can sort them. The sorting + ** is done via a custom method that will sort the vaults based on the sortBy and + ** sortDirection values. + **********************************************************************************************/ + const sortedVaultsToDisplay = useSortVaults([...searchedVaultsToDisplay], sortBy, sortDirection); + + /* 🔵 - Yearn Finance ************************************************************************** + ** The VaultList component is memoized to prevent it from being re-created on every render. + ** It contains either the list of vaults, is some are available, or a message to the user. + **********************************************************************************************/ + const VaultList = useMemo((): ReactNode => { + const filteredByChains = sortedVaultsToDisplay.filter(({chainID}): boolean => chains.includes(chainID)); + + if (isLoadingVaultList || isZero(filteredByChains.length) || chains.length === 0) { + return ( + + ); + } + return filteredByChains.map((vault): ReactNode => { + if (!vault) { + return null; + } + return ( + + ); + }); + }, [categories, chains, isLoadingVaultList, onChangeCategories, onChangeChains, search, sortedVaultsToDisplay]); + + return ( +
+
+ +
+ + + 0}> +
+ {retiredVaults + .filter((vault): boolean => !!vault) + .map( + (vault): ReactNode => ( + + ) + )} +
+
+ + 0}> +
+ {migratableVaults + .filter((vault): boolean => !!vault) + .map( + (vault): ReactNode => ( + + ) + )} +
+
+ +
+ { + onChangeSortBy(newSortBy as TPossibleSortBy); + onChangeSortDirection(newSortDirection as TSortDirection); + }} + items={[ + {label: , value: 'chain', sortable: false, className: 'col-span-1'}, + {label: 'Token', value: 'name', sortable: true}, + {label: 'Est. APR', value: 'estAPR', sortable: true, className: 'col-span-2'}, + {label: 'Hist. APR', value: 'apr', sortable: true, className: 'col-span-2'}, + {label: 'Available', value: 'available', sortable: true, className: 'col-span-2'}, + {label: 'Deposited', value: 'deposited', sortable: true, className: 'col-span-2'}, + {label: 'TVL', value: 'tvl', sortable: true, className: 'col-span-2'} + ]} + /> + + {VaultList} +
+ ); +} + +function Index(): ReactElement { + return ( +
+
+
+

+ {'BRAND NEW VAULTS'} +

+

+ {'Corn asked for new pretty design for this page, so hope you like it mates <3'} +

+
+ +
+
+
+
+
+ +
+
+ + {'TVL'} + + + {formatAmount(420420690, 0, 0)} + +
+
+
+
+
+
+
+ + + +
+ + + +
+
+
+ +
+ +
+ +
+ + + +
+
+
+ ); +} + +Index.getLayout = function getLayout(page: ReactElement, router: NextRouter): ReactElement { + return {page}; +}; + +export default Index; diff --git a/style.css b/style.css index deea57b46..d43e06321 100755 --- a/style.css +++ b/style.css @@ -39,6 +39,32 @@ @apply scroll-mt-12 mb-0 md:mb-44; } + :root.v3 { + --color-primary: 220 95% 50%; + --color-neutral-900: 0 0% 100%; + --color-neutral-800: 0 0% 96%; + --color-neutral-700: 0 0% 92%; + --color-neutral-600: 0 0% 88%; + --color-neutral-500: 0 0% 62%; + --color-neutral-400: 0 0% 50%; + --color-neutral-300: 0 0% 36%; + --color-neutral-200: 0 0% 26%; + /* --color-primary: 220 95% 50%; */ + /* --color-neutral-900: 231 100% 11%; */ + /* --color-neutral-800: 0 0% 96%; + --color-neutral-700: 0 0% 92%; + --color-neutral-600: 0 0% 88%; + --color-neutral-500: 0 0% 62%; + --color-neutral-400: 0 0% 50%; + --color-neutral-300: 0 0% 36%; + --color-neutral-200: 0 0% 26%; + --color-neutral-100: 0 0% 16%; */ + --color-neutral-200: 242 54% 27%; + --color-neutral-100: 231 100% 6%; + --color-neutral-0: 231 100% 11%; + /* background-color: #000836; */ + } + /* 🔵 - Yearn Finance ****************************************************************************** ** AppBox is the style used to make the nices animations on the home page feel nice and smooth ** A custom overwrite is required for dark/light mode diff --git a/tailwind.config.js b/tailwind.config.js index a1381452f..76e032aca 100755 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -49,7 +49,12 @@ module.exports = { 13: 'repeat(13, minmax(0, 1fr))', 14: 'repeat(14, minmax(0, 1fr))', 20: 'repeat(20, minmax(0, 1fr))', - 30: 'repeat(30, minmax(0, 1fr))' + 30: 'repeat(30, minmax(0, 1fr))', + 75: 'repeat(75, minmax(0, 1fr))' + }, + gridColumn: { + 'span-46': 'span 46 / span 46', + 'span-29': 'span 29 / span 29' }, fontSize: { xxs: ['10px', '16px'], diff --git a/tsconfig.json b/tsconfig.json index 106454e3c..0ce9e9ec7 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,7 @@ "paths": { "@common/*": ["apps/common/*"], "@vaults/*": ["apps/vaults/*"], + "@vaults-v3/*": ["apps/vaults-v3/*"], "@veYFI/*": ["apps/veyfi/*"], "@yBribe/*": ["apps/ybribe/*"], "@yCRV/*": ["apps/ycrv/*"], From b2590215a4fb4de3c4ffb82fcb6ee6b724472c80 Mon Sep 17 00:00:00 2001 From: Majorfi Date: Mon, 23 Oct 2023 17:42:39 +0200 Subject: [PATCH 002/111] feat: v3 design --- .../common/components/MultiSelectDropdown.tsx | 30 +-- apps/common/components/SearchBar.tsx | 17 +- apps/common/utils/QueryParamsProvider.tsx | 4 +- .../components/{ListHero.tsx => Filters.tsx} | 48 +++-- .../vaults-v3/components/ImageWithOverlay.tsx | 53 ----- apps/vaults-v3/components/RewardsTab.tsx | 201 ------------------ apps/vaults-v3/components/SettingsPopover.tsx | 196 ----------------- .../components/list/VaultListFactory.tsx | 196 ----------------- .../components/list/VaultsV3ListHead.tsx | 102 +++++++++ ...{VaultsListRow.tsx => VaultsV3ListRow.tsx} | 108 ++++++++-- .../components/list/VaultListOptions.tsx | 8 +- pages/_app.tsx | 2 +- pages/vaults-v3/index.tsx | 137 +++++------- style.css | 3 +- 14 files changed, 313 insertions(+), 792 deletions(-) rename apps/vaults-v3/components/{ListHero.tsx => Filters.tsx} (67%) delete mode 100644 apps/vaults-v3/components/ImageWithOverlay.tsx delete mode 100644 apps/vaults-v3/components/RewardsTab.tsx delete mode 100755 apps/vaults-v3/components/SettingsPopover.tsx delete mode 100644 apps/vaults-v3/components/list/VaultListFactory.tsx create mode 100644 apps/vaults-v3/components/list/VaultsV3ListHead.tsx rename apps/vaults-v3/components/list/{VaultsListRow.tsx => VaultsV3ListRow.tsx} (84%) diff --git a/apps/common/components/MultiSelectDropdown.tsx b/apps/common/components/MultiSelectDropdown.tsx index ce8fbcff1..a75926dbb 100755 --- a/apps/common/components/MultiSelectDropdown.tsx +++ b/apps/common/components/MultiSelectDropdown.tsx @@ -19,6 +19,8 @@ type TMultiSelectProps = { options: TMultiSelectOptionProps[]; placeholder?: string; onSelect: (options: TMultiSelectOptionProps[]) => void; + buttonClassName?: string; + comboboxOptionsClassName?: string; }; function SelectAllOption(option: TMultiSelectOptionProps): ReactElement { @@ -45,7 +47,7 @@ function Option(option: TMultiSelectOptionProps): ReactElement { className={'transition-colors hover:bg-neutral-100'}>
- {option?.icon ?
{option.icon}
: null} + {option?.icon ?
{option.icon}
: null}

{option.label}

(options); + const [currentOptions, set_currentOptions] = useState(props.options); const [areAllSelected, set_areAllSelected] = useState(false); const [query, set_query] = useState(''); const componentRef = useRef(null); useEffect((): void => { - set_currentOptions(options); - }, [options]); + set_currentOptions(props.options); + }, [props.options]); useEffect((): void => { set_areAllSelected(currentOptions.every((option): boolean => option.isSelected)); @@ -146,26 +148,27 @@ export function MultiSelectDropdown({options, onSelect, placeholder = ''}: TMult } set_currentOptions(currentState); - onSelect(currentState); + props.onSelect(currentState); }} multiple>
set_isOpen((o: boolean): boolean => !o)} - className={ + className={cl( + props.buttonClassName, 'flex h-10 w-full items-center justify-between bg-neutral-0 p-2 text-base text-neutral-900 md:px-3' - }> + )}> !option.isSelected) + props.options.every((option): boolean => !option.isSelected) ? 'text-neutral-400' : 'text-neutral-900' )} displayValue={(options: TMultiSelectOptionProps[]): string => { const selectedOptions = options.filter((option): boolean => option.isSelected); if (selectedOptions.length === 0) { - return placeholder; + return props.placeholder || ''; } if (selectedOptions.length === 1) { @@ -178,7 +181,7 @@ export function MultiSelectDropdown({options, onSelect, placeholder = ''}: TMult return 'Multiple'; }} - placeholder={placeholder} + placeholder={props.placeholder || ''} spellCheck={false} onChange={(event): void => set_query(event.target.value)} /> @@ -200,9 +203,10 @@ export function MultiSelectDropdown({options, onSelect, placeholder = ''}: TMult set_query(''); }}> + )}> void; className?: string; + iconClassName?: string; }; -export function SearchBar({searchPlaceholder, searchValue, set_searchValue, className}: TSearchBar): ReactElement { +export function SearchBar(props: TSearchBar): ReactElement { return ( <>
@@ -25,15 +26,15 @@ export function SearchBar({searchPlaceholder, searchValue, set_searchValue, clas 'h-10 w-full overflow-x-scroll border-none bg-transparent px-0 py-2 text-base outline-none scrollbar-none placeholder:text-neutral-400' } type={'text'} - placeholder={searchPlaceholder} - value={searchValue} + placeholder={props.searchPlaceholder} + value={props.searchValue} onChange={(e: ChangeEvent): void => { - if (set_searchValue) { - set_searchValue(e.target.value); + if (props.set_searchValue) { + props.set_searchValue(e.target.value); } }} /> -
+
{ return { replace(location: PartialLocation): void { - router.replace(pathname + location.search); + router.replace(pathname + location.search, {scroll: false}); }, push(location: PartialLocation): void { - router.push(pathname + location.search); + router.push(pathname + location.search, {scroll: false}); }, get location(): {search: string} { return { diff --git a/apps/vaults-v3/components/ListHero.tsx b/apps/vaults-v3/components/Filters.tsx similarity index 67% rename from apps/vaults-v3/components/ListHero.tsx rename to apps/vaults-v3/components/Filters.tsx index 223af0024..df6c6de47 100644 --- a/apps/vaults-v3/components/ListHero.tsx +++ b/apps/vaults-v3/components/Filters.tsx @@ -1,4 +1,5 @@ import {useMemo} from 'react'; +import {VaultListOptions} from '@vaults/components/list/VaultListOptions'; import {ALL_CATEGORIES} from '@vaults/constants'; import {IconArbitrumChain} from '@yearn-finance/web-lib/icons/chains/IconArbitrumChain'; import {IconBaseChain} from '@yearn-finance/web-lib/icons/chains/IconBaseChain'; @@ -20,7 +21,7 @@ type TListHero = { onSearch: (searchValue: string) => void; }; -export function ListHero({ +export function Filters({ categories, onChangeCategories, searchValue, @@ -75,11 +76,31 @@ export function ListHero({ }, [categories]); return ( -
-
-
- {'Select Blockchain'} +
+ + {'Filters'} + + +
+ +
+ +
+

{'Search'}

+ +
+
+
+

{'Select Blockchain'}

{ @@ -90,10 +111,11 @@ export function ListHero({ }} />
- -
- {'Filter'} +
+

{'Filter'}

{ @@ -104,16 +126,6 @@ export function ListHero({ }} />
- -
- {'Search'} - -
); diff --git a/apps/vaults-v3/components/ImageWithOverlay.tsx b/apps/vaults-v3/components/ImageWithOverlay.tsx deleted file mode 100644 index 8279682f9..000000000 --- a/apps/vaults-v3/components/ImageWithOverlay.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import {IconCross} from '@yearn-finance/web-lib/icons/IconCross'; -import {ImageWithFallback} from '@common/components/ImageWithFallback'; - -import type {ReactElement} from 'react'; - -type TImageWithOverlayProps = { - imageSrc: string; - imageAlt: string; - imageWidth: number; - imageHeight: number; - overlayText: string; - buttonText: string; - href: string; - onCloseClick: () => void; -}; - -export const ImageWithOverlay: React.FC = ({ - imageSrc, - imageAlt, - imageWidth, - imageHeight, - overlayText, - buttonText, - href, - onCloseClick -}): ReactElement => { - return ( -
- -
- -

{overlayText}

- - - -
-
- ); -}; diff --git a/apps/vaults-v3/components/RewardsTab.tsx b/apps/vaults-v3/components/RewardsTab.tsx deleted file mode 100644 index 0c3f1322c..000000000 --- a/apps/vaults-v3/components/RewardsTab.tsx +++ /dev/null @@ -1,201 +0,0 @@ -import {useCallback, useState} from 'react'; -import {useContractRead} from 'wagmi'; -import {useStakingRewards} from '@vaults/contexts/useStakingRewards'; -import {claim as claimAction, stake as stakeAction, unstake as unstakeAction} from '@vaults/utils/actions'; -import {Button} from '@yearn-finance/web-lib/components/Button'; -import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; -import {VAULT_ABI} from '@yearn-finance/web-lib/utils/abi/vault.abi'; -import {toAddress} from '@yearn-finance/web-lib/utils/address'; -import {ZERO_ADDRESS} from '@yearn-finance/web-lib/utils/constants'; -import {toBigInt, toNormalizedBN} from '@yearn-finance/web-lib/utils/format.bigNumber'; -import {formatCounterValue} from '@yearn-finance/web-lib/utils/format.value'; -import {isZero} from '@yearn-finance/web-lib/utils/isZero'; -import {defaultTxStatus} from '@yearn-finance/web-lib/utils/web3/transaction'; -import {Input} from '@common/components/Input'; -import {useWallet} from '@common/contexts/useWallet'; -import {useToken} from '@common/hooks/useToken'; -import {approveERC20} from '@common/utils/actions'; - -import type {ReactElement} from 'react'; -import type {TYDaemonVault} from '@common/schemas/yDaemonVaultsSchemas'; - -const DISPLAY_DECIMALS = 10; -const trimAmount = (amount: string | number): string => Number(Number(amount).toFixed(DISPLAY_DECIMALS)).toString(); - -export function RewardsTab({currentVault}: {currentVault: TYDaemonVault}): ReactElement { - const {provider, address, isActive} = useWeb3(); - const {refresh: refreshBalances} = useWallet(); - const { - stakingRewardsByVault, - stakingRewardsMap, - positionsMap, - refresh: refreshStakingRewards - } = useStakingRewards(); - const stakingRewardsAddress = stakingRewardsByVault[currentVault.address]; - const stakingRewards = stakingRewardsAddress ? stakingRewardsMap[stakingRewardsAddress] : undefined; - const stakingRewardsPosition = stakingRewardsAddress ? positionsMap[stakingRewardsAddress] : undefined; - const vaultToken = useToken({address: currentVault.address, chainID: currentVault.chainID}); - const rewardTokenBalance = useToken({ - address: toAddress(stakingRewards?.rewardsToken), - chainID: currentVault.chainID - }); - const [approveStakeStatus, set_approveStakeStatus] = useState(defaultTxStatus); - const [stakeStatus, set_stakeStatus] = useState(defaultTxStatus); - const [claimStatus, set_claimStatus] = useState(defaultTxStatus); - const [unstakeStatus, set_unstakeStatus] = useState(defaultTxStatus); - const stakeBalance = toNormalizedBN(toBigInt(stakingRewardsPosition?.stake), currentVault.decimals); - const rewardBalance = toNormalizedBN(toBigInt(stakingRewardsPosition?.reward), rewardTokenBalance.decimals); - - const { - data: allowance, - isLoading, - refetch - } = useContractRead({ - address: currentVault.address, - abi: VAULT_ABI, - chainId: currentVault.chainID, - functionName: 'allowance', - args: [toAddress(address), toAddress(stakingRewards?.address)], - enabled: toAddress(stakingRewards?.address) !== ZERO_ADDRESS - }); - const isApproved = toBigInt(allowance) >= vaultToken.balance.raw; - - const refreshData = useCallback(async (): Promise => { - await Promise.all([refreshBalances(), refreshStakingRewards()]); - }, [refreshBalances, refreshStakingRewards]); - - const onApprove = useCallback(async (): Promise => { - const result = await approveERC20({ - connector: provider, - chainID: currentVault.chainID, - contractAddress: currentVault.address, - spenderAddress: toAddress(stakingRewards?.address), - amount: vaultToken.balance.raw, - statusHandler: set_approveStakeStatus - }); - if (result.isSuccessful) { - refetch(); - } - }, [currentVault.address, provider, refetch, stakingRewards?.address, vaultToken.balance.raw]); - - const onStake = useCallback(async (): Promise => { - const result = await stakeAction({ - connector: provider, - chainID: currentVault.chainID, - contractAddress: toAddress(stakingRewards?.address), - amount: vaultToken.balance.raw, - statusHandler: set_stakeStatus - }); - if (result.isSuccessful) { - refreshData(); - } - }, [provider, refreshData, stakingRewards?.address, vaultToken.balance.raw]); - - const onUnstake = useCallback(async (): Promise => { - const result = await unstakeAction({ - connector: provider, - chainID: currentVault.chainID, - contractAddress: toAddress(stakingRewards?.address), - statusHandler: set_unstakeStatus - }); - if (result.isSuccessful) { - refreshData(); - } - }, [provider, refreshData, stakingRewards?.address]); - - const onClaim = useCallback(async (): Promise => { - const result = await claimAction({ - connector: provider, - chainID: currentVault.chainID, - contractAddress: toAddress(stakingRewards?.address), - statusHandler: set_claimStatus - }); - if (result.isSuccessful) { - refreshData(); - } - }, [provider, refreshData, stakingRewards?.address]); - - return ( -
-
-
-
{'Stake'}
-
-

{'Stake your yVault tokens for additional $OP rewards.'}

-
-
-
- - -
-
-
-
-
{'Claim'}
-
-

{"Claim your staking rewards here. You've earned it anon."}

-
-
-
- - -
-
-
-
-
{'Unstake'}
-
-

- { - 'Unstake your yVault tokens and your remaining $OP rewards will be claimed automatically. Boom.' - } -

-
-
-
- - -
-
-
- ); -} diff --git a/apps/vaults-v3/components/SettingsPopover.tsx b/apps/vaults-v3/components/SettingsPopover.tsx deleted file mode 100755 index 9f8048bab..000000000 --- a/apps/vaults-v3/components/SettingsPopover.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import {Fragment, useMemo} from 'react'; -import {Popover, Transition} from '@headlessui/react'; -import {isSolverDisabled} from '@vaults/contexts/useSolver'; -import {useStakingRewards} from '@vaults/contexts/useStakingRewards'; -import {Renderable} from '@yearn-finance/web-lib/components/Renderable'; -import {IconSettings} from '@yearn-finance/web-lib/icons/IconSettings'; -import {Switch} from '@common/components/Switch'; -import {useYearn} from '@common/contexts/useYearn'; -import {Solver} from '@common/schemas/yDaemonTokenListBalances'; - -import type {ReactElement} from 'react'; -import type {TSolver} from '@common/schemas/yDaemonTokenListBalances'; -import type {TYDaemonVault} from '@common/schemas/yDaemonVaultsSchemas'; - -type TSettingPopover = { - vault: TYDaemonVault; -}; - -function Label({children}: {children: string}): ReactElement { - return ( - - ); -} - -export function SettingsPopover({vault}: TSettingPopover): ReactElement { - const { - zapProvider, - set_zapProvider, - zapSlippage, - set_zapSlippage, - isStakingOpBoostedVaults, - set_isStakingOpBoostedVaults - } = useYearn(); - const {stakingRewardsByVault} = useStakingRewards(); - - const {address, chainID} = vault; - const hasStakingRewards = !!stakingRewardsByVault?.[address]; - - const currentZapProvider = useMemo((): TSolver => { - if (chainID !== 1 && zapProvider === 'Cowswap') { - return 'Wido'; - } - return zapProvider; - }, [chainID, zapProvider]); - - return ( - - {(): ReactElement => ( - <> - - {'Settings'} - - - - -
-
-
- - - - - {'Submit a'}  - - {'gasless order'} - -  {'using CoW Swap.'} - - - - - {'Submit an order via'}  - - {'Wido'} - -  {'(0.3% fee).'} - - - -   - -
-
- -
- - -
- { - set_zapSlippage(parseFloat(e.target.value) || 0); - }} - /> -

{'%'}

-
-
-
- {hasStakingRewards ? ( -
- -
-
-

{'Stake automatically'}

- - set_isStakingOpBoostedVaults(!isStakingOpBoostedVaults) - } - /> -
-
-
- ) : null} -
-
-
-
- - )} -
- ); -} diff --git a/apps/vaults-v3/components/list/VaultListFactory.tsx b/apps/vaults-v3/components/list/VaultListFactory.tsx deleted file mode 100644 index 8f0878f05..000000000 --- a/apps/vaults-v3/components/list/VaultListFactory.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import {useCallback, useMemo, useState} from 'react'; -import {VaultListOptions} from '@vaults/components/list/VaultListOptions'; -import {VaultsListEmptyFactory} from '@vaults/components/list/VaultsListEmpty'; -import {VaultsListRow} from '@vaults/components/list/VaultsListRow'; -import {useAppSettings} from '@vaults/contexts/useAppSettings'; -import {useFilteredVaults} from '@vaults/hooks/useFilteredVaults'; -import {useSortVaults} from '@vaults/hooks/useSortVaults'; -import {isZero} from '@yearn-finance/web-lib/utils/isZero'; -import {performBatchedUpdates} from '@yearn-finance/web-lib/utils/performBatchedUpdates'; -import {ListHead} from '@common/components/ListHead'; -import {ListHero} from '@common/components/ListHero'; -import {useWallet} from '@common/contexts/useWallet'; -import {useYearn} from '@common/contexts/useYearn'; -import {isAutomatedVault, type TYDaemonVaults} from '@common/schemas/yDaemonVaultsSchemas'; -import {getVaultName} from '@common/utils'; - -import type {ReactElement, ReactNode} from 'react'; -import type {TSortDirection} from '@common/types/types'; -import type {TPossibleSortBy} from '@vaults/hooks/useSortVaults'; - -export function VaultListFactory(): ReactElement { - const {getToken} = useWallet(); - const {vaults, isLoadingVaultList} = useYearn(); - const [sortBy, set_sortBy] = useState('apr'); - const [sortDirection, set_sortDirection] = useState(''); - const {shouldHideLowTVLVaults, shouldHideDust, searchValue, set_searchValue} = useAppSettings(); - const [category, set_category] = useState('Curve Factory Vaults'); - - /* 🔵 - Yearn Finance ************************************************************************** - ** It's best to memorize the filtered vaults, which saves a lot of processing time by only - ** performing the filtering once. - **********************************************************************************************/ - const curveVaults = useFilteredVaults( - vaults, - (vault): boolean => vault.category === 'Curve' && isAutomatedVault(vault) - ); - const holdingsVaults = useFilteredVaults(vaults, (vault): boolean => { - const {category, address, chainID} = vault; - const holding = getToken({address, chainID}); - const hasValidBalance = holding.balance.raw > 0n; - const balanceValue = holding.value || 0; - if (shouldHideDust && balanceValue < 0.01) { - return false; - } - if (hasValidBalance && category === 'Curve' && isAutomatedVault(vault)) { - return true; - } - return false; - }); - - /* 🔵 - Yearn Finance ************************************************************************** - ** First, we need to determine in which category we are. The vaultsToDisplay function will - ** decide which vaults to display based on the category. No extra filters are applied. - ** The possible lists are memoized to avoid unnecessary re-renders. - **********************************************************************************************/ - const vaultsToDisplay = useMemo((): TYDaemonVaults => { - let _vaultList: TYDaemonVaults = [...Object.values(vaults || {})]; - - if (category === 'Curve Factory Vaults') { - _vaultList = curveVaults; - } else if (category === 'Holdings') { - _vaultList = holdingsVaults; - } - - if (shouldHideLowTVLVaults && category !== 'Holdings') { - _vaultList = _vaultList.filter((vault): boolean => vault.tvl.tvl > 10_000); - } - - return _vaultList; - }, [category, curveVaults, holdingsVaults, shouldHideLowTVLVaults, vaults]); - - /* 🔵 - Yearn Finance ************************************************************************** - ** Then, on the vaultsToDisplay list, we apply the search filter. The search filter is - ** implemented as a simple string.includes() on the vault name. - **********************************************************************************************/ - const searchedVaults = useMemo((): TYDaemonVaults => { - const vaultsToUse = [...vaultsToDisplay]; - - if (searchValue === '') { - return vaultsToUse; - } - return vaultsToUse.filter((vault): boolean => { - const searchString = getVaultName(vault); - return searchString.toLowerCase().includes(searchValue.toLowerCase()); - }); - }, [vaultsToDisplay, searchValue]); - - /* 🔵 - Yearn Finance ************************************************************************** - ** Then, once we have reduced the list of vaults to display, we can sort them. The sorting - ** is done via a custom method that will sort the vaults based on the sortBy and - ** sortDirection values. - **********************************************************************************************/ - const sortedVaultsToDisplay = useSortVaults([...searchedVaults], sortBy, sortDirection); - - /* 🔵 - Yearn Finance ************************************************************************** - ** Callback method used to sort the vaults list. - ** The use of useCallback() is to prevent the method from being re-created on every render. - **********************************************************************************************/ - const onSort = useCallback((newSortBy: string, newSortDirection: string): void => { - performBatchedUpdates((): void => { - set_sortBy(newSortBy as TPossibleSortBy); - set_sortDirection(newSortDirection as TSortDirection); - }); - }, []); - - /* 🔵 - Yearn Finance ************************************************************************** - ** The VaultList component is memoized to prevent it from being re-created on every render. - ** It contains either the list of vaults, is some are available, or a message to the user. - **********************************************************************************************/ - const VaultList = useMemo((): ReactNode => { - if (isLoadingVaultList || isZero(sortedVaultsToDisplay.length)) { - return ( - - ); - } - return sortedVaultsToDisplay.map((vault): ReactNode => { - if (!vault) { - return null; - } - return ( - - ); - }); - }, [category, isLoadingVaultList, sortedVaultsToDisplay]); - - return ( -
-
- -
- - - - - {VaultList} -
- ); -} diff --git a/apps/vaults-v3/components/list/VaultsV3ListHead.tsx b/apps/vaults-v3/components/list/VaultsV3ListHead.tsx new file mode 100644 index 000000000..38f4a7b26 --- /dev/null +++ b/apps/vaults-v3/components/list/VaultsV3ListHead.tsx @@ -0,0 +1,102 @@ +import {useCallback} from 'react'; +import {cl} from '@yearn-finance/web-lib/utils/cl'; +import {IconChevronPlain} from '@common/icons/IconChevronPlain'; + +import type {ReactElement} from 'react'; +import type {TSortDirection} from '@common/types/types'; + +export type TListHead = { + items: { + label: string | ReactElement; + value: string; + sortable?: boolean; + className?: string; + }[]; + sortBy: string; + sortDirection: TSortDirection; + onSort: (sortBy: string, sortDirection: TSortDirection) => void; +}; + +export function VaultsV3ListHead({items, sortBy, sortDirection, onSort}: TListHead): ReactElement { + const toggleSortDirection = (newSortBy: string): TSortDirection => { + return sortBy === newSortBy + ? sortDirection === '' + ? 'desc' + : sortDirection === 'desc' + ? 'asc' + : 'desc' + : 'desc'; + }; + + const renderChevron = useCallback( + (shouldSortBy: boolean): ReactElement => { + if (shouldSortBy && sortDirection === 'desc') { + return ; + } + if (shouldSortBy && sortDirection === 'asc') { + return ( + + ); + } + return ( + + ); + }, + [sortDirection] + ); + + const [token, ...rest] = items; + return ( +
+
+
+ +
+ +
+ {rest.map( + (item, index): ReactElement => ( + + ) + )} +
+
+
+ ); +} diff --git a/apps/vaults-v3/components/list/VaultsListRow.tsx b/apps/vaults-v3/components/list/VaultsV3ListRow.tsx similarity index 84% rename from apps/vaults-v3/components/list/VaultsListRow.tsx rename to apps/vaults-v3/components/list/VaultsV3ListRow.tsx index 4928d3fe5..5c972d9bc 100755 --- a/apps/vaults-v3/components/list/VaultsListRow.tsx +++ b/apps/vaults-v3/components/list/VaultsV3ListRow.tsx @@ -2,6 +2,7 @@ import {useMemo} from 'react'; import Link from 'next/link'; import {Renderable} from '@yearn-finance/web-lib/components/Renderable'; import {toAddress} from '@yearn-finance/web-lib/utils/address'; +import {cl} from '@yearn-finance/web-lib/utils/cl'; import {ETH_TOKEN_ADDRESS, WETH_TOKEN_ADDRESS, WFTM_TOKEN_ADDRESS} from '@yearn-finance/web-lib/utils/constants'; import {formatAmount} from '@yearn-finance/web-lib/utils/format.number'; import {isZero} from '@yearn-finance/web-lib/utils/isZero'; @@ -9,12 +10,11 @@ import {ImageWithFallback} from '@common/components/ImageWithFallback'; import {RenderAmount} from '@common/components/RenderAmount'; import {useWallet} from '@common/contexts/useWallet'; import {useBalance} from '@common/hooks/useBalance'; -import {getVaultName} from '@common/utils'; import type {ReactElement} from 'react'; import type {TYDaemonVault} from '@common/schemas/yDaemonVaultsSchemas'; -export function VaultForwardAPR({currentVault}: {currentVault: TYDaemonVault}): ReactElement { +function VaultForwardAPR({currentVault}: {currentVault: TYDaemonVault}): ReactElement { const isEthMainnet = currentVault.chainID === 1; if (currentVault.apr.forwardAPR.type === '') { const hasZeroAPR = isZero(currentVault.apr?.netAPR) || Number(currentVault.apr?.netAPR.toFixed(2)) === 0; @@ -247,7 +247,7 @@ export function VaultForwardAPR({currentVault}: {currentVault: TYDaemonVault}): ); } -export function VaultHistoricalAPR({currentVault}: {currentVault: TYDaemonVault}): ReactElement { +function VaultHistoricalAPR({currentVault}: {currentVault: TYDaemonVault}): ReactElement { const hasZeroAPR = isZero(currentVault.apr?.netAPR) || Number(currentVault.apr?.netAPR.toFixed(2)) === 0; if (currentVault.apr?.extra.stakingRewardsAPR > 0) { @@ -324,7 +324,58 @@ export function VaultHistoricalAPR({currentVault}: {currentVault: TYDaemonVault} ); } -export function VaultsListRow({currentVault}: {currentVault: TYDaemonVault}): ReactElement { +function VaultChainTag({chainID}: {chainID: number}): ReactElement { + switch (chainID) { + case 1: + return ( +
+
{'Ethereum'}
+
+ ); + case 10: + return ( +
+
{'Optimism'}
+
+ ); + case 137: + return ( +
+
+ {'Polygon PoS'} +
+
+ ); + case 250: + return ( +
+
{'Fantom'}
+
+ ); + case 8453: + return ( +
+
{'Base'}
+
+ ); + case 42161: + return ( +
+
{'Arbitrum'}
+
+ ); + default: + return ( +
+
{'Ethereum'}
+
+ ); + } +} + +export function VaultsV3ListRow({currentVault}: {currentVault: TYDaemonVault}): ReactElement { const {getToken} = useWallet(); const balanceOfWant = useBalance({chainID: currentVault.chainID, address: currentVault.token.address}); const balanceOfCoin = useBalance({chainID: currentVault.chainID, address: ETH_TOKEN_ADDRESS}); @@ -332,15 +383,11 @@ export function VaultsListRow({currentVault}: {currentVault: TYDaemonVault}): Re chainID: currentVault.chainID, address: toAddress(currentVault.token.address) === WFTM_TOKEN_ADDRESS ? WFTM_TOKEN_ADDRESS : WETH_TOKEN_ADDRESS //TODO: Create a wagmi Chain upgrade to add the chain wrapper token address }); - const vaultName = useMemo((): string => getVaultName(currentVault), [currentVault]); - const availableToDeposit = useMemo((): bigint => { if (toAddress(currentVault.token.address) === WETH_TOKEN_ADDRESS) { - // Handle ETH native coin return balanceOfWrappedCoin.raw + balanceOfCoin.raw; } if (toAddress(currentVault.token.address) === WFTM_TOKEN_ADDRESS) { - // Handle FTM native coin return balanceOfWrappedCoin.raw + balanceOfCoin.raw; } return balanceOfWant.raw; @@ -356,18 +403,29 @@ export function VaultsListRow({currentVault}: {currentVault: TYDaemonVault}): Re -
-
- -
-
-
-
+
+
+ +
+
+
-

{vaultName}

+
+ + {currentVault.name} + +

{currentVault.token.name}

+ +
-
+
diff --git a/apps/vaults/components/list/VaultListOptions.tsx b/apps/vaults/components/list/VaultListOptions.tsx index ab49b5463..a1ca780f1 100644 --- a/apps/vaults/components/list/VaultListOptions.tsx +++ b/apps/vaults/components/list/VaultListOptions.tsx @@ -2,11 +2,15 @@ import {Fragment} from 'react'; import {Popover, Transition} from '@headlessui/react'; import {useAppSettings} from '@vaults/contexts/useAppSettings'; import {IconSettings} from '@yearn-finance/web-lib/icons/IconSettings'; +import {cl} from '@yearn-finance/web-lib/utils/cl'; import {Switch} from '@common/components/Switch'; import type {ReactElement} from 'react'; -export function VaultListOptions(): ReactElement { +type TVautListOptions = { + panelClassName?: string; +}; +export function VaultListOptions(props: TVautListOptions): ReactElement { const {shouldHideDust, onSwitchHideDust, shouldHideLowTVLVaults, onSwitchHideLowTVLVaults} = useAppSettings(); return ( @@ -29,7 +33,7 @@ export function VaultListOptions(): ReactElement { 'absolute right-0 top-6 z-[1000] mt-3 w-screen max-w-[180px] md:-right-4 md:top-4' }>
-
+
-
+
-
- -
diff --git a/style.css b/style.css index d43e06321..5701afd8c 100755 --- a/style.css +++ b/style.css @@ -47,8 +47,6 @@ --color-neutral-600: 0 0% 88%; --color-neutral-500: 0 0% 62%; --color-neutral-400: 0 0% 50%; - --color-neutral-300: 0 0% 36%; - --color-neutral-200: 0 0% 26%; /* --color-primary: 220 95% 50%; */ /* --color-neutral-900: 231 100% 11%; */ /* --color-neutral-800: 0 0% 96%; @@ -59,6 +57,7 @@ --color-neutral-300: 0 0% 36%; --color-neutral-200: 0 0% 26%; --color-neutral-100: 0 0% 16%; */ + --color-neutral-300: 231 51% 19%; --color-neutral-200: 242 54% 27%; --color-neutral-100: 231 100% 6%; --color-neutral-0: 231 100% 11%; From a54d2b733f951d5adcf4767eed5a3d3487590f7c Mon Sep 17 00:00:00 2001 From: Majorfi Date: Mon, 23 Oct 2023 18:22:40 +0200 Subject: [PATCH 003/111] fix: some ui mistakes --- apps/common/contexts/useYearn.tsx | 2 +- pages/_app.tsx | 9 +++-- pages/_document.tsx | 31 +-------------- pages/vaults-v3/index.tsx | 66 +++++++++++++++++-------------- 4 files changed, 43 insertions(+), 65 deletions(-) diff --git a/apps/common/contexts/useYearn.tsx b/apps/common/contexts/useYearn.tsx index ef393a0a4..e27ea0cde 100755 --- a/apps/common/contexts/useYearn.tsx +++ b/apps/common/contexts/useYearn.tsx @@ -94,7 +94,7 @@ export const YearnContextApp = memo(function YearnContextApp({children}: {childr strategiesDetails: 'withDetails', strategiesRisk: 'withRisk', strategiesCondition: 'inQueue', - chainIDs: [1, 10, 250, 8453, 42161].join(','), + chainIDs: [1, 10, 137, 250, 8453, 42161].join(','), limit: `2500` })}`, schema: yDaemonVaultsSchema diff --git a/pages/_app.tsx b/pages/_app.tsx index cf21b1ead..9835696d0 100755 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -76,10 +76,11 @@ function useCurrentTheme({name}: {name: string}): void { document.documentElement.classList.remove('dark'); document.documentElement.classList.add('v3'); } else { - switchToPreferedColorScheme(); + if (document.documentElement.classList.contains('v3')) { + switchToPreferedColorScheme(); + } } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [name === 'V3', switchToPreferedColorScheme]); + }, [name, switchToPreferedColorScheme]); } /** 🔵 - Yearn Finance *************************************************************************** @@ -115,7 +116,7 @@ const WithLayout = memo(function WithLayout(props: AppProps): ReactElement {
-
+
{ const initialProps = await Document.getInitialProps(ctx); @@ -39,9 +12,7 @@ class MyDocument extends Document { render(): ReactElement { return ( - -