diff --git a/apps/common/components/Table.tsx b/apps/common/components/Table.tsx index 7cfdd8423..656779321 100644 --- a/apps/common/components/Table.tsx +++ b/apps/common/components/Table.tsx @@ -8,6 +8,7 @@ import {usePagination} from '@common/hooks/usePagination'; import {IconChevronPlain} from '@common/icons/IconChevronPlain'; import type {ReactElement} from 'react'; +import type {TNormalizedBN} from '@common/types/types'; type TSortOrder = 'asc' | 'desc'; @@ -36,9 +37,10 @@ type TTableProps = { initialSortBy?: Extract; onRowClick?: (item: T) => void; itemsPerPage?: number; + isLoading?: boolean; } -export function Table({metadata, data, columns, initialSortBy, onRowClick, itemsPerPage}: TTableProps): ReactElement { +export function Table({metadata, data, columns, initialSortBy, onRowClick, itemsPerPage, isLoading}: TTableProps): ReactElement { const [{sortedBy, order}, set_state] = useState>({sortedBy: initialSortBy, order: 'desc'}); const sortedData = useMemo((): T[] => { @@ -112,6 +114,25 @@ export function Table({metadata, data, columns, initialSortBy, onRowClick, it ))} + {currentItems.length === 0 && isLoading ? + ( +
+ {'Fetching gauge data'} +

{'We are retrieving the gauges. Please wait.'}

+
+ +
+
+ ) : currentItems.length === 0 && !isLoading ? + ( +
+ {'No Gauges'} +

+ {'No gauges available.'} +

+
+ ) : null + } {currentItems.map((item, rowIndex): ReactElement => (
({metadata, data, columns, initialSortBy, onRowClick, it {!fullWidth && }
diff --git a/apps/veyfi/components/GaugesTab.tsx b/apps/veyfi/components/GaugesTab.tsx index b4f03cc64..67bd6f1b0 100644 --- a/apps/veyfi/components/GaugesTab.tsx +++ b/apps/veyfi/components/GaugesTab.tsx @@ -1,12 +1,13 @@ import {useCallback, useState} from 'react'; import {useDeepCompareMemo} from '@react-hookz/web'; import {useGauge} from '@veYFI/contexts/useGauge'; +import {useOption} from '@veYFI/contexts/useOption'; import {approveAndStake, stake, unstake} from '@veYFI/utils/actions/gauge'; -import {VEYFI_CHAIN_ID} from '@veYFI/utils/constants'; +import {SECONDS_PER_YEAR, VEYFI_CHAIN_ID} from '@veYFI/utils/constants'; import {Button} from '@yearn-finance/web-lib/components/Button'; import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; import {allowanceKey, toAddress, truncateHex} from '@yearn-finance/web-lib/utils/address'; -import {toBigInt, toNormalizedBN} from '@yearn-finance/web-lib/utils/format.bigNumber'; +import {formatToNormalizedValue, toBigInt, toNormalizedBN} from '@yearn-finance/web-lib/utils/format.bigNumber'; import {formatAmount, formatPercent} from '@yearn-finance/web-lib/utils/format.number'; import {defaultTxStatus} from '@yearn-finance/web-lib/utils/web3/transaction'; import {ImageWithFallback} from '@common/components/ImageWithFallback'; @@ -26,7 +27,7 @@ type TGaugeData = { vaultName: string, vaultApy: number, vaultDeposited: TNormalizedBN, - gaugeApy: number, + gaugeAPR: number, gaugeBoost: number, gaugeStaked: TNormalizedBN, allowance: TNormalizedBN, @@ -38,11 +39,11 @@ function GaugeTabButtons({isApproved, vaultAddress, gaugeAddress, vaultDeposited const {provider, address, isActive} = useWeb3(); const {refresh: refreshGauges} = useGauge(); const {refresh: refreshBalances} = useWallet(); - const refreshData = (): unknown => Promise.all([refreshGauges(), refreshBalances()]); const [approveAndStakeStatus, set_approveAndStakeStatus] = useState(defaultTxStatus); const [stakeStatus, set_stakeStatus] = useState(defaultTxStatus); const [unstakeStatus, set_unstakeStatus] = useState(defaultTxStatus); const userAddress = address as TAddress; + const refreshData = useCallback((): unknown => Promise.all([refreshGauges(), refreshBalances()]), [refreshGauges, refreshBalances]); const onApproveAndStake = useCallback(async (vaultAddress: TAddress, gaugeAddress: TAddress, amount: bigint): Promise => { const response = await approveAndStake({ @@ -121,36 +122,60 @@ function GaugeTabButtons({isApproved, vaultAddress, gaugeAddress, vaultDeposited export function GaugesTab(): ReactElement { const {address} = useWeb3(); - const {gaugeAddresses, gaugesMap, positionsMap, allowancesMap} = useGauge(); - const {vaults} = useYearn(); + const {gaugesMap, positionsMap, allowancesMap} = useGauge(); + const {vaults, prices} = useYearn(); const {balances} = useWallet(); + const {dYFIPrice} = useOption(); + const [isLoadingGauges, set_isLoadingGauges] = useState(true); const userAddress = address as TAddress; const gaugesData = useDeepCompareMemo((): TGaugeData[] => { - return ( - gaugeAddresses.map((address): TGaugeData => { - const gauge = gaugesMap[address]; - const vaultAddress = toAddress(gauge?.vaultAddress); - const vault = vaults[vaultAddress]; - - return ({ - gaugeAddress: address, - vaultAddress, - decimals: gauge?.decimals ?? 18, - vaultIcon: `${process.env.BASE_YEARN_ASSETS_URI}/1/${vaultAddress}/logo-128.png`, - vaultName: vault?.display_name ?? `Vault ${truncateHex(vaultAddress, 4)}`, - vaultApy: vault?.apy.net_apy ?? 0, - vaultDeposited: balances[vaultAddress], - gaugeApy: 0, // TODO: gauge apy calcs - gaugeBoost: positionsMap[address]?.boost ?? 1, - gaugeStaked: positionsMap[address]?.deposit ?? toNormalizedBN(0), - allowance: allowancesMap[allowanceKey(VEYFI_CHAIN_ID, vaultAddress, address, userAddress)], - isApproved: toBigInt(allowancesMap[allowanceKey(VEYFI_CHAIN_ID, vaultAddress, address, userAddress)]?.raw) >= toBigInt(balances[vaultAddress]?.raw), - actions: undefined - }); - }) - ); - }, [gaugesMap, gaugeAddresses, vaults, balances, positionsMap, allowancesMap, userAddress]); + if (!vaults || Object.values(vaults).length === 0) { + return []; + } + + const data: TGaugeData[] = []; + for (const gauge of Object.values(gaugesMap)) { + if (!gauge) { + continue; + } + + const vault = vaults[toAddress(gauge?.vaultAddress)]; + const tokenPrice = formatToNormalizedValue(toBigInt(prices?.[vault.token.address] || 0), 6); + const boost = Number(positionsMap[gauge.address]?.boost || 1); + let APRFor10xBoost = Number(gauge?.rewardRate.normalized || 0) * dYFIPrice * SECONDS_PER_YEAR / Number(gauge?.totalStaked.normalized || 0) / tokenPrice * 100; + if (tokenPrice === 0 || Number(gauge?.totalStaked.normalized || 0) === 0) { + APRFor10xBoost = 0; + } + + console.warn(`${Number(gauge?.rewardRate.normalized || 0)} x ${dYFIPrice} x ${SECONDS_PER_YEAR} / ${Number(gauge?.totalStaked.normalized || 0)} / ${tokenPrice} = ${APRFor10xBoost}`); + + // gauge.rewardRate() * dYFI_price * seconds_per_year / gauge.totalAssets() / vault_token_price + + + // const APR = rewardRate * 31556952n / totalAssets.raw; + // console.log(APR) + // gauge.rewardRate() * seconds_per_year / gauge.totalAssets() / vault_token_price + + data.push({ + gaugeAddress: gauge.address, + vaultAddress: vault.address, + decimals: gauge.decimals, + vaultIcon: `${process.env.BASE_YEARN_ASSETS_URI}/1/${vault.address}/logo-128.png`, + vaultName: vault?.display_name ?? `Vault ${truncateHex(vault.address, 4)}`, + vaultApy: vault?.apy.net_apy ?? 0, + vaultDeposited: balances[vault.address], + gaugeAPR: APRFor10xBoost, + gaugeBoost: boost, + gaugeStaked: positionsMap[gauge.address]?.deposit ?? toNormalizedBN(0), + allowance: allowancesMap[allowanceKey(VEYFI_CHAIN_ID, vault.address, gauge.address, userAddress)], + isApproved: toBigInt(allowancesMap[allowanceKey(VEYFI_CHAIN_ID, vault.address, gauge.address, userAddress)]?.raw) >= toBigInt(balances[vault.address]?.raw), + actions: undefined + }); + } + set_isLoadingGauges(false); + return data; + }, [gaugesMap, vaults, balances, positionsMap, allowancesMap, userAddress]); return ( @@ -160,7 +185,7 @@ export function GaugesTab(): ReactElement { { key: 'vaultName', label: 'Asset', - columnSpan: 2, + columnSpan: 3, sortable: true, fullWidth: true, className: 'my-4 md:my-0', @@ -193,9 +218,17 @@ export function GaugesTab(): ReactElement { format: ({vaultDeposited}): string => formatAmount(vaultDeposited?.normalized || 0, 2, 6) }, { - key: 'gaugeApy', - label: 'Gauge APY', - sortable: true + key: 'gaugeAPR', + label: 'Gauge APR', + columnSpan: 2, + sortable: true, + className: 'whitespace-break text-right', + format: ({gaugeAPR}): string => { + // if (gaugeAPR === 0) { + // return formatAmount(gaugeAPR, 2, 2); + // } + return `${formatAmount(gaugeAPR / 10, 2, 2)}% → ${formatAmount(gaugeAPR, 2, 2)}%`; + } }, { key: 'gaugeBoost', @@ -207,6 +240,7 @@ export function GaugesTab(): ReactElement { key: 'gaugeStaked', label: 'Staked', sortable: true, + className: 'mr-4', format: ({gaugeStaked}): string => formatAmount(gaugeStaked?.normalized || 0, 2, 6) }, { @@ -218,9 +252,10 @@ export function GaugesTab(): ReactElement { transform: (props): ReactElement => } ]} + isLoading={isLoadingGauges} data={gaugesData} - columns={9} - initialSortBy={'gaugeApy'} + columns={11} + initialSortBy={'gaugeAPR'} />
); diff --git a/apps/veyfi/components/RewardsTab.tsx b/apps/veyfi/components/RewardsTab.tsx index 447b12bb6..908524d97 100644 --- a/apps/veyfi/components/RewardsTab.tsx +++ b/apps/veyfi/components/RewardsTab.tsx @@ -1,4 +1,4 @@ -import {useCallback, useState} from 'react'; +import {useCallback, useMemo, useState} from 'react'; import {useGauge} from '@veYFI/contexts/useGauge'; import {useOption} from '@veYFI/contexts/useOption'; import * as GaugeActions from '@veYFI/utils/actions/gauge'; @@ -19,7 +19,7 @@ import type {TDropdownOption} from '@common/components/Dropdown'; export function RewardsTab(): ReactElement { const [selectedGauge, set_selectedGauge] = useState(); const {provider, isActive} = useWeb3(); - const {gaugeAddresses, gaugesMap, positionsMap, refresh: refreshGauges} = useGauge(); + const {gaugesMap, positionsMap, refresh: refreshGauges} = useGauge(); const {dYFIPrice} = useOption(); const {vaults} = useYearn(); const refreshData = useCallback((): unknown => Promise.all([refreshGauges()]), [refreshGauges]); @@ -39,18 +39,22 @@ export function RewardsTab(): ReactElement { } }, [provider, refreshData, selectedGaugeAddress]); - const gaugeOptions = gaugeAddresses.filter((address): boolean => toBigInt(positionsMap[address]?.reward?.raw) > 0n ?? false) - .map((address): TDropdownOption => { - const gauge = gaugesMap[address]; - const vaultAddress = toAddress(gauge?.vaultAddress); - const vault = vaults[vaultAddress]; + const gaugeOptions = useMemo((): TDropdownOption[] => { + const options: TDropdownOption[] = []; + for (const gauge of Object.values(gaugesMap)) { + if (!gauge || toBigInt(positionsMap[gauge.address]?.reward?.raw) === 0n) { + continue; + } + const vault = vaults[toAddress(gauge?.vaultAddress)]; - return { - id: address, - label: vault?.display_name ?? `Vault ${truncateHex(vaultAddress, 4)}`, - icon: `${process.env.BASE_YEARN_ASSETS_URI}/1/${vaultAddress}/logo-128.png` - }; - }); + options.push({ + id: gauge.address, + label: vault?.display_name ?? `Vault ${truncateHex(vault.address, 4)}`, + icon: `${process.env.BASE_YEARN_ASSETS_URI}/1/${vault.address}/logo-128.png` + }); + } + return options; + }, [gaugesMap, positionsMap, vaults]); return (
diff --git a/apps/veyfi/contexts/useGauge.tsx b/apps/veyfi/contexts/useGauge.tsx index a08926204..6f50322a3 100644 --- a/apps/veyfi/contexts/useGauge.tsx +++ b/apps/veyfi/contexts/useGauge.tsx @@ -22,7 +22,7 @@ export type TGauge = { symbol: string, decimals: number, totalStaked: TNormalizedBN, - // apy?: number; + rewardRate: TNormalizedBN, } export type TGaugePosition = { @@ -33,14 +33,12 @@ export type TGaugePosition = { } export type TGaugeContext = { - gaugeAddresses: TAddress[], gaugesMap: TDict, positionsMap: TDict, allowancesMap: TDict, refresh: () => void, } const defaultProps: TGaugeContext = { - gaugeAddresses: [], gaugesMap: {}, positionsMap: {}, allowancesMap: {}, @@ -62,17 +60,26 @@ export const GaugeContextApp = memo(function GaugeContextApp({children}: {childr {address: gaugeAddress, abi: VEYFI_GAUGE_ABI, chainId: VEYFI_CHAIN_ID, functionName: 'name'}, {address: gaugeAddress, abi: VEYFI_GAUGE_ABI, chainId: VEYFI_CHAIN_ID, functionName: 'symbol'}, {address: gaugeAddress, abi: VEYFI_GAUGE_ABI, chainId: VEYFI_CHAIN_ID, functionName: 'decimals'}, - {address: gaugeAddress, abi: VEYFI_GAUGE_ABI, chainId: VEYFI_CHAIN_ID, functionName: 'totalAssets'} + {address: gaugeAddress, abi: VEYFI_GAUGE_ABI, chainId: VEYFI_CHAIN_ID, functionName: 'totalAssets'}, + {address: gaugeAddress, abi: VEYFI_GAUGE_ABI, chainId: VEYFI_CHAIN_ID, functionName: 'rewardRate'} ] }); const decimals = Number(decodeAsBigInt(results[3])) || decodeAsNumber(results[3]); + const totalAssets = toNormalizedBN(decodeAsBigInt(results[4]), decimals); + const rewardRate = toNormalizedBN(decodeAsBigInt(results[5]), 18); + + //Debug value to test + // const totalAssets = toNormalizedBN(400000000000000000000n, decimals); + // const rewardRate = toNormalizedBN(3306878306878n, 18); + return ({ address: gaugeAddress, vaultAddress: decodeAsAddress(results[0]), name: decodeAsString(results[1]), symbol: decodeAsString(results[2]), decimals: decimals, - totalStaked: toNormalizedBN(decodeAsBigInt(results[4]), decimals) + totalStaked: totalAssets, + rewardRate }); }); @@ -183,7 +190,6 @@ export const GaugeContextApp = memo(function GaugeContextApp({children}: {childr }, [refreshAllowances, refreshPositions, refreshVotingEscrow]); const contextValue = useDeepCompareMemo((): TGaugeContext => ({ - gaugeAddresses: gauges.map((gauge): TAddress => toAddress(gauge.address)), gaugesMap: keyBy(gauges, 'address'), positionsMap: positionsMap, allowancesMap: allowancesMap ?? {}, diff --git a/apps/veyfi/utils/constants.ts b/apps/veyfi/utils/constants.ts index a9b167591..42aee7192 100644 --- a/apps/veyfi/utils/constants.ts +++ b/apps/veyfi/utils/constants.ts @@ -12,6 +12,7 @@ export const VEYFI_DYFI_ADDRESS = toAddress('0x41252E8691e964f7DE35156B68493bAb6 export const SNAPSHOT_DELEGATE_REGISTRY_ADDRESS = toAddress('0x469788fE6E9E9681C6ebF3bF78e7Fd26Fc015446'); export const YEARN_SNAPSHOT_SPACE = 'veyfi.eth'; +export const SECONDS_PER_YEAR = 31556952; export const MAX_LOCK_TIME: TWeeks = 208; export const MIN_LOCK_TIME: TWeeks = 1; export const MIN_LOCK_AMOUNT: TWeeks = 1;