From e30ef9c7fadaec43e7e6087ac0c58be8b23a5cdb Mon Sep 17 00:00:00 2001 From: Amir Ekbatanifard Date: Mon, 11 Nov 2024 19:39:36 +0330 Subject: [PATCH] feat: display portfolio change in account detail (#1644) Co-authored-by: Nick --- .../AccountInformationForDetails.tsx | 5 +- .../accountDetails/components/TotalChart.tsx | 79 +++++++++++++------ .../partials/TotalBalancePieChart.tsx | 29 ++++--- .../homeFullScreen/partials/WatchList.tsx | 21 +++-- .../src/hooks/useYouHave.ts | 45 +++++------ 5 files changed, 107 insertions(+), 72 deletions(-) diff --git a/packages/extension-polkagate/src/fullscreen/accountDetails/components/AccountInformationForDetails.tsx b/packages/extension-polkagate/src/fullscreen/accountDetails/components/AccountInformationForDetails.tsx index 51155e7b9..0b986e11e 100644 --- a/packages/extension-polkagate/src/fullscreen/accountDetails/components/AccountInformationForDetails.tsx +++ b/packages/extension-polkagate/src/fullscreen/accountDetails/components/AccountInformationForDetails.tsx @@ -78,7 +78,7 @@ interface BalanceRowJSXType { } const BalanceRow = ({ balanceToShow, isBalanceOutdated, isPriceOutdated, price }: BalanceRowJSXType) => ( - + @@ -106,7 +106,7 @@ const SelectedAssetBox = ({ balanceToShow, genesisHash, isBalanceOutdated, isPri - + @@ -152,7 +152,6 @@ function AccountInformationForDetails ({ accountAssets, address, label, price, p const theme = useTheme(); const { account, chain, genesisHash, token } = useInfo(address); - const calculatePrice = useCallback((amount: BN, decimal: number, _price: number) => { return parseFloat(amountToHuman(amount, decimal)) * _price; }, []); diff --git a/packages/extension-polkagate/src/fullscreen/accountDetails/components/TotalChart.tsx b/packages/extension-polkagate/src/fullscreen/accountDetails/components/TotalChart.tsx index 64afc6fb0..fd02bc698 100644 --- a/packages/extension-polkagate/src/fullscreen/accountDetails/components/TotalChart.tsx +++ b/packages/extension-polkagate/src/fullscreen/accountDetails/components/TotalChart.tsx @@ -4,21 +4,23 @@ /* eslint-disable sort-keys */ /* eslint-disable react/jsx-max-props-per-line */ -import type { BN } from '@polkadot/util'; import type { FetchedBalance } from '../../../hooks/useAssetsBalances'; import type { Prices } from '../../../util/types'; import { Divider, Grid, Typography, useTheme } from '@mui/material'; import { Chart, registerables } from 'chart.js'; import React, { useCallback, useEffect, useMemo, useRef } from 'react'; +import CountUp from 'react-countup'; import { AssetLogo } from '../../../components'; import FormatPrice from '../../../components/FormatPrice'; -import { useTranslation } from '../../../hooks'; +import { useCurrency, useTranslation } from '../../../hooks'; +import { calcChange, calcPrice } from '../../../hooks/useYouHave'; +import { COIN_GECKO_PRICE_CHANGE_DURATION } from '../../../util/api/getPrices'; import { DEFAULT_COLOR } from '../../../util/constants'; import getLogo2 from '../../../util/getLogo2'; -import { amountToHuman } from '../../../util/utils'; -import { adjustColor } from '../../homeFullScreen/partials/TotalBalancePieChart'; +import { countDecimalPlaces, fixFloatingPoint } from '../../../util/utils'; +import { adjustColor, changeSign, PORTFOLIO_CHANGE_DECIMAL } from '../../homeFullScreen/partials/TotalBalancePieChart'; interface Props { accountAssets: FetchedBalance[] | null | undefined; @@ -35,29 +37,32 @@ export default function TotalChart ({ accountAssets, pricesInCurrency }: Props): const { t } = useTranslation(); const theme = useTheme(); const chartRef = useRef(null); + const currency = useCurrency(); Chart.register(...registerables); - const calPrice = useCallback((assetPrice: number | undefined, balance: BN, decimal: number) => parseFloat(amountToHuman(balance, decimal)) * (assetPrice ?? 0), []); - const priceOf = useCallback((priceId: string): number => pricesInCurrency?.prices?.[priceId]?.value || 0, [pricesInCurrency?.prices]); + const changePriceOf = useCallback((priceId: string): number => pricesInCurrency?.prices?.[priceId]?.change || 0, [pricesInCurrency?.prices]); const formatNumber = useCallback((num: number): number => parseFloat(Math.trunc(num) === 0 ? num.toFixed(2) : num.toFixed(1)), []); - const { assets, totalWorth } = useMemo(() => { + const { assets, totalChange, totalWorth } = useMemo(() => { if (accountAssets?.length) { const _assets = accountAssets as unknown as AssetsToShow[]; let total = 0; + let totalChange = 0; /** to add asset's worth and color */ accountAssets.forEach((asset, index) => { - const assetWorth = calPrice(priceOf(asset.priceId), asset.totalBalance, asset.decimal); + const assetWorth = calcPrice(priceOf(asset.priceId), asset.totalBalance, asset.decimal); const assetColor = getLogo2(asset.genesisHash, asset.token)?.color || DEFAULT_COLOR; _assets[index].worth = assetWorth; _assets[index].color = adjustColor(asset.token, assetColor, theme); total += assetWorth; + + totalChange += calcChange(priceOf(asset.priceId), Number(asset.totalBalance) / (10 ** asset.decimal), changePriceOf(asset.priceId)); }); /** to add asset's percentage */ @@ -70,11 +75,11 @@ export default function TotalChart ({ accountAssets, pricesInCurrency }: Props): _assets.sort((a, b) => b.worth - a.worth); const nonZeroAssets = _assets.filter((asset) => asset.worth > 0); - return { assets: nonZeroAssets, totalWorth: total }; + return { assets: nonZeroAssets, totalChange, totalWorth: total }; } - return { assets: undefined, totalWorth: undefined }; - }, [accountAssets, calPrice, formatNumber, priceOf, theme]); + return { assets: undefined, totalChange: 0, totalWorth: undefined }; + }, [accountAssets, changePriceOf, formatNumber, priceOf, theme]); useEffect(() => { const worths = assets?.map(({ worth }) => worth); @@ -111,21 +116,46 @@ export default function TotalChart ({ accountAssets, pricesInCurrency }: Props): return () => { chartInstance.destroy(); }; - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [assets?.length, theme.palette.divider]); + const accountBalanceTotalChange = useMemo(() => { + if (!totalChange) { + return 0; + } + + const value = fixFloatingPoint(totalChange, PORTFOLIO_CHANGE_DECIMAL, false, true); + + return parseFloat(value); + }, [totalChange]); + return ( - - - - {t('Total')} - - + + + + + {t('Total')} + + + + + 0 ? 'success.main' : 'warning.main', fontSize: '16px', fontWeight: 500 }}> + + + {assets && assets.length > 0 && @@ -153,7 +183,8 @@ export default function TotalChart ({ accountAssets, pricesInCurrency }: Props): }) } - } + + } ); } diff --git a/packages/extension-polkagate/src/fullscreen/homeFullScreen/partials/TotalBalancePieChart.tsx b/packages/extension-polkagate/src/fullscreen/homeFullScreen/partials/TotalBalancePieChart.tsx index 5ed95f19b..998d4e999 100644 --- a/packages/extension-polkagate/src/fullscreen/homeFullScreen/partials/TotalBalancePieChart.tsx +++ b/packages/extension-polkagate/src/fullscreen/homeFullScreen/partials/TotalBalancePieChart.tsx @@ -16,11 +16,12 @@ import { stars6Black, stars6White } from '../../../assets/icons'; import { AccountsAssetsContext, AssetLogo } from '../../../components'; import FormatPrice from '../../../components/FormatPrice'; import { useCurrency, usePrices, useTranslation, useYouHave } from '../../../hooks'; +import { calcPrice } from '../../../hooks/useYouHave'; import { isPriceOutdated } from '../../../popup/home/YouHave'; import { COIN_GECKO_PRICE_CHANGE_DURATION } from '../../../util/api/getPrices'; import { DEFAULT_COLOR, TEST_NETS, TOKENS_WITH_BLACK_LOGO } from '../../../util/constants'; import getLogo2 from '../../../util/getLogo2'; -import { amountToHuman, countDecimalPlaces, fixFloatingPoint } from '../../../util/utils'; +import { countDecimalPlaces, fixFloatingPoint } from '../../../util/utils'; import Chart from './Chart'; interface Props { @@ -57,6 +58,12 @@ export interface AssetsWithUiAndPrice { votingBalance?: BN } +export const PORTFOLIO_CHANGE_DECIMAL = 2; + +export const changeSign = (change: number | undefined) => !change + ? '' + : change > 0 ? '+ ' : '- '; + export function adjustColor (token: string, color: string | undefined, theme: Theme): string { if (color && (TOKENS_WITH_BLACK_LOGO.find((t) => t === token) && theme.palette.mode === 'dark')) { const cleanedColor = color.replace(/^#/, ''); @@ -123,10 +130,6 @@ function TotalBalancePieChart ({ hideNumbers, setGroupedAssets }: Props): React. const [showMore, setShowMore] = useState(false); - const calPrice = useCallback((assetPrice: number | undefined, balance: BN, decimal: number) => - parseFloat(amountToHuman(balance, decimal)) * (assetPrice ?? 0), - []); - const formatNumber = useCallback( (num: number, decimal = 2) => parseFloat(Math.trunc(num) === 0 ? num.toFixed(decimal) : num.toFixed(1)) @@ -157,7 +160,7 @@ function TotalBalancePieChart ({ hideNumbers, setGroupedAssets }: Props): React. const assetPrice = pricesInCurrencies.prices[assetSample.priceId]?.value; const accumulatedPricePerAsset = groupedAssets[index].reduce((sum: BN, { totalBalance }: FetchedBalance) => sum.add(new BN(totalBalance)), BN_ZERO) as BN; - const balancePrice = calPrice(assetPrice, accumulatedPricePerAsset, assetSample.decimal ?? 0); + const balancePrice = calcPrice(assetPrice, accumulatedPricePerAsset, assetSample.decimal ?? 0); const _percent = (balancePrice / youHave.portfolio) * 100; @@ -187,7 +190,7 @@ function TotalBalancePieChart ({ hideNumbers, setGroupedAssets }: Props): React. }); return aggregatedAssets; - }, [accountsAssets, youHave, calPrice, formatNumber, pricesInCurrencies, theme]); + }, [accountsAssets, youHave, formatNumber, pricesInCurrencies, theme]); useEffect(() => { assets && setGroupedAssets([...assets]); @@ -200,17 +203,13 @@ function TotalBalancePieChart ({ hideNumbers, setGroupedAssets }: Props): React. return 0; } - const value = fixFloatingPoint(youHave.change, 2, false, true); + const value = fixFloatingPoint(youHave.change, PORTFOLIO_CHANGE_DECIMAL, false, true); return parseFloat(value); }, [youHave?.change]); - const changeSign = !youHave?.change - ? '' - : youHave.change > 0 ? '+ ' : '- '; - return ( - + {t('My Portfolio')} @@ -233,10 +232,10 @@ function TotalBalancePieChart ({ hideNumbers, setGroupedAssets }: Props): React. /> 0 ? 'success.main' : 'warning.main', fontSize: '18px', fontWeight: 500 }}> diff --git a/packages/extension-polkagate/src/fullscreen/homeFullScreen/partials/WatchList.tsx b/packages/extension-polkagate/src/fullscreen/homeFullScreen/partials/WatchList.tsx index 557f15e35..baff88fae 100644 --- a/packages/extension-polkagate/src/fullscreen/homeFullScreen/partials/WatchList.tsx +++ b/packages/extension-polkagate/src/fullscreen/homeFullScreen/partials/WatchList.tsx @@ -3,6 +3,8 @@ /* eslint-disable react/jsx-max-props-per-line */ +import type { Prices } from '../../../util/types'; +import type { CurrencyItemType } from './Currency'; import type { AssetsWithUiAndPrice } from './TotalBalancePieChart'; import { ArrowDropDown as ArrowDropDownIcon, ArrowDropDown as DownIcon, ArrowDropUp as UpIcon } from '@mui/icons-material'; @@ -15,13 +17,16 @@ import { useCurrency, usePrices, useTranslation } from '../../../hooks'; import getLogo2 from '../../../util/getLogo2'; interface Props { - groupedAssets: AssetsWithUiAndPrice[] | undefined + groupedAssets: AssetsWithUiAndPrice[] | undefined; } -const AssetPriceChange = ({ asset }: { asset: AssetsWithUiAndPrice }) => { - const currency = useCurrency(); - const pricesInCurrencies = usePrices(); +interface AssetPriceChangeProps { + asset: AssetsWithUiAndPrice; + currency: CurrencyItemType | undefined; + pricesInCurrencies: Prices | null | undefined; +} +const AssetPriceChange = React.memo(function AssetPriceChange ({ asset, currency, pricesInCurrencies }: AssetPriceChangeProps) { const logoInfo = useMemo(() => asset && getLogo2(asset.genesisHash, asset.token), [asset]); const change = pricesInCurrencies ? pricesInCurrencies.prices[asset.priceId]?.change : undefined; @@ -53,11 +58,13 @@ const AssetPriceChange = ({ asset }: { asset: AssetsWithUiAndPrice }) => { ); -}; +}); function WatchList ({ groupedAssets }: Props): React.ReactElement { const { t } = useTranslation(); const theme = useTheme(); + const currency = useCurrency(); + const pricesInCurrencies = usePrices(); const [showMore, setShowMore] = useState(false); @@ -83,7 +90,9 @@ function WatchList ({ groupedAssets }: Props): React.ReactElement { {uniqueAssets.slice(0, 3).map((asset, index) => ( ))} {uniqueAssets.length > 3 && @@ -92,7 +101,9 @@ function WatchList ({ groupedAssets }: Props): React.ReactElement { {uniqueAssets.slice(3).map((asset, index) => ( ))} diff --git a/packages/extension-polkagate/src/hooks/useYouHave.ts b/packages/extension-polkagate/src/hooks/useYouHave.ts index ac3ef4de2..ae57f3147 100644 --- a/packages/extension-polkagate/src/hooks/useYouHave.ts +++ b/packages/extension-polkagate/src/hooks/useYouHave.ts @@ -3,12 +3,11 @@ import type { BN } from '@polkadot/util'; -import { useCallback, useContext, useMemo } from 'react'; +import { useContext, useMemo } from 'react'; import { AccountsAssetsContext } from '../components'; -import { ASSETS_AS_CURRENCY_LIST } from '../util/currencyList'; import { amountToHuman } from '../util/utils'; -import { useCurrency, usePrices } from '.'; +import { usePrices } from '.'; export interface YouHaveType { change: number; @@ -16,6 +15,18 @@ export interface YouHaveType { portfolio: number; } +export const calcPrice = (assetPrice: number | undefined, balance: BN, decimal: number) => parseFloat(amountToHuman(balance, decimal)) * (assetPrice ?? 0); + +export const calcChange = (tokenPrice: number, tokenBalance: number, tokenPriceChange: number) => { + if (tokenPriceChange === -100) { + return 0; + } + + const totalChange = (tokenPriceChange * tokenBalance) / 100; + + return totalChange * tokenPrice; +}; + /** * @description * returns all user portfolio balance in selected currency @@ -24,20 +35,6 @@ export interface YouHaveType { export default function useYouHave (): YouHaveType | undefined | null { const pricesInCurrencies = usePrices(); const { accountsAssets } = useContext(AccountsAssetsContext); - const currency = useCurrency(); - - const calcPrice = useCallback( - (assetPrice: number | undefined, balance: BN, decimal: number) => - parseFloat(amountToHuman(balance, decimal)) * (assetPrice ?? 0) - , []); - - const calcChange = useCallback((currentAssetPrice: number, change: number) => { - if (change === -100) { - return 0; - } - - return currentAssetPrice && change ? currentAssetPrice / (1 + (change / 100)) : 0; - }, []); const youHave = useMemo(() => { if (!accountsAssets?.balances) { @@ -56,20 +53,18 @@ export default function useYouHave (): YouHaveType | undefined | null { Object.keys(balances).forEach((address) => { Object.keys(balances?.[address]).forEach((genesisHash) => { balances?.[address]?.[genesisHash].forEach((asset) => { - const currentAssetPrice = calcPrice(pricesInCurrencies.prices[asset.priceId]?.value ?? 0, asset.totalBalance, asset.decimal); + const tokenValue = pricesInCurrencies.prices[asset.priceId]?.value ?? 0; + const tokenPriceChange = pricesInCurrencies.prices[asset.priceId]?.change ?? 0; + const currentAssetPrice = calcPrice(tokenValue, asset.totalBalance, asset.decimal); totalPrice += currentAssetPrice; - totalBeforeChange += calcChange(currentAssetPrice, pricesInCurrencies.prices[asset.priceId]?.change); + totalBeforeChange += calcChange(tokenValue, Number(asset.totalBalance) / (10 ** asset.decimal), tokenPriceChange); }); }); }); - const change = currency?.code - ? ASSETS_AS_CURRENCY_LIST.includes(currency.code.toUpperCase()) ? 0 : totalPrice - totalBeforeChange - : 0; - - return { change, date, portfolio: totalPrice } as unknown as YouHaveType; - }, [accountsAssets, calcChange, calcPrice, currency, pricesInCurrencies]); + return { change: totalBeforeChange, date, portfolio: totalPrice } as unknown as YouHaveType; + }, [accountsAssets, pricesInCurrencies]); return youHave; }