diff --git a/.changeset/violet-candles-tell.md b/.changeset/violet-candles-tell.md new file mode 100644 index 000000000..a9b9394dd --- /dev/null +++ b/.changeset/violet-candles-tell.md @@ -0,0 +1,6 @@ +--- +"@fuel-wallet/types": minor +"fuels-wallet": minor +--- + +Format tiny, large, and regular amounts, applying 6 decimal places of precision. diff --git a/packages/app/playwright/e2e/HomeWallet.test.ts b/packages/app/playwright/e2e/HomeWallet.test.ts index 5be7ac664..47b72d862 100644 --- a/packages/app/playwright/e2e/HomeWallet.test.ts +++ b/packages/app/playwright/e2e/HomeWallet.test.ts @@ -52,14 +52,14 @@ test.describe('HomeWallet', () => { test('should not show user balance when user sets it to hidden', async () => { await visit(page, '/wallet'); - await hasText(page, /ETH.+0\.0/i); + await hasText(page, /ETH.+0/i); await getByAriaLabel(page, 'Hide balance').click(); // click on the hide balance await hasText(page, /ETH.+•••••/i); // should hide balance await reload(page); // reload the page await hasText(page, /ETH.+•••••/i); // should not show balance await getByAriaLabel(page, 'Show balance').click(); - await hasText(page, /ETH.+0\.0/i); + await hasText(page, /ETH.+0/i); await reload(page); // reload the page - await hasText(page, /ETH.+0\.0/i); + await hasText(page, /ETH.+0/i); }); }); diff --git a/packages/app/src/systems/Account/components/BalanceAssets/BalanceAssets.tsx b/packages/app/src/systems/Account/components/BalanceAssets/BalanceAssets.tsx index e04c9be28..09254b34c 100644 --- a/packages/app/src/systems/Account/components/BalanceAssets/BalanceAssets.tsx +++ b/packages/app/src/systems/Account/components/BalanceAssets/BalanceAssets.tsx @@ -45,7 +45,6 @@ export const BalanceAssets = ({ amount={balance.amount} onRemove={onRemove} onEdit={onEdit} - showActions /> ))} {!!(!isLoading && unknownLength) && ( diff --git a/packages/app/src/systems/Account/components/BalanceWidget/BalanceWidget.stories.tsx b/packages/app/src/systems/Account/components/BalanceWidget/BalanceWidget.stories.tsx index 292d86a66..fecedaa83 100644 --- a/packages/app/src/systems/Account/components/BalanceWidget/BalanceWidget.stories.tsx +++ b/packages/app/src/systems/Account/components/BalanceWidget/BalanceWidget.stories.tsx @@ -3,6 +3,7 @@ import { bn } from 'fuels'; import { MOCK_ACCOUNTS } from '../../__mocks__'; +import type { AccountWithBalance } from '@fuel-wallet/types'; import type { BalanceWidgetProps } from './BalanceWidget'; import { BalanceWidget } from './BalanceWidget'; @@ -11,10 +12,11 @@ export default { title: 'Account/Components/BalanceWidget', }; -const ACCOUNT = { +const ACCOUNT: AccountWithBalance = { ...MOCK_ACCOUNTS[0], balance: bn(12008943834), balanceSymbol: '$', + balances: [], }; export const Usage = (args: BalanceWidgetProps) => ( diff --git a/packages/app/src/systems/Account/components/BalanceWidget/BalanceWidget.test.tsx b/packages/app/src/systems/Account/components/BalanceWidget/BalanceWidget.test.tsx index bec63a94b..a0291586d 100644 --- a/packages/app/src/systems/Account/components/BalanceWidget/BalanceWidget.test.tsx +++ b/packages/app/src/systems/Account/components/BalanceWidget/BalanceWidget.test.tsx @@ -6,14 +6,16 @@ import { renderWithProvider } from '~/systems/Core/__tests__/utils'; import { MOCK_ACCOUNTS } from '../../__mocks__'; -import { Address } from 'fuels'; +import type { AccountWithBalance } from '@fuel-wallet/types'; +import { Address, bn } from 'fuels'; import { act } from 'react'; import { BalanceWidget } from './BalanceWidget'; -const ACCOUNT = { +const ACCOUNT: AccountWithBalance = { ...MOCK_ACCOUNTS[0], - balance: '4999989994', + balance: bn(4999989994), balanceSymbol: 'ETH', + balances: [], }; describe('BalanceWidget', () => { diff --git a/packages/app/src/systems/Account/components/BalanceWidget/BalanceWidget.tsx b/packages/app/src/systems/Account/components/BalanceWidget/BalanceWidget.tsx index 5331a9abb..feef45b90 100644 --- a/packages/app/src/systems/Account/components/BalanceWidget/BalanceWidget.tsx +++ b/packages/app/src/systems/Account/components/BalanceWidget/BalanceWidget.tsx @@ -1,10 +1,21 @@ import { cssObj } from '@fuel-ui/css'; -import { Avatar, Box, Button, Heading, Icon, Text } from '@fuel-ui/react'; -import type { Account } from '@fuel-wallet/types'; +import { + Avatar, + Box, + Button, + Heading, + Icon, + Text, + Tooltip, +} from '@fuel-ui/react'; +import type { AccountWithBalance } from '@fuel-wallet/types'; import { type ReactNode, useMemo } from 'react'; import { FuelAddress } from '~/systems/Account'; -import type { Maybe } from '~/systems/Core'; -import { AmountVisibility, VisibilityButton } from '~/systems/Core'; +import { + AmountVisibility, + VisibilityButton, + formatBalance, +} from '~/systems/Core'; import { useAccounts } from '../../hooks'; @@ -29,13 +40,14 @@ export function BalanceWidgetWrapper({ } export type BalanceWidgetProps = { - account?: Maybe; + account?: AccountWithBalance; isLoading?: boolean; visibility?: boolean; onPressAccounts?: () => void; onChangeVisibility?: (visibility: boolean) => void; }; +const decimals = DECIMAL_FUEL; export function BalanceWidget({ account, isLoading, @@ -43,6 +55,11 @@ export function BalanceWidget({ onChangeVisibility, }: BalanceWidgetProps) { const { handlers } = useAccounts(); + + const { original, tooltip } = useMemo(() => { + return formatBalance(account?.balance, decimals); + }, [account]); + if (isLoading || !account) return ; return ( @@ -84,14 +101,20 @@ export function BalanceWidget({ <> Balance - - {account.balanceSymbol || '$'}  - - + + + {account.balanceSymbol || '$'}  + + + ( - - - - - - - + + + + + + } bottom={ - <> - - - - - + + + + } /> ); diff --git a/packages/app/src/systems/Account/hooks/useAccounts.tsx b/packages/app/src/systems/Account/hooks/useAccounts.tsx index 5a39e1492..031c75aaf 100644 --- a/packages/app/src/systems/Account/hooks/useAccounts.tsx +++ b/packages/app/src/systems/Account/hooks/useAccounts.tsx @@ -1,8 +1,6 @@ -import type { AssetData } from '@fuel-wallet/types'; import { bn } from 'fuels'; import { useEffect, useRef } from 'react'; import { Services, store } from '~/store'; -import { useAssets } from '~/systems/Asset'; import { useOverlay } from '~/systems/Overlay'; import type { AccountsMachineState } from '../machines'; diff --git a/packages/app/src/systems/Account/machines/accountsMachine.tsx b/packages/app/src/systems/Account/machines/accountsMachine.tsx index c71a0fd6f..eed930198 100644 --- a/packages/app/src/systems/Account/machines/accountsMachine.tsx +++ b/packages/app/src/systems/Account/machines/accountsMachine.tsx @@ -1,9 +1,8 @@ -import type { Account } from '@fuel-wallet/types'; +import type { Account, AccountWithBalance } from '@fuel-wallet/types'; import type { InterpreterFrom, StateFrom } from 'xstate'; import { assign, createMachine } from 'xstate'; import { IS_LOGGED_KEY } from '~/config'; import { store } from '~/store'; -import type { Maybe } from '~/systems/Core'; import { CoreService, FetchMachine, Storage } from '~/systems/Core'; import { NetworkService } from '~/systems/Network'; @@ -12,7 +11,7 @@ import type { AccountInputs } from '../services/account'; type MachineContext = { accounts?: Account[]; - account?: Maybe; + account?: AccountWithBalance; error?: unknown; }; @@ -21,7 +20,7 @@ type MachineServices = { data: Account[]; }; fetchAccount: { - data: Account; + data: AccountWithBalance; }; setCurrentAccount: { data: Account; @@ -229,7 +228,7 @@ export const accountsMachine = createMachine( return AccountService.getAccounts(); }, }), - fetchAccount: FetchMachine.create({ + fetchAccount: FetchMachine.create({ showError: true, maxAttempts: 1, async fetch() { @@ -245,6 +244,7 @@ export const accountsMachine = createMachine( account: accountToFetch, providerUrl, }); + return accountWithBalance; }, }), diff --git a/packages/app/src/systems/Account/services/account.ts b/packages/app/src/systems/Account/services/account.ts index 6ffd79f5b..fcc0c6ae5 100644 --- a/packages/app/src/systems/Account/services/account.ts +++ b/packages/app/src/systems/Account/services/account.ts @@ -1,5 +1,10 @@ import { createProvider } from '@fuel-wallet/connections'; -import type { Account, CoinAsset } from '@fuel-wallet/types'; +import type { + Account, + AccountBalance, + AccountWithBalance, + CoinAsset, +} from '@fuel-wallet/types'; import { Address, type Provider, bn } from 'fuels'; import { AssetService } from '~/systems/Asset/services'; import { getFuelAssetByAssetId } from '~/systems/Asset/utils'; @@ -24,9 +29,6 @@ export type AccountInputs = { fetchAccount: { address: string; }; - setBalance: { - data: Pick; - }; setCurrentAccount: { address: string; }; @@ -85,7 +87,9 @@ export class AccountService { return account; } - static async fetchBalance(input: AccountInputs['fetchBalance']) { + static async fetchBalance( + input: AccountInputs['fetchBalance'] + ): Promise { if (!input.account) { throw new Error('Account not defined'); } @@ -111,7 +115,7 @@ export class AccountService { ...prev, { ...balance, - amount: balance.amount.toString(), + amount: balance.amount, asset, }, ]; @@ -137,38 +141,31 @@ export class AccountService { (balance) => balance.assetId === baseAssetId.toString() ); const ethBalance = ethAsset?.amount; - const nextAccountWithAssets = { - address: account.address || '', - balance: bn(ethBalance || 0).toString(), + const accountAssets: AccountBalance = { + balance: ethBalance ?? bn(0), balanceSymbol: 'ETH', balances: nextBalancesWithAssets, }; - const nextAccount = await AccountService.setBalance({ - data: nextAccountWithAssets, - }); + const result: AccountWithBalance = { + ...account, + ...accountAssets, + }; - return nextAccount ?? account; + return result; } catch (_error) { - const nextAccount = await AccountService.setBalance({ - data: { - address: account.address || '', - balance: bn(0).toString(), - balanceSymbol: 'ETH', - balances: [], - }, - }); - return nextAccount ?? account; - } - } + const accountAssets: AccountBalance = { + balance: bn(0), + balanceSymbol: 'ETH', + balances: [], + }; + const result: AccountWithBalance = { + ...account, + ...accountAssets, + }; - static async setBalance(input: AccountInputs['setBalance']) { - if (!db.isOpen()) return; - return db.transaction('rw!', db.accounts, async () => { - const { address, ...updateData } = input.data; - await db.accounts.update(address, updateData); - return db.accounts.get({ address: input.data.address }); - }); + return result; + } } static toMap(accounts: Account[]) { diff --git a/packages/app/src/systems/Asset/components/AssetItem/AssetItem.tsx b/packages/app/src/systems/Asset/components/AssetItem/AssetItem.tsx index 85e20d1f7..d710049d4 100644 --- a/packages/app/src/systems/Asset/components/AssetItem/AssetItem.tsx +++ b/packages/app/src/systems/Asset/components/AssetItem/AssetItem.tsx @@ -13,13 +13,18 @@ import { } from '@fuel-ui/react'; import { type FC, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; -import { AmountVisibility, Pages, shortAddress } from '~/systems/Core'; +import { + AmountVisibility, + Pages, + formatBalance, + shortAddress, +} from '~/systems/Core'; import { useBalanceVisibility } from '~/systems/Core/hooks/useVisibility'; import { AssetRemoveDialog } from '../AssetRemoveDialog'; import type { AssetData, AssetFuelData } from '@fuel-wallet/types'; -import { type BNInput, bn } from 'fuels'; +import type { BNInput } from 'fuels'; import useFuelAsset from '../../hooks/useFuelAsset'; import { AssetItemLoader } from './AssetItemLoader'; @@ -113,11 +118,13 @@ export const AssetItem: AssetItemComponent = ({ } if (amount) { + const { original, tooltip } = formatBalance(amount, decimals); + return ( { decimals, amount, } = assetAmount || {}; - return ( diff --git a/packages/app/src/systems/Core/components/AmountVisibility/AmountVisibility.tsx b/packages/app/src/systems/Core/components/AmountVisibility/AmountVisibility.tsx index 6fda4cdff..26d05a205 100644 --- a/packages/app/src/systems/Core/components/AmountVisibility/AmountVisibility.tsx +++ b/packages/app/src/systems/Core/components/AmountVisibility/AmountVisibility.tsx @@ -1,6 +1,6 @@ import type { BNInput, BigNumberish } from 'fuels'; -import { formatAmount } from '../../utils'; +import { formatBalance } from '../../utils'; export type AmountVisibilityProps = { value?: BigNumberish | BNInput; @@ -13,11 +13,6 @@ export function AmountVisibility({ value, units, }: AmountVisibilityProps) { - return ( - <> - {visibility - ? formatAmount({ amount: value, options: { units } }) - : '•••••'} - - ); + const { formatted } = formatBalance(value, units); + return <>{visibility ? formatted.display : '•••••'}; } diff --git a/packages/app/src/systems/Core/utils/math.ts b/packages/app/src/systems/Core/utils/math.ts index 2b4aa1552..77ece39e5 100644 --- a/packages/app/src/systems/Core/utils/math.ts +++ b/packages/app/src/systems/Core/utils/math.ts @@ -1,7 +1,23 @@ -import type { BNInput, FormatConfig } from 'fuels'; -import { bn } from 'fuels'; +import type { BN, BNInput, FormatConfig } from 'fuels'; +import { DECIMAL_FUEL, bn } from 'fuels'; import { MAX_FRACTION_DIGITS } from '~/config'; +const MINIMUM_ZEROS_TO_DISPLAY = 5; // it means 0.000001 (at least 5 zeros in decimals) +const PRECISION = 6; + +export type FormatBalanceResult = { + amount: BN; + tooltip: boolean; + formatted: { + display: string; + fractionDigits: number; + }; + original: { + display: string; + fractionDigits: number; + }; +}; + // this function replaces native bn.format because it fails when it's 0 units // put link to ts-sdk issue here export function formatAmount({ @@ -25,3 +41,87 @@ export function formatAmount({ return bn(amount).format(formatParams); } + +export const formatBalance = ( + input: BNInput | null | undefined = '0', + units: number | undefined = DECIMAL_FUEL +): FormatBalanceResult => { + const amount = bn(input); + const minimum = bn('1'.padEnd(units - MINIMUM_ZEROS_TO_DISPLAY, '0')); + + // covers a bug in the sdk that format don't work when unit is zero. we'll use 1 instead, then multiply by 10 later + if (!units) { + const display = formatAmount({ amount, options: { units: 0 } }); + + return { + amount, + tooltip: false, + formatted: { + display, + fractionDigits: 0, + }, + original: { + display, + fractionDigits: 0, + }, + }; + } + + if (amount.isZero()) { + return { + amount, + tooltip: false, + formatted: { + display: '0', + fractionDigits: 0, + }, + original: { + display: '0', + fractionDigits: 0, + }, + }; + } + + // Format the original amount, example "0.000000000002409883". Good to use in tooltips. + // But in UIs, it may break the layout, so we need to display only the "formatted" there. + const originalDisplay = amount.format({ + units: units, + precision: units, + }); + + if (minimum.gt(amount)) { + return { + amount, + tooltip: true, + formatted: { + display: `<${minimum.format({ + units: units, + precision: PRECISION, + })}`, + fractionDigits: PRECISION, + }, + original: { + display: originalDisplay, + fractionDigits: units, + }, + }; + } + + const formattedDisplay = amount.format({ + units: units, + precision: PRECISION, + }); + + return { + amount, + tooltip: formattedDisplay !== originalDisplay, + formatted: { + display: formattedDisplay, + fractionDigits: PRECISION, + }, + original: { + display: originalDisplay, + fractionDigits: units, + }, + }; +}; diff --git a/packages/app/src/systems/Transaction/services/transaction.tsx b/packages/app/src/systems/Transaction/services/transaction.tsx index 3f0937c83..18ca6b967 100644 --- a/packages/app/src/systems/Transaction/services/transaction.tsx +++ b/packages/app/src/systems/Transaction/services/transaction.tsx @@ -465,9 +465,3 @@ export class TxService { }; } } - -export function getAssetAccountBalance(account: Account, assetId: string) { - const balances = account.balances || []; - const asset = balances.find((balance) => balance.assetId === assetId); - return bn(asset?.amount); -} diff --git a/packages/types/src/accounts.ts b/packages/types/src/accounts.ts index d4bc05986..4b52ce3da 100644 --- a/packages/types/src/accounts.ts +++ b/packages/types/src/accounts.ts @@ -1,4 +1,4 @@ -import type { AssetFuel, BN, BigNumberish } from 'fuels'; +import type { AssetFuel, BN } from 'fuels'; import type { Coin } from './coin'; @@ -7,7 +7,9 @@ export type Vault = { data: string; }; -export type CoinAsset = Coin & { asset?: AssetFuel }; +export interface CoinAsset extends Coin { + asset?: AssetFuel; +} export type Account = { name: string; @@ -15,8 +17,13 @@ export type Account = { vaultId?: number; publicKey: string; isHidden?: boolean; - balance?: BigNumberish | BN; - balanceSymbol?: string; - balances?: CoinAsset[]; isCurrent?: boolean; }; + +export type AccountBalance = { + balance: BN; + balanceSymbol: string; + balances: CoinAsset[]; +}; + +export type AccountWithBalance = Account & AccountBalance;