Skip to content

Commit

Permalink
feat: add portfolio price change (#1641)
Browse files Browse the repository at this point in the history
  • Loading branch information
Nick-1979 authored Nov 11, 2024
1 parent a01d177 commit 8e4eacb
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 37 deletions.
7 changes: 4 additions & 3 deletions packages/extension-polkagate/src/components/FormatPrice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import React, { useMemo } from 'react';

import { useCurrency } from '../hooks';
import { ASSETS_AS_CURRENCY_LIST } from '../util/currencyList';
import { amountToHuman } from '../util/utils';
import { amountToHuman, fixFloatingPoint } from '../util/utils';

interface Props {
amount?: BN | null;
decimalPoint?: number;
decimals?: number;
commify?: boolean;
fontSize?: string;
fontWeight?: number;
lineHeight?: number;
Expand Down Expand Up @@ -53,7 +54,7 @@ export function nFormatter (num: number, decimalPoint: number) {

const DECIMAL_POINTS_FOR_CRYPTO_AS_CURRENCY = 4;

function FormatPrice ({ amount, decimalPoint = 2, decimals, fontSize, fontWeight, height, lineHeight = 1, mt = '0px', num, price, sign, skeletonHeight = 15, textAlign = 'left', textColor, width = '90px' }: Props): React.ReactElement<Props> {
function FormatPrice ({ amount, commify, decimalPoint = 2, decimals, fontSize, fontWeight, height, lineHeight = 1, mt = '0px', num, price, sign, skeletonHeight = 15, textAlign = 'left', textColor, width = '90px' }: Props): React.ReactElement<Props> {
const currency = useCurrency();

const total = useMemo(() => {
Expand Down Expand Up @@ -90,7 +91,7 @@ function FormatPrice ({ amount, decimalPoint = 2, decimals, fontSize, fontWeight
lineHeight={lineHeight}
sx={{ color: textColor }}
>
{sign || currency?.sign || ''}{nFormatter(total as number, _decimalPoint)}
{sign || currency?.sign || ''}{ commify ? fixFloatingPoint(total as number, _decimalPoint, true) : nFormatter(total as number, _decimalPoint)}
</Typography>
: <Skeleton
animation='wave'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ const AccountTotal = ({ hideNumbers, totalBalance }: { hideNumbers: boolean | un
hideNumbers || hideNumbers === undefined
? <Box component='img' src={(theme.palette.mode === 'dark' ? stars6White : stars6Black) as string} sx={{ height: '36px', width: '154px' }} />
: <FormatPrice
fontSize='32px'
fontSize='28px'
fontWeight={700}
num={totalBalance}
skeletonHeight={28}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ import { BN, BN_ZERO } from '@polkadot/util';
import { stars6Black, stars6White } from '../../../assets/icons';
import { AccountsAssetsContext, AssetLogo } from '../../../components';
import FormatPrice from '../../../components/FormatPrice';
import { usePrices, useTranslation, useYouHave } from '../../../hooks';
import { useCurrency, usePrices, useTranslation, useYouHave } from '../../../hooks';
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 } from '../../../util/utils';
import { amountToHuman, fixFloatingPoint } from '../../../util/utils';
import Chart from './Chart';

interface Props {
Expand Down Expand Up @@ -112,6 +113,7 @@ const DisplayAssetRow = ({ asset, hideNumbers }: { asset: AssetsWithUiAndPrice,
function TotalBalancePieChart ({ hideNumbers, setGroupedAssets }: Props): React.ReactElement {
const theme = useTheme();
const { t } = useTranslation();
const currency = useCurrency();

const pricesInCurrencies = usePrices();
const youHave = useYouHave();
Expand All @@ -121,7 +123,7 @@ function TotalBalancePieChart ({ hideNumbers, setGroupedAssets }: Props): React.
const [showMore, setShowMore] = useState<boolean>(false);

const calPrice = useCallback((assetPrice: number | undefined, balance: BN, decimal: number) =>
parseFloat(amountToHuman(balance, decimal)) * (assetPrice ?? 0),
parseFloat(amountToHuman(balance, decimal)) * (assetPrice ?? 0),
[]);

const formatNumber = useCallback(
Expand All @@ -140,13 +142,13 @@ function TotalBalancePieChart ({ hideNumbers, setGroupedAssets }: Props): React.
Object.keys(balances).forEach((address) => {
Object.keys(balances?.[address]).forEach((genesisHash) => {
if (!TEST_NETS.includes(genesisHash)) {
//@ts-ignore
// @ts-ignore
allAccountsAssets = allAccountsAssets.concat(balances[address][genesisHash]);
}
});
});

//@ts-ignore
// @ts-ignore
const groupedAssets = Object.groupBy(allAccountsAssets, ({ genesisHash, token }: { genesisHash: string, token: string }) => `${token}_${genesisHash}`);
const aggregatedAssets = Object.keys(groupedAssets).map((index) => {
const assetSample = groupedAssets[index][0] as AssetsWithUiAndPrice;
Expand Down Expand Up @@ -193,25 +195,32 @@ function TotalBalancePieChart ({ hideNumbers, setGroupedAssets }: Props): React.
const toggleAssets = useCallback(() => setShowMore(!showMore), [showMore]);

return (
<Grid alignItems='center' container direction='column' item justifyContent='center' sx={{ bgcolor: 'background.paper', borderRadius: '5px', boxShadow: '2px 3px 4px 0px rgba(0, 0, 0, 0.1)', height: 'fit-content', p: '15px 25px 10px', width: '430px' }}>
<Grid alignItems='center' container gap='15px' item justifyContent='center'>
<Typography sx={{ fontSize: '28px', fontVariant: 'small-caps', fontWeight: 400 }}>
<Grid alignItems='flex-start' container direction='column' item justifyContent='flex-start' sx={{ bgcolor: 'background.paper', borderRadius: '5px', boxShadow: '2px 3px 4px 0px rgba(0, 0, 0, 0.1)', minHeight: '287px', p: '15px 25px 10px', width: '430px' }}>
<Grid alignItems='flex-start' container item justifyContent='flex-start'>
<Typography sx={{ fontSize: '23px', fontVariant: 'small-caps', fontWeight: 400 }}>
{t('My Portfolio')}
</Typography>
{hideNumbers || hideNumbers === undefined
? <Box
component='img'
src={(theme.palette.mode === 'dark' ? stars6White : stars6Black) as string}
sx={{ height: '60px', width: '154px' }}
/>
: <FormatPrice
fontSize='40px'
fontWeight={700}
lineHeight={1.5}
num={youHave?.portfolio}
textColor= { isPriceOutdated(youHave) ? 'primary.light' : 'text.primary'}
/>
}
<Grid alignItems='center' container item justifyContent = 'space-between' sx={{ my: '13px' }}>
{hideNumbers || hideNumbers === undefined || !youHave
? <Box
component='img'
src={(theme.palette.mode === 'dark' ? stars6White : stars6Black) as string}
sx={{ height: '32px', width: '154px' }}
/>
: <>
<FormatPrice
commify
fontSize='32px'
fontWeight={700}
num={youHave?.portfolio}
textColor= { isPriceOutdated(youHave) ? 'primary.light' : 'text.primary'}
/>
<Typography sx={{ color: youHave.change > 0 ? 'success.main' : 'warning.main', fontSize: '18px', fontWeight: 500 }}>
{youHave.change > 0 ? '+ ' : '- '}{currency?.sign}{ fixFloatingPoint(youHave?.change, 2, true, true)} {`(${COIN_GECKO_PRICE_CHANGE_DURATION}h)`}
</Typography>
</>
}
</Grid>
</Grid>
{youHave?.portfolio !== 0 && assets && assets.length > 0 &&
<Grid container item sx={{ borderTop: '1px solid', borderTopColor: 'divider', pt: '10px' }}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ function WatchList ({ groupedAssets }: Props): React.ReactElement {
<Divider sx={{ bgcolor: 'divider', height: '2px', mt: '10px', width: '100%' }} />
<Grid alignItems='center' container item onClick={toggleAssets} sx={{ cursor: 'pointer', p: '5px', width: 'fit-content' }}>
<Typography color='secondary.light' fontSize='14px' fontWeight={400}>
{t<string>(showMore ? t('Less') : t('More'))}
{t(showMore ? t('Less') : t('More'))}
</Typography>
<ArrowDropDownIcon sx={{ color: 'secondary.light', fontSize: '20px', stroke: theme.palette.secondary.light, strokeWidth: '2px', transform: showMore ? 'rotate(-180deg)' : 'rotate(0deg)', transitionDuration: '0.2s', transitionProperty: 'transform' }} />
</Grid>
Expand Down
27 changes: 21 additions & 6 deletions packages/extension-polkagate/src/hooks/useYouHave.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import { amountToHuman } from '../util/utils';
import { usePrices } from '.';

export interface YouHaveType {
portfolio: number,
date: number
change: number;
date: number;
portfolio: number;
}

/**
Expand All @@ -23,11 +24,19 @@ export default function useYouHave (): YouHaveType | undefined | null {
const pricesInCurrencies = usePrices();
const { accountsAssets } = useContext(AccountsAssetsContext);

const calPrice = useCallback(
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) {
return null;
Expand All @@ -38,19 +47,25 @@ export default function useYouHave (): YouHaveType | undefined | null {
}

let totalPrice = 0;
let totalBeforeChange = 0;
const balances = accountsAssets.balances;
const date = Math.min(accountsAssets.timeStamp, pricesInCurrencies.date);

Object.keys(balances).forEach((address) => {
Object.keys(balances?.[address]).forEach((genesisHash) => {
balances?.[address]?.[genesisHash].forEach((asset) => {
totalPrice += calPrice(pricesInCurrencies.prices[asset.priceId]?.value ?? 0, asset.totalBalance, asset.decimal);
const currentAssetPrice = calcPrice(pricesInCurrencies.prices[asset.priceId]?.value ?? 0, asset.totalBalance, asset.decimal);

totalPrice += currentAssetPrice;
totalBeforeChange += calcChange(currentAssetPrice, pricesInCurrencies.prices[asset.priceId]?.change);
});
});
});

return { date, portfolio: totalPrice } as unknown as YouHaveType;
}, [accountsAssets, calPrice, pricesInCurrencies]);
const change = totalPrice - totalBeforeChange;

return { change, date, portfolio: totalPrice } as unknown as YouHaveType;
}, [accountsAssets, calcChange, calcPrice, pricesInCurrencies]);

return youHave;
}
2 changes: 1 addition & 1 deletion packages/extension-polkagate/src/popup/home/YouHave.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export default function YouHave ({ hideNumbers, setHideNumbers }: Props): React.
/>
: <Grid item pr='15px'>
<FormatPrice
fontSize='42px'
fontSize='38px'
fontWeight={500}
height={36}
num={youHave?.portfolio }
Expand Down
4 changes: 3 additions & 1 deletion packages/extension-polkagate/src/util/api/getPrices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ export const EXTRA_PRICE_IDS: Record<string, string> = {
pendulum: 'pendulum-chain'
};

export const COIN_GECKO_PRICE_CHANGE_DURATION = 24;

export default async function getPrices (priceIds: string[], currencyCode = 'usd') {
console.log(' getting prices for:', priceIds.sort());

const revisedPriceIds = priceIds.map((item) => (EXTRA_PRICE_IDS[item] || item));

const prices = await getReq(`https://api.coingecko.com/api/v3/simple/price?ids=${revisedPriceIds}&vs_currencies=${currencyCode}&include_24hr_change=true`, {});
const prices = await getReq(`https://api.coingecko.com/api/v3/simple/price?ids=${revisedPriceIds}&vs_currencies=${currencyCode}&include_${COIN_GECKO_PRICE_CHANGE_DURATION}hr_change=true`, {});

const outputObjectPrices: PricesType = {};

Expand Down
25 changes: 23 additions & 2 deletions packages/extension-polkagate/src/util/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,19 @@ export function isValidAddress (_address: string | undefined): boolean {
}
}

export function fixFloatingPoint (_number: number | string, decimalDigit = FLOATING_POINT_DIGIT, commify?: boolean): string {
function countLeadingZerosInFraction (numStr: string) {
const match = numStr.match(/\.(0+)/);

if (match) {
return match[1].length;
}

return 0;
}

export function fixFloatingPoint (_number: number | string, decimalDigit = FLOATING_POINT_DIGIT, commify?: boolean, dynamicDecimal?: boolean): string {
const MAX_DECIMAL_POINTS = 6;

// make number positive if it is negative
const sNumber = Number(_number) < 0 ? String(-Number(_number)) : String(_number);

Expand All @@ -52,9 +64,18 @@ export function fixFloatingPoint (_number: number | string, decimalDigit = FLOAT

let integerDigits = sNumber.slice(0, dotIndex);

integerDigits = commify ? Number(integerDigits).toLocaleString() : integerDigits;
if (integerDigits === '0' && dynamicDecimal) { // to show very small numbers such as 0.0000001123
const leadingZerosInFraction = countLeadingZerosInFraction(sNumber);

if (leadingZerosInFraction > 0 && leadingZerosInFraction < MAX_DECIMAL_POINTS) {
decimalDigit = leadingZerosInFraction + 1;
}
}

const fractionalDigits = sNumber.slice(dotIndex, dotIndex + decimalDigit + 1);

integerDigits = commify ? Number(integerDigits).toLocaleString() : integerDigits;

return integerDigits + fractionalDigits;
}

Expand Down

0 comments on commit 8e4eacb

Please sign in to comment.