From 67c672e71646aaaa85568599daff392315071064 Mon Sep 17 00:00:00 2001 From: sharky Date: Mon, 30 Dec 2024 16:10:51 +0000 Subject: [PATCH 1/3] Initial version of staking estimator --- src/AppRoutes.tsx | 5 + src/utils/constants.ts | 1 + .../components/CompareSkeleton.tsx | 7 + .../components/RTokenCard.tsx | 344 ++++++++++++++++++ .../components/RTokenList.tsx | 47 +++ .../components/staking-estimator/index.tsx | 222 +++++++++++ src/views/explorer/index.tsx | 6 + 7 files changed, 632 insertions(+) create mode 100644 src/views/explorer/components/staking-estimator/components/CompareSkeleton.tsx create mode 100644 src/views/explorer/components/staking-estimator/components/RTokenCard.tsx create mode 100644 src/views/explorer/components/staking-estimator/components/RTokenList.tsx create mode 100644 src/views/explorer/components/staking-estimator/index.tsx diff --git a/src/AppRoutes.tsx b/src/AppRoutes.tsx index cbfd39831..277ef490a 100644 --- a/src/AppRoutes.tsx +++ b/src/AppRoutes.tsx @@ -19,6 +19,7 @@ import PortfolioWrapper from 'views/portfolio' import Staking from 'views/staking' import Home from 'views/home' import AvailableRevenue from 'views/explorer/components/revenue' +import StakingEstimator from 'views/explorer/components/staking-estimator' import Terms from 'views/terms' // Preloadable components @@ -99,6 +100,10 @@ const AppRoutes = () => ( element={} /> } /> + } + /> } /> Not found} /> diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 1b6f30eb6..73bd5671c 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -181,6 +181,7 @@ export const ROUTES = Object.freeze({ EXPLORER_COLLATERALS: 'collaterals', EXPLORER_GOVERNANCE: '/explorer/governance', EXPLORER_REVENUE: '/explorer/revenue', + EXPLORER_STAKING_ESTIMATOR: '/explorer/staking-estimator', EXPLORER_TRANSACTIONS: 'transactions', TERMS: '/terms', }) diff --git a/src/views/explorer/components/staking-estimator/components/CompareSkeleton.tsx b/src/views/explorer/components/staking-estimator/components/CompareSkeleton.tsx new file mode 100644 index 000000000..39a97c57c --- /dev/null +++ b/src/views/explorer/components/staking-estimator/components/CompareSkeleton.tsx @@ -0,0 +1,7 @@ +import Skeleton from 'react-loading-skeleton' + +const CompareSkeleton = () => ( + +) + +export default CompareSkeleton diff --git a/src/views/explorer/components/staking-estimator/components/RTokenCard.tsx b/src/views/explorer/components/staking-estimator/components/RTokenCard.tsx new file mode 100644 index 000000000..a5c1afa75 --- /dev/null +++ b/src/views/explorer/components/staking-estimator/components/RTokenCard.tsx @@ -0,0 +1,344 @@ +import { t, Trans } from '@lingui/macro' +import { Button } from 'components' +import DgnETHButtonAppendix from 'components/dgneth/DgnETHButtonAppendix' +import ChainLogo from 'components/icons/ChainLogo' +import ChevronRight from 'components/icons/ChevronRight' +import CollaterizationIcon from 'components/icons/CollaterizationIcon' +import MoneyIcon from 'components/icons/MoneyIcon' +import PegIcon from 'components/icons/PegIcon' +import TokenLogo from 'components/icons/TokenLogo' +import { ListedToken } from 'hooks/useTokenList' +import { memo } from 'react' +import { useNavigate } from 'react-router-dom' +import { Box, BoxProps, Card, Text } from 'theme-ui' +import { formatCurrency, getTokenRoute } from 'utils' +import { CHAIN_TAGS, ROUTES } from 'utils/constants' +import EarnButton from 'views/compare/components/EarnButton' +import MobileCollateralInfo from 'views/compare/components/MobileCollateralInfo' +import VerticalDivider from 'views/compare/components/VerticalDivider' +import usePriceETH from 'views/compare/hooks/usePriceETH' +import CollateralPieChartWrapper from 'views/overview/components/CollateralPieChartWrapper' +import RTokenAddresses from 'views/overview/components/RTokenAddresses' + +interface Props extends BoxProps { + token: ListedToken +} + +const ChainBadge = ({ chain }: { chain: number }) => ( + + + + {CHAIN_TAGS[chain] + ' Native'} + + +) + +// TODO: Component should be splitted +const RTokenCard = ({ token, ...props }: Props) => { + const navigate = useNavigate() + const { priceETHTerms, supplyETHTerms } = usePriceETH(token) + + const handleNavigate = (route: string) => { + navigate(getTokenRoute(token.id, token.chain, route)) + } + + return ( + { + e.stopPropagation() + handleNavigate(ROUTES.OVERVIEW) + }} + {...props} + > + + + + + + + + + {token.symbol} + + + + + + + + + + + {/* + + + + + + + */} + + + + + + + {token.symbol} + + + {priceETHTerms + ? `${priceETHTerms} ${token.targetUnits} ($${formatCurrency( + token.price, + 3 + )})` + : `$${formatCurrency(token.price, 3)}`} + + + + + + + + + + + + + + + + + + + Market cap: + + + + + ${formatCurrency(token.supply, 0)} + + {supplyETHTerms && ( + + {`(${formatCurrency(supplyETHTerms, 0)} ${ + token.targetUnits + })`} + + )} + + + + + + + Peg: + + + {token?.targetUnits?.split(',').length > 2 + ? `${token?.targetUnits?.split(',').length} targets` + : token.targetUnits} + + + + + + + Backing + Overcollaterization: + + + {(token.backing + token.overcollaterization).toFixed(0)}% + + + + + + + + + + + + + + + + + + + ) +} + +export default memo(RTokenCard) diff --git a/src/views/explorer/components/staking-estimator/components/RTokenList.tsx b/src/views/explorer/components/staking-estimator/components/RTokenList.tsx new file mode 100644 index 000000000..aa990528e --- /dev/null +++ b/src/views/explorer/components/staking-estimator/components/RTokenList.tsx @@ -0,0 +1,47 @@ +import useTokenList from 'hooks/useTokenList' +import { useAtomValue } from 'jotai' +import { useMemo } from 'react' +import useRTokenPools from 'views/earn/hooks/useRTokenPools' +import CompareSkeleton from './CompareSkeleton' +import RTokenCard from './RTokenCard' + +const RTokenList = () => { + const { list, isLoading } = useTokenList() + // Load pools to get rtoken earn info + useRTokenPools() + + + // const chains = useAtomValue(chainsFilterAtom) + // const targets = useAtomValue(targetFilterAtom) + + + + // const filteredList = useMemo(() => { + // return list.filter((token) => { + // if (!chains.length || !chains.includes(token.chain.toString())) { + // return false + // } + + // if ( + // !targets.length || + // !targets.find((t) => token.targetUnits.includes(t)) + // ) { + // return false + // } + + // return true + // }) + // }, [list, chains, targets]) + + return ( + <> + {isLoading && !list.length && } + + {list.map((token) => ( + + ))} + + ) +} + +export default RTokenList diff --git a/src/views/explorer/components/staking-estimator/index.tsx b/src/views/explorer/components/staking-estimator/index.tsx new file mode 100644 index 000000000..a25118154 --- /dev/null +++ b/src/views/explorer/components/staking-estimator/index.tsx @@ -0,0 +1,222 @@ +import { Trans, t } from '@lingui/macro' +import { CellContext, Row, createColumnHelper } from '@tanstack/react-table' +import BasketCubeIcon from 'components/icons/BasketCubeIcon' +import ChainLogo from 'components/icons/ChainLogo' +import { Table, TableProps } from 'components/table' +import TokenItem from 'components/token-item' +import useRTokenLogo from 'hooks/useRTokenLogo' +import useTokenList, { ListedToken } from 'hooks/useTokenList' +import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai' +import { useMemo, useState } from 'react' +import { ChevronDown, ChevronUp } from 'react-feather' +import { Box, Link, Text } from 'theme-ui' +import { + formatCurrency, + formatPercentage, + formatUsdCurrencyCell, + getTokenRoute, +} from 'utils' +import { ROUTES, TARGET_UNITS, supportedChainList } from 'utils/constants' +import CirclesIcon from 'components/icons/CirclesIcon' +import Ethereum from 'components/icons/logos/Ethereum' +import EarnNavIcon from 'components/icons/EarnNavIcon' +import { borderRadius } from 'theme' +import ChainFilter from 'views/explorer/components/filters/ChainFilter' +import CalculatorIcon from 'components/icons/CalculatorIcon' + +const filtersAtom = atom<{ chains: string[]; targets: string[] }>({ + chains: supportedChainList.map((chain) => chain.toString()), + targets: [TARGET_UNITS.USD, TARGET_UNITS.ETH], +}) + +const renderSubComponent = ({ row }: { row: Row }) => { + return ( + +
+        {JSON.stringify(row.original, null, 2)}
+      
+
+ ) +} + +const cellShares = (data: CellContext) => { + const value = data.getValue() + const sharePercents = Array.isArray(value) ? value : [value] + + const totalYield = + (data.row.original.supply * data.row.original.basketApy) / 100 + return ( + <> + {sharePercents.length < 1 && -} + + {sharePercents.map((sharePercent) => ( + + + {formatPercentage(sharePercent)} + + {sharePercent > 0 && ( + + (~$ + {formatCurrency((sharePercent / 100) * totalYield, 2, { + notation: 'compact', + })} + ) + + )} + + ))} + + + ) +} + +const ExploreStakingEstimator = (props: Partial) => { + const { list, isLoading } = useTokenList() + const columnHelper = createColumnHelper() + + const columns = useMemo( + () => [ + columnHelper.accessor('symbol', { + header: t`Token`, + cell: (data) => { + const logo = useRTokenLogo( + data.row.original.id, + data.row.original.chain + ) + return ( + + ) + }, + }), + columnHelper.accessor('supply', { + header: t`Mkt Cap`, + cell: (data) => `$${formatCurrency(data.getValue(), 0)}`, + }), + columnHelper.accessor('stakeUsd', { + header: t`Staked RSR`, + cell: (data) => `$${formatCurrency(data.getValue(), 0)}`, + }), + columnHelper.accessor('basketApy', { + header: t`Basket APY`, + cell: (data) => formatPercentage(data.getValue()), + }), + columnHelper.accessor((row) => parseFloat(row.distribution.holders), { + id: 'Holders share', + cell: cellShares, + }), + columnHelper.accessor((row) => parseFloat(row.distribution.stakers), { + id: 'Stakers share', + cell: cellShares, + }), + columnHelper.accessor( + (row) => + row.distribution.external.map((dist) => parseFloat(dist.total)), + { + id: 'External shares', + cell: cellShares, + } + ), + columnHelper.accessor( + (row) => { + const totalYield = (row.supply * row.basketApy) / 100 + const stakerYieldPercent = parseFloat(row.distribution.stakers) + const totalRsrStakerYield = (totalYield * stakerYieldPercent) / 100 + + return totalRsrStakerYield / row.rsrStaked + }, + { + id: 'Est. Yield per RSR', + cell: (data) => `$${formatCurrency(data.getValue(), 5)}`, + } + ), + columnHelper.accessor( + (row) => { + const totalYield = (row.supply * row.basketApy) / 100 + const stakerYieldPercent = parseFloat(row.distribution.stakers) + const totalRsrStakerYield = (totalYield * stakerYieldPercent) / 100 + + return (totalRsrStakerYield / row.rsrStaked) * 100000 + }, + { + id: 'Est. Yield per 100k RSR', + cell: (data) => ( + + ${formatCurrency(data.getValue())} + + ), + } + ), + columnHelper.accessor('id', { + header: t`Stake`, + cell: (data) => ( + e.stopPropagation()} + > + Stake + + ), + }), + ], + [] + ) + + const handleClick = (data: any, row: any) => { + row.toggleExpanded() + } + + return ( + + + + + Staking Estimator + + + + + ) +} + +export default ExploreStakingEstimator diff --git a/src/views/explorer/index.tsx b/src/views/explorer/index.tsx index bb52db67e..46534a7ec 100644 --- a/src/views/explorer/index.tsx +++ b/src/views/explorer/index.tsx @@ -51,6 +51,12 @@ const Navigation = () => { CollateralsGovernanceRevenue + + + Estimator + + Staking Estimator + ) } From fd945d6a157728edba4ec0f65bb0bcfeb0d10def Mon Sep 17 00:00:00 2001 From: sharky Date: Mon, 30 Dec 2024 16:12:53 +0000 Subject: [PATCH 2/3] Remove unused code --- .../components/CompareSkeleton.tsx | 7 - .../components/RTokenCard.tsx | 344 ------------------ .../components/RTokenList.tsx | 47 --- .../components/staking-estimator/index.tsx | 25 +- 4 files changed, 3 insertions(+), 420 deletions(-) delete mode 100644 src/views/explorer/components/staking-estimator/components/CompareSkeleton.tsx delete mode 100644 src/views/explorer/components/staking-estimator/components/RTokenCard.tsx delete mode 100644 src/views/explorer/components/staking-estimator/components/RTokenList.tsx diff --git a/src/views/explorer/components/staking-estimator/components/CompareSkeleton.tsx b/src/views/explorer/components/staking-estimator/components/CompareSkeleton.tsx deleted file mode 100644 index 39a97c57c..000000000 --- a/src/views/explorer/components/staking-estimator/components/CompareSkeleton.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import Skeleton from 'react-loading-skeleton' - -const CompareSkeleton = () => ( - -) - -export default CompareSkeleton diff --git a/src/views/explorer/components/staking-estimator/components/RTokenCard.tsx b/src/views/explorer/components/staking-estimator/components/RTokenCard.tsx deleted file mode 100644 index a5c1afa75..000000000 --- a/src/views/explorer/components/staking-estimator/components/RTokenCard.tsx +++ /dev/null @@ -1,344 +0,0 @@ -import { t, Trans } from '@lingui/macro' -import { Button } from 'components' -import DgnETHButtonAppendix from 'components/dgneth/DgnETHButtonAppendix' -import ChainLogo from 'components/icons/ChainLogo' -import ChevronRight from 'components/icons/ChevronRight' -import CollaterizationIcon from 'components/icons/CollaterizationIcon' -import MoneyIcon from 'components/icons/MoneyIcon' -import PegIcon from 'components/icons/PegIcon' -import TokenLogo from 'components/icons/TokenLogo' -import { ListedToken } from 'hooks/useTokenList' -import { memo } from 'react' -import { useNavigate } from 'react-router-dom' -import { Box, BoxProps, Card, Text } from 'theme-ui' -import { formatCurrency, getTokenRoute } from 'utils' -import { CHAIN_TAGS, ROUTES } from 'utils/constants' -import EarnButton from 'views/compare/components/EarnButton' -import MobileCollateralInfo from 'views/compare/components/MobileCollateralInfo' -import VerticalDivider from 'views/compare/components/VerticalDivider' -import usePriceETH from 'views/compare/hooks/usePriceETH' -import CollateralPieChartWrapper from 'views/overview/components/CollateralPieChartWrapper' -import RTokenAddresses from 'views/overview/components/RTokenAddresses' - -interface Props extends BoxProps { - token: ListedToken -} - -const ChainBadge = ({ chain }: { chain: number }) => ( - - - - {CHAIN_TAGS[chain] + ' Native'} - - -) - -// TODO: Component should be splitted -const RTokenCard = ({ token, ...props }: Props) => { - const navigate = useNavigate() - const { priceETHTerms, supplyETHTerms } = usePriceETH(token) - - const handleNavigate = (route: string) => { - navigate(getTokenRoute(token.id, token.chain, route)) - } - - return ( - { - e.stopPropagation() - handleNavigate(ROUTES.OVERVIEW) - }} - {...props} - > - - - - - - - - - {token.symbol} - - - - - - - - - - - {/* - - - - - - - */} - - - - - - - {token.symbol} - - - {priceETHTerms - ? `${priceETHTerms} ${token.targetUnits} ($${formatCurrency( - token.price, - 3 - )})` - : `$${formatCurrency(token.price, 3)}`} - - - - - - - - - - - - - - - - - - - Market cap: - - - - - ${formatCurrency(token.supply, 0)} - - {supplyETHTerms && ( - - {`(${formatCurrency(supplyETHTerms, 0)} ${ - token.targetUnits - })`} - - )} - - - - - - - Peg: - - - {token?.targetUnits?.split(',').length > 2 - ? `${token?.targetUnits?.split(',').length} targets` - : token.targetUnits} - - - - - - - Backing + Overcollaterization: - - - {(token.backing + token.overcollaterization).toFixed(0)}% - - - - - - - - - - - - - - - - - - - ) -} - -export default memo(RTokenCard) diff --git a/src/views/explorer/components/staking-estimator/components/RTokenList.tsx b/src/views/explorer/components/staking-estimator/components/RTokenList.tsx deleted file mode 100644 index aa990528e..000000000 --- a/src/views/explorer/components/staking-estimator/components/RTokenList.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import useTokenList from 'hooks/useTokenList' -import { useAtomValue } from 'jotai' -import { useMemo } from 'react' -import useRTokenPools from 'views/earn/hooks/useRTokenPools' -import CompareSkeleton from './CompareSkeleton' -import RTokenCard from './RTokenCard' - -const RTokenList = () => { - const { list, isLoading } = useTokenList() - // Load pools to get rtoken earn info - useRTokenPools() - - - // const chains = useAtomValue(chainsFilterAtom) - // const targets = useAtomValue(targetFilterAtom) - - - - // const filteredList = useMemo(() => { - // return list.filter((token) => { - // if (!chains.length || !chains.includes(token.chain.toString())) { - // return false - // } - - // if ( - // !targets.length || - // !targets.find((t) => token.targetUnits.includes(t)) - // ) { - // return false - // } - - // return true - // }) - // }, [list, chains, targets]) - - return ( - <> - {isLoading && !list.length && } - - {list.map((token) => ( - - ))} - - ) -} - -export default RTokenList diff --git a/src/views/explorer/components/staking-estimator/index.tsx b/src/views/explorer/components/staking-estimator/index.tsx index a25118154..fdc31b461 100644 --- a/src/views/explorer/components/staking-estimator/index.tsx +++ b/src/views/explorer/components/staking-estimator/index.tsx @@ -1,34 +1,15 @@ import { Trans, t } from '@lingui/macro' import { CellContext, Row, createColumnHelper } from '@tanstack/react-table' -import BasketCubeIcon from 'components/icons/BasketCubeIcon' -import ChainLogo from 'components/icons/ChainLogo' import { Table, TableProps } from 'components/table' import TokenItem from 'components/token-item' import useRTokenLogo from 'hooks/useRTokenLogo' import useTokenList, { ListedToken } from 'hooks/useTokenList' -import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai' -import { useMemo, useState } from 'react' -import { ChevronDown, ChevronUp } from 'react-feather' +import { useMemo } from 'react' import { Box, Link, Text } from 'theme-ui' -import { - formatCurrency, - formatPercentage, - formatUsdCurrencyCell, - getTokenRoute, -} from 'utils' -import { ROUTES, TARGET_UNITS, supportedChainList } from 'utils/constants' -import CirclesIcon from 'components/icons/CirclesIcon' -import Ethereum from 'components/icons/logos/Ethereum' -import EarnNavIcon from 'components/icons/EarnNavIcon' -import { borderRadius } from 'theme' -import ChainFilter from 'views/explorer/components/filters/ChainFilter' +import { formatCurrency, formatPercentage, getTokenRoute } from 'utils' +import { ROUTES } from 'utils/constants' import CalculatorIcon from 'components/icons/CalculatorIcon' -const filtersAtom = atom<{ chains: string[]; targets: string[] }>({ - chains: supportedChainList.map((chain) => chain.toString()), - targets: [TARGET_UNITS.USD, TARGET_UNITS.ETH], -}) - const renderSubComponent = ({ row }: { row: Row }) => { return ( Date: Mon, 30 Dec 2024 16:37:21 +0000 Subject: [PATCH 3/3] Remove row click for now --- src/views/explorer/components/staking-estimator/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/views/explorer/components/staking-estimator/index.tsx b/src/views/explorer/components/staking-estimator/index.tsx index fdc31b461..2673e6d87 100644 --- a/src/views/explorer/components/staking-estimator/index.tsx +++ b/src/views/explorer/components/staking-estimator/index.tsx @@ -189,7 +189,6 @@ const ExploreStakingEstimator = (props: Partial) => { data={list} isLoading={isLoading && !list.length} columns={columns} - onRowClick={handleClick} sorting sortBy={[{ id: 'supply', desc: true }]} sx={{ borderRadius: '0 0 20px 20px' }}