diff --git a/apps/trading-e2e/src/integration/withdraw.cy.ts b/apps/trading-e2e/src/integration/withdraw.cy.ts index 00329c0bb..47989bf5d 100644 --- a/apps/trading-e2e/src/integration/withdraw.cy.ts +++ b/apps/trading-e2e/src/integration/withdraw.cy.ts @@ -77,18 +77,24 @@ describe('withdraw', { tags: '@smoke' }, () => { }, }); selectAsset(asset1Name); - cy.getByTestId('balance-available') - .should('contain.text', 'Balance available') - .find('td') - .should('have.text', '1,000.00000'); - cy.getByTestId('withdrawal-threshold') - .should('contain.text', 'Delayed withdrawal threshold') - .find('td') - .should('contain.text', '1m+'); - cy.getByTestId('delay-time') - .should('contain.text', 'Delay time') - .find('td') - .should('have.text', 'None'); + cy.getByTestId('BALANCE_AVAILABLE_label').should( + 'contain.text', + 'Balance available' + ); + cy.getByTestId('BALANCE_AVAILABLE_value').should( + 'have.text', + '1,000.00000' + ); + cy.getByTestId('WITHDRAWAL_THRESHOLD_label').should( + 'contain.text', + 'Delayed withdrawal threshold' + ); + cy.getByTestId('WITHDRAWAL_THRESHOLD_value').should( + 'contain.text', + '100.00000' + ); + cy.getByTestId('DELAY_TIME_label').should('contain.text', 'Delay time'); + cy.getByTestId('DELAY_TIME_value').should('have.text', 'None'); cy.get(amountField).clear().type('10'); cy.getByTestId(submitWithdrawBtn).click(); cy.getByTestId('dialog-title').should( diff --git a/libs/deposits/src/lib/deposit-form.spec.tsx b/libs/deposits/src/lib/deposit-form.spec.tsx index 4c4632918..29fa82f8e 100644 --- a/libs/deposits/src/lib/deposit-form.spec.tsx +++ b/libs/deposits/src/lib/deposit-form.spec.tsx @@ -227,20 +227,12 @@ describe('Deposit form', () => { ); // Check deposit limit is displayed - expect( - screen.getByText('Balance available', { selector: 'th' }) - .nextElementSibling - ).toHaveTextContent(balance.toString()); - expect( - screen.getByText('Maximum total deposit amount', { selector: 'th' }) - .nextElementSibling - ).toHaveTextContent(max.toString()); - expect( - screen.getByText('Deposited', { selector: 'th' }).nextElementSibling - ).toHaveTextContent(deposited.toString()); - expect( - screen.getByText('Remaining', { selector: 'th' }).nextElementSibling - ).toHaveTextContent(max.minus(deposited).toString()); + expect(screen.getByTestId('BALANCE_AVAILABLE_value')).toHaveTextContent( + '50' + ); + expect(screen.getByTestId('MAX_LIMIT_value')).toHaveTextContent('20'); + expect(screen.getByTestId('DEPOSITED_value')).toHaveTextContent('10'); + expect(screen.getByTestId('REMAINING_value')).toHaveTextContent('10'); fireEvent.change(screen.getByLabelText('Amount'), { target: { value: '8' }, diff --git a/libs/deposits/src/lib/deposit-form.tsx b/libs/deposits/src/lib/deposit-form.tsx index 54143ee32..08aa7d3f5 100644 --- a/libs/deposits/src/lib/deposit-form.tsx +++ b/libs/deposits/src/lib/deposit-form.tsx @@ -220,7 +220,12 @@ export const DepositForm = ({ {selectedAsset && max && deposited && (
- +
)} diff --git a/libs/deposits/src/lib/deposit-limits.tsx b/libs/deposits/src/lib/deposit-limits.tsx index 4bfe63249..c026fe1ee 100644 --- a/libs/deposits/src/lib/deposit-limits.tsx +++ b/libs/deposits/src/lib/deposit-limits.tsx @@ -1,60 +1,65 @@ -import { formatNumber, t } from '@vegaprotocol/react-helpers'; +import type { Asset } from '@vegaprotocol/assets'; +import { compactNumber, t } from '@vegaprotocol/react-helpers'; +import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit'; import type BigNumber from 'bignumber.js'; +// Note: all of the values here are with correct asset's decimals +// See `libs/deposits/src/lib/use-deposit-balances.ts` + interface DepositLimitsProps { max: BigNumber; deposited: BigNumber; + asset: Asset; balance?: BigNumber; } export const DepositLimits = ({ max, deposited, + asset, balance, }: DepositLimitsProps) => { - let maxLimit = ''; - if (max.isEqualTo(Infinity)) { - maxLimit = t('No limit'); - } else if (max.isGreaterThan(1_000_000)) { - maxLimit = t('1m+'); - } else { - maxLimit = max.toString(); - } - - let remaining = ''; - if (deposited.isEqualTo(0)) { - remaining = maxLimit; - } else { - const amountRemaining = max.minus(deposited); - remaining = amountRemaining.isGreaterThan(1_000_000) - ? t('1m+') - : amountRemaining.toString(); - } + const limits = [ + { + key: 'BALANCE_AVAILABLE', + label: t('Balance available'), + rawValue: balance, + value: balance ? compactNumber(balance, asset.decimals) : '-', + }, + { + key: 'MAX_LIMIT', + label: t('Maximum total deposit amount'), + rawValue: max, + value: compactNumber(max, asset.decimals), + }, + { + key: 'DEPOSITED', + label: t('Deposited'), + rawValue: deposited, + value: compactNumber(deposited, asset.decimals), + }, + { + key: 'REMAINING', + label: t('Remaining'), + rawValue: max.minus(deposited), + value: compactNumber(max.minus(deposited), asset.decimals), + }, + ]; return ( - - - - - - - - - - - - - - - - - - - -
{t('Balance available')} - {balance ? formatNumber(balance) : '-'} -
- {t('Maximum total deposit amount')} - {maxLimit}
{t('Deposited')}{formatNumber(deposited)}
{t('Remaining')}{remaining}
+ + {limits.map(({ key, label, rawValue, value }) => ( + +
{label}
+
+ {value} +
+
+ ))} +
); }; diff --git a/libs/react-helpers/src/lib/format/number.spec.ts b/libs/react-helpers/src/lib/format/number.spec.tsx similarity index 53% rename from libs/react-helpers/src/lib/format/number.spec.ts rename to libs/react-helpers/src/lib/format/number.spec.tsx index 331b3f336..cf5d4ff34 100644 --- a/libs/react-helpers/src/lib/format/number.spec.ts +++ b/libs/react-helpers/src/lib/format/number.spec.tsx @@ -4,6 +4,7 @@ import { formatNumberPercentage, toNumberParts, isNumeric, + compactNumber, } from './number'; describe('formatNumber and formatNumberPercentage', () => { @@ -89,3 +90,93 @@ describe('isNumeric', () => { } ); }); + +describe('compactNumber', () => { + const short: [BigNumber, string | JSX.Element, number | 'infer'][] = [ + [new BigNumber(Infinity), '∞', 'infer'], + [new BigNumber(-Infinity), '-∞', 'infer'], + [new BigNumber(0), '0', 'infer'], + [new BigNumber(1), '1', 'infer'], + [new BigNumber(100), '100', 'infer'], + [new BigNumber(100.456601), '100.456601', 'infer'], + [new BigNumber(1_000), '1,000', 'infer'], + [new BigNumber(999_999), '999,999', 'infer'], + [new BigNumber(1_000_000), '1M', 'infer'], + [new BigNumber(100_000_000), '100M', 'infer'], + [new BigNumber(1_000_000_000), '1B', 'infer'], + [new BigNumber(1_000_000_000_000), '1T', 'infer'], + [new BigNumber(3.23e12), '3.23T', 2], + [new BigNumber(3.23e12), '3.23000T', 5], + [ + new BigNumber(3.23e24), + + 3.23000{' '} + + × 1024 + + , + 5, + ], + [ + new BigNumber(1.579208923731619e59), + + 1.57921{' '} + + × 10 + 59 + + , + 5, + ], + ]; + it.each(short)( + 'compacts %d to %p (decimal places: %p)', + (input, output, decimals) => { + expect(compactNumber(input, decimals)).toEqual(output); + } + ); + + const long: [BigNumber, string | JSX.Element, number | 'infer'][] = [ + [new BigNumber(Infinity), '∞', 'infer'], + [new BigNumber(-Infinity), '-∞', 'infer'], + [new BigNumber(0), '0', 'infer'], + [new BigNumber(1), '1', 'infer'], + [new BigNumber(100), '100', 'infer'], + [new BigNumber(100.456601), '100.456601', 'infer'], + [new BigNumber(1_000), '1,000', 'infer'], + [new BigNumber(999_999), '999,999', 'infer'], + [new BigNumber(1_000_000), '1 million', 'infer'], + [new BigNumber(100_000_000), '100 million', 'infer'], + [new BigNumber(1_000_000_000), '1 billion', 'infer'], + [new BigNumber(1_000_000_000_000), '1 trillion', 'infer'], + [new BigNumber(3.23e12), '3.23 trillion', 2], + [new BigNumber(3.23e12), '3.23000 trillion', 5], + [ + new BigNumber(3.23e24), + + 3.23000{' '} + + × 1024 + + , + 5, + ], + [ + new BigNumber(1.579208923731619e59), + + 1.57921{' '} + + × 10 + 59 + + , + 5, + ], + ]; + it.each(long)( + 'compacts %d to %p (decimal places: %p)', + (input, output, decimals) => { + expect(compactNumber(input, decimals, 'long')).toEqual(output); + } + ); +}); diff --git a/libs/react-helpers/src/lib/format/number.ts b/libs/react-helpers/src/lib/format/number.tsx similarity index 59% rename from libs/react-helpers/src/lib/format/number.ts rename to libs/react-helpers/src/lib/format/number.tsx index 2f309cad6..2c93b8279 100644 --- a/libs/react-helpers/src/lib/format/number.ts +++ b/libs/react-helpers/src/lib/format/number.tsx @@ -100,3 +100,67 @@ export const useNumberParts = ( export const isNumeric = ( value?: string | number | BigNumber | null ): value is NonNullable => /^-?\d*\.?\d+$/.test(String(value)); + +const INFINITY = '∞'; +const DEFAULT_COMPACT_ABOVE = 1_000_000; +const DEFAULT_COMPACT_CAP = new BigNumber(1e24); +/** + * Compacts given number to human readable format. + * @param number + * @param decimals Number of decimal places + * @param compactDisplay Display mode; short -> 1e6 == 1M; ling -> 1e6 1 million + * @param compactAbove Compact number above threshold + * @param cap Use scientific notation above threshold + */ +export const compactNumber = ( + number: BigNumber, + decimals: number | 'infer' = 'infer', + compactDisplay: 'short' | 'long' = 'short', + compactAbove = DEFAULT_COMPACT_ABOVE, + cap = DEFAULT_COMPACT_CAP +) => { + if (!number.isFinite()) return `${number.isNegative() ? '-' : ''}${INFINITY}`; + + const decimalPlaces = + (decimals === 'infer' ? number.decimalPlaces() : decimals) || 0; + + if (number.isLessThan(DEFAULT_COMPACT_ABOVE)) { + return formatNumber(number, decimalPlaces); + } + + /** + * Note: it compacts number up to 1_000_000_000_000 (1e12) -> 1T, all above is formatted as iteration of T. + * Example: 1.579208923731619e59 -> 157,920,892,373,161,900,000,000,000,000,000,000,000,000,000,000T + */ + const compactNumFormat = new Intl.NumberFormat(getUserLocale(), { + minimumFractionDigits: Math.max(0, decimalPlaces), + maximumFractionDigits: Math.max(0, decimalPlaces), + notation: 'compact', + compactDisplay, + }); + const scientificNumFormat = new Intl.NumberFormat(getUserLocale(), { + minimumFractionDigits: Math.max(0, decimalPlaces), + maximumFractionDigits: Math.max(0, decimalPlaces), + notation: 'scientific', + }); + + if (number.isGreaterThan(DEFAULT_COMPACT_CAP)) { + const r = /E(\d+)$/i; + const formatted = scientificNumFormat.format(Number(number)); + const eNotation = formatted.match(r); + if (eNotation && eNotation.length > 1) { + const power = eNotation[1]; + return ( + + {formatted.replace(r, '')}{' '} + + × 10 + {power} + + + ); + } + } + + return compactNumFormat.format(Number(number)); +}; diff --git a/libs/withdraws/src/lib/withdraw-form.tsx b/libs/withdraws/src/lib/withdraw-form.tsx index ccf90a0fd..60eac34e9 100644 --- a/libs/withdraws/src/lib/withdraw-form.tsx +++ b/libs/withdraws/src/lib/withdraw-form.tsx @@ -1,5 +1,4 @@ import type { Asset } from '@vegaprotocol/assets'; -import { formatNumber } from '@vegaprotocol/react-helpers'; import { ethereumAddress, minSafe, @@ -147,7 +146,8 @@ export const WithdrawForm = ({ amount={amount} threshold={threshold} delay={delay} - balance={formatNumber(balance, selectedAsset.decimals)} + balance={balance} + asset={selectedAsset} /> )} diff --git a/libs/withdraws/src/lib/withdraw-limits.tsx b/libs/withdraws/src/lib/withdraw-limits.tsx index cd94fc799..2432321e0 100644 --- a/libs/withdraws/src/lib/withdraw-limits.tsx +++ b/libs/withdraws/src/lib/withdraw-limits.tsx @@ -1,12 +1,15 @@ -import { t } from '@vegaprotocol/react-helpers'; +import type { Asset } from '@vegaprotocol/assets'; +import { compactNumber, t } from '@vegaprotocol/react-helpers'; +import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit'; import BigNumber from 'bignumber.js'; import { formatDistanceToNow } from 'date-fns'; interface WithdrawLimitsProps { amount: string; threshold: BigNumber; - balance: string; + balance: BigNumber; delay: number | undefined; + asset: Asset; } export const WithdrawLimits = ({ @@ -14,40 +17,47 @@ export const WithdrawLimits = ({ threshold, balance, delay, + asset, }: WithdrawLimitsProps) => { - let text = ''; - - if (threshold.isEqualTo(Infinity)) { - text = t('No limit'); - } else if (threshold.isGreaterThan(1_000_000)) { - text = t('1m+'); - } else { - text = threshold.toString(); - } - const delayTime = new BigNumber(amount).isGreaterThan(threshold) && delay ? formatDistanceToNow(Date.now() + delay * 1000) : t('None'); + const limits = [ + { + key: 'BALANCE_AVAILABLE', + label: t('Balance available'), + rawValue: balance, + value: balance ? compactNumber(balance, asset.decimals) : '-', + }, + { + key: 'WITHDRAWAL_THRESHOLD', + label: t('Delayed withdrawal threshold'), + rawValue: threshold, + value: compactNumber(threshold, asset.decimals), + }, + { + key: 'DELAY_TIME', + label: t('Delay time'), + value: delayTime, + }, + ]; + return ( - - - - - - - - - - - - - - - -
{t('Balance available')}{balance}
- {t('Delayed withdrawal threshold')} - {text}
{t('Delay time')}{delayTime}
+ + {limits.map(({ key, label, rawValue, value }) => ( + +
{label}
+
+ {value} +
+
+ ))} +
); };