diff --git a/ui-react/src/__tests__/utils/text.test.ts b/ui-react/src/__tests__/utils/text.test.ts index 3ad32e9..3609b27 100644 --- a/ui-react/src/__tests__/utils/text.test.ts +++ b/ui-react/src/__tests__/utils/text.test.ts @@ -2,6 +2,7 @@ import BigNumber from 'bignumber.js'; import { getThemeColor, checkIsExtrinsicIndex, + formatBalanceAmount, formatHash, formatNumber, getBalanceAmount, @@ -92,6 +93,20 @@ describe('Utils - text', () => { }); }); + describe('formatBalanceAmount', () => { + it('should compact million-scale balances', () => { + expect(formatBalanceAmount(new BigNumber('12064779'))).toBe('12.0648 Million'); + }); + + it('should keep ordinary balances readable', () => { + expect(formatBalanceAmount(new BigNumber('255969555555555554095805'), 18)).toBe('255,969.555556'); + }); + + it('should keep small balances readable', () => { + expect(formatBalanceAmount(new BigNumber('123456789'), 18)).toBe('< 0.00000001'); + }); + }); + describe('timeAgo', () => { const mockNow = new Date(2025, 5, 15, 12, 0, 0).getTime(); // 2025-05-15 12:00:00 diff --git a/ui-react/src/components/account/accountTable.tsx b/ui-react/src/components/account/accountTable.tsx index 1631423..9122522 100644 --- a/ui-react/src/components/account/accountTable.tsx +++ b/ui-react/src/components/account/accountTable.tsx @@ -2,7 +2,7 @@ import React, { useMemo } from 'react' import { BareProps } from '@/types/page' import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, getKeyValue, Spinner } from '@heroui/react' -import { formatHash, getBalanceAmount, getThemeColor } from '@/utils/text' +import { formatBalanceAmount, formatHash, getThemeColor } from '@/utils/text' import { getExtrinsicListParams, unwrap, useAccounts } from '@/utils/api' import { PAGE_SIZE } from '@/utils/const' import { useData } from '@/context' @@ -67,7 +67,7 @@ const Component: React.FC = ({ children, className, args }) => { {(columnKey) => { if (columnKey === 'balance') { - return {getBalanceAmount(new BigNumber(item.balance), token?.decimals).toFormat()} + return {formatBalanceAmount(new BigNumber(item.balance), token?.decimals)} } else if (columnKey === 'address') { return ( diff --git a/ui-react/src/components/erc20Token/tokenHolderTable.tsx b/ui-react/src/components/erc20Token/tokenHolderTable.tsx index 9f978c8..8ab46bf 100644 --- a/ui-react/src/components/erc20Token/tokenHolderTable.tsx +++ b/ui-react/src/components/erc20Token/tokenHolderTable.tsx @@ -5,7 +5,7 @@ import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, getKey import { getPVMTokenHolderListParams, pvmTokenType, unwrap, usePVMTokenHolders } from '@/utils/api' import { PAGE_SIZE } from '@/utils/const' import BigNumber from 'bignumber.js' -import { getBalanceAmount, getThemeColor } from '@/utils/text' +import { formatBalanceAmount, getThemeColor } from '@/utils/text' import { Link } from '../link' import { CursorPagination } from '../cursorPagination' import { env } from 'next-runtime-env' @@ -73,7 +73,7 @@ const Component: React.FC = ({ args, token, children, className }) => { ) } else if (columnKey === 'balance') { - return {getBalanceAmount(new BigNumber(item.balance), token.decimals).toFormat()} + return {formatBalanceAmount(new BigNumber(item.balance), token.decimals)} } return {getKeyValue(item, columnKey)} }} diff --git a/ui-react/src/components/erc20Token/tokenTransferTable.tsx b/ui-react/src/components/erc20Token/tokenTransferTable.tsx index c83119f..01abf81 100644 --- a/ui-react/src/components/erc20Token/tokenTransferTable.tsx +++ b/ui-react/src/components/erc20Token/tokenTransferTable.tsx @@ -10,7 +10,7 @@ import { } from '@/utils/api' import { PAGE_SIZE } from '@/utils/const' import BigNumber from 'bignumber.js' -import { formatHash, getBalanceAmount, getThemeColor, timeAgo } from '@/utils/text' +import { formatBalanceAmount, formatHash, getThemeColor, timeAgo } from '@/utils/text' import { Link } from '../link' import { CursorPagination } from '../cursorPagination' import { env } from 'next-runtime-env' @@ -90,7 +90,7 @@ const Component: React.FC = ({ args, token, children, className }) => { } else if (columnKey === 'value') { return ( - {getBalanceAmount(new BigNumber(item.value), item.decimals).toFormat()} {item.symbol} + {formatBalanceAmount(new BigNumber(item.value), item.decimals)} {item.symbol} ) } else if (columnKey === 'create_at') { diff --git a/ui-react/src/components/pvmAccount/accountTable.tsx b/ui-react/src/components/pvmAccount/accountTable.tsx index ff4fca5..d528aa9 100644 --- a/ui-react/src/components/pvmAccount/accountTable.tsx +++ b/ui-react/src/components/pvmAccount/accountTable.tsx @@ -2,7 +2,7 @@ import React, { useMemo } from 'react' import { BareProps } from '@/types/page' import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, getKeyValue, Spinner } from '@heroui/react' -import { getBalanceAmount, getThemeColor } from '@/utils/text' +import { formatBalanceAmount, getThemeColor } from '@/utils/text' import { getExtrinsicListParams, unwrap, usePVMAccounts } from '@/utils/api' import { PAGE_SIZE } from '@/utils/const' import { useData } from '@/context' @@ -66,7 +66,7 @@ const Component: React.FC = ({ children, className, args }) => { {(columnKey) => { if (columnKey === 'balance') { - return {getBalanceAmount(new BigNumber(item.balance), token?.decimals).toFormat()} + return {formatBalanceAmount(new BigNumber(item.balance), token?.decimals)} } else if (columnKey === 'address') { return ( diff --git a/ui-react/src/components/pvmAccount/tokenTable.tsx b/ui-react/src/components/pvmAccount/tokenTable.tsx index 92fbec8..959cfd1 100644 --- a/ui-react/src/components/pvmAccount/tokenTable.tsx +++ b/ui-react/src/components/pvmAccount/tokenTable.tsx @@ -5,7 +5,7 @@ import { Table, Pagination, TableHeader, TableColumn, TableBody, TableRow, Table import { getPVMAccountTokenListParams, getPVMTokenTransferListParams, pvmTokenType, unwrap, usePVMAccountTokens } from '@/utils/api' import { PAGE_SIZE } from '@/utils/const' import BigNumber from 'bignumber.js' -import { formatHash, getBalanceAmount, getThemeColor, timeAgo } from '@/utils/text' +import { formatBalanceAmount, formatHash, getThemeColor, timeAgo } from '@/utils/text' import { Link } from '../link' import { env } from 'next-runtime-env' @@ -55,7 +55,7 @@ const Component: React.FC = ({ args, token, children, className }) => { {(columnKey) => { if (columnKey === 'balance') { - return {getBalanceAmount(new BigNumber(item.balance), item.decimals).toFormat()} + return {formatBalanceAmount(new BigNumber(item.balance), item.decimals)} } else if (columnKey === 'name') { return {`${item.name}(${item.symbol})`} } else if (columnKey === 'category') { diff --git a/ui-react/src/components/transfer/transferTable.tsx b/ui-react/src/components/transfer/transferTable.tsx index 7021e33..56503ba 100644 --- a/ui-react/src/components/transfer/transferTable.tsx +++ b/ui-react/src/components/transfer/transferTable.tsx @@ -2,7 +2,7 @@ import React, { useMemo } from 'react' import { BareProps } from '@/types/page' import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, getKeyValue, Spinner } from '@heroui/react' -import { formatHash, getBalanceAmount, getThemeColor, timeAgo } from '@/utils/text' +import { formatBalanceAmount, formatHash, getThemeColor, timeAgo } from '@/utils/text' import { getTransferListParams, unwrap, useTransfers } from '@/utils/api' import { PAGE_SIZE } from '@/utils/const' import { useData } from '@/context' @@ -81,7 +81,7 @@ const Component: React.FC = ({ children, className, args }) => { } else if (columnKey === 'block_timestamp') { return {timeAgo(item.block_timestamp)} } else if (columnKey === 'amount') { - return {getBalanceAmount(new BigNumber(item.amount), token?.decimals).toFormat()} + return {formatBalanceAmount(new BigNumber(item.amount), token?.decimals)} } if (columnKey === 'sender' || columnKey === 'receiver') { const address = columnKey === 'sender' ? item.sender : item.receiver diff --git a/ui-react/src/components/tx/txTable.tsx b/ui-react/src/components/tx/txTable.tsx index ac6e3af..6094525 100644 --- a/ui-react/src/components/tx/txTable.tsx +++ b/ui-react/src/components/tx/txTable.tsx @@ -2,7 +2,7 @@ import React, { useMemo } from 'react' import { BareProps } from '@/types/page' import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, getKeyValue, Spinner } from '@heroui/react' -import { formatHash, getBalanceAmount, getThemeColor, timeAgo } from '@/utils/text' +import { formatBalanceAmount, formatHash, getThemeColor, timeAgo } from '@/utils/text' import { getPVMTxListParams, unwrap, usePVMTxs } from '@/utils/api' import { PAGE_SIZE, PVM_DECIMAL } from '@/utils/const' import { useData } from '@/context' @@ -83,7 +83,7 @@ const Component: React.FC = ({ children, className, args }) => { ) } else if (columnKey === 'value') { - return {getBalanceAmount(new BigNumber(item.value), PVM_DECIMAL).toFormat()} + return {formatBalanceAmount(new BigNumber(item.value), PVM_DECIMAL)} } else if (columnKey === 'block_num') { return ( diff --git a/ui-react/src/pages/address/[id].tsx b/ui-react/src/pages/address/[id].tsx index 0307ad7..c33508e 100644 --- a/ui-react/src/pages/address/[id].tsx +++ b/ui-react/src/pages/address/[id].tsx @@ -1,7 +1,7 @@ import React from 'react' import { CardBody, Card, Tabs, Tab } from '@heroui/react' import { useRouter } from 'next/router' -import { getBalanceAmount, getThemeColor } from '@/utils/text' +import { formatBalanceAmount, getThemeColor } from '@/utils/text' import { unwrap, usePVMAccounts } from '@/utils/api' import { useData } from '@/context' import BigNumber from 'bignumber.js' @@ -44,7 +44,7 @@ export default function Page() {
Balance
- {getBalanceAmount(new BigNumber(accountData.balance), token?.decimals).toFormat()} {token?.symbol} + {formatBalanceAmount(new BigNumber(accountData.balance), token?.decimals)} {token?.symbol}
diff --git a/ui-react/src/pages/contract/[id].tsx b/ui-react/src/pages/contract/[id].tsx index 6655b7f..c2e43f6 100644 --- a/ui-react/src/pages/contract/[id].tsx +++ b/ui-react/src/pages/contract/[id].tsx @@ -1,7 +1,7 @@ import React from 'react' import { CardBody, Card, Tabs, Tab, Divider } from '@heroui/react' import { useRouter } from 'next/router' -import { getBalanceAmount, getThemeColor } from '@/utils/text' +import { formatBalanceAmount, getThemeColor } from '@/utils/text' import { unwrap, usePVMAccounts, usePVMContract } from '@/utils/api' import { useData } from '@/context' import { TxTable } from '@/components/tx' @@ -69,7 +69,7 @@ export default function Page() {
Balance
- {getBalanceAmount(new BigNumber(accountData?.balance || 0), token?.decimals).toFormat()} {token?.symbol} + {formatBalanceAmount(new BigNumber(accountData?.balance || 0), token?.decimals)} {token?.symbol}
diff --git a/ui-react/src/pages/sub/account/[id].tsx b/ui-react/src/pages/sub/account/[id].tsx index 17bb35d..5b7743c 100644 --- a/ui-react/src/pages/sub/account/[id].tsx +++ b/ui-react/src/pages/sub/account/[id].tsx @@ -1,7 +1,7 @@ import React, { useMemo } from 'react' import { CardBody, Card, Divider, Tabs, Tab } from '@heroui/react' import { useRouter } from 'next/router' -import { getBalanceAmount, getThemeColor } from '@/utils/text' +import { formatBalanceAmount, getThemeColor } from '@/utils/text' import { unwrap, useAccount } from '@/utils/api' import { useData } from '@/context' import BigNumber from 'bignumber.js' @@ -22,6 +22,7 @@ export default function Page() { }) const accountData = unwrap(data) + const tokenSymbol = token?.symbol ? ` ${token.symbol}` : '' const transferable = useMemo(() => { if (accountData) { @@ -56,27 +57,42 @@ export default function Page() {
Total Balance
-
{getBalanceAmount(new BigNumber(accountData.balance), token?.decimals).toFormat()}
+
+ {formatBalanceAmount(new BigNumber(accountData.balance), token?.decimals)} + {tokenSymbol} +
Transferrable
-
{getBalanceAmount(transferable, token?.decimals).toFormat()}
+
+ {formatBalanceAmount(transferable, token?.decimals)} + {tokenSymbol} +
Locked
-
{getBalanceAmount(new BigNumber(accountData.locked), token?.decimals).toFormat()}
+
+ {formatBalanceAmount(new BigNumber(accountData.locked), token?.decimals)} + {tokenSymbol} +
Vested
-
{getBalanceAmount(new BigNumber(accountData.vested || 0), token?.decimals).toFormat()}
+
+ {formatBalanceAmount(new BigNumber(accountData.vested || 0), token?.decimals)} + {tokenSymbol} +
Reserved
-
{getBalanceAmount(new BigNumber(accountData.reserved), token?.decimals).toFormat()}
+
+ {formatBalanceAmount(new BigNumber(accountData.reserved), token?.decimals)} + {tokenSymbol} +
diff --git a/ui-react/src/pages/token/[id].tsx b/ui-react/src/pages/token/[id].tsx index 1934560..2c6ef27 100644 --- a/ui-react/src/pages/token/[id].tsx +++ b/ui-react/src/pages/token/[id].tsx @@ -1,7 +1,7 @@ import React from 'react' import { CardBody, Card, Tabs, Tab, Divider } from '@heroui/react' import { useRouter } from 'next/router' -import { getBalanceAmount, getThemeColor } from '@/utils/text' +import { formatBalanceAmount, getThemeColor } from '@/utils/text' import { unwrap, usePVMTokens } from '@/utils/api' import { useData } from '@/context' import BigNumber from 'bignumber.js' @@ -53,7 +53,7 @@ export default function Page() {
Token Supply
-
{getBalanceAmount(new BigNumber(tokenData.totalSupply), tokenData.decimals).toFormat()}
+
{formatBalanceAmount(new BigNumber(tokenData.totalSupply), tokenData.decimals)}
diff --git a/ui-react/src/pages/tx/[id].tsx b/ui-react/src/pages/tx/[id].tsx index b203189..365c788 100644 --- a/ui-react/src/pages/tx/[id].tsx +++ b/ui-react/src/pages/tx/[id].tsx @@ -1,7 +1,7 @@ import React, { useMemo } from 'react' import { CardBody, Card, Divider } from '@heroui/react' import { useRouter } from 'next/router' -import { getBalanceAmount, getUTCTime, timeAgo } from '@/utils/text' +import { formatBalanceAmount, getUTCTime, timeAgo } from '@/utils/text' import { unwrap, usePVMTx } from '@/utils/api' import { Container, PageContent } from '@/ui' import { PVM_DECIMAL } from '@/utils/const' @@ -87,7 +87,7 @@ export default function Page() {
Value
-
{getBalanceAmount(new BigNumber(extrinsicData.value), PVM_DECIMAL).toFormat()}
+
{formatBalanceAmount(new BigNumber(extrinsicData.value), PVM_DECIMAL)}
@@ -110,12 +110,12 @@ export default function Page() {
Txn Fee
- {getBalanceAmount( + {formatBalanceAmount( new BigNumber(extrinsicData.gas_used).times( extrinsicData.txn_type === 2 ? extrinsicData.effective_gas_price : extrinsicData.gas_price ), PVM_DECIMAL - ).toFormat()} + )}
diff --git a/ui-react/src/utils/text.ts b/ui-react/src/utils/text.ts index 1fd1ac5..13f4674 100644 --- a/ui-react/src/utils/text.ts +++ b/ui-react/src/utils/text.ts @@ -47,6 +47,36 @@ export function getBalanceAmount(amount: BigNumber, decimals?: number): BigNumbe return new BigNumber(amount).dividedBy(BIG_TEN.pow(decimals || 0)) } +const COMPACT_BALANCE_UNITS = [ + { value: new BigNumber('1000000000000'), label: 'Trillion' }, + { value: new BigNumber('1000000000'), label: 'Billion' }, + { value: new BigNumber('1000000'), label: 'Million' }, +] as const + +export function formatBalanceAmount(amount: BigNumber, decimals?: number): string { + const balance = getBalanceAmount(amount, decimals) + const absBalance = balance.abs() + const compactUnit = COMPACT_BALANCE_UNITS.find((unit) => absBalance.isGreaterThanOrEqualTo(unit.value)) + + if (compactUnit) { + return `${balance.dividedBy(compactUnit.value).decimalPlaces(4, BigNumber.ROUND_HALF_UP).toFormat(4)} ${compactUnit.label}` + } + + if (absBalance.isZero()) { + return '0' + } + + if (absBalance.isLessThan(1)) { + if (absBalance.isLessThan('0.00000001')) { + return balance.isNegative() ? '> -0.00000001' : '< 0.00000001' + } + + return balance.decimalPlaces(8, BigNumber.ROUND_HALF_UP).toFormat() + } + + return balance.decimalPlaces(6, BigNumber.ROUND_HALF_UP).toFormat() +} + export function timeAgo(time: number | string, now = Date.now()) { const second = +time * 1000 const d = new Date(second)