From fd1e342a81041aa6fed925df9c45449cb3946e00 Mon Sep 17 00:00:00 2001 From: Arthur Geron <3487334+arthurgeron@users.noreply.github.com> Date: Mon, 3 Feb 2025 21:26:17 -0300 Subject: [PATCH] feat: show token value converted to USD (#1788) - Closes #1623 - Closes #1792 - Closes FE-1380 - Closes FE-810 - Created the convertAsset utility, which returns the token amount converted to USD. - Token now also display their values in USD throughout Fuel Wallet. - Moved asset endpoint data to global constants. | ![Home](https://github.com/user-attachments/assets/4a4baf00-b547-4ad2-ac3e-c1252e577f84) | ![](https://github.com/user-attachments/assets/6e36f2ad-60b2-46a3-a41c-1cc1c6014dfe) | ![](https://github.com/user-attachments/assets/cd27b7ce-e799-40c8-b96e-12a8b71e2d49) | |----------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------| ![](https://github.com/user-attachments/assets/fb477ce0-4df8-4ffa-80a9-123670514407) | ![]() | | --- .changeset/brown-wasps-swim.md | 6 + .changeset/tall-suns-carry.md | 2 - packages/app/jest.setup.ts | 141 ++++++++++++++---- packages/app/playwright/crx/assets.test.ts | 117 ++++++++++++--- .../app/playwright/e2e/HomeWallet.test.ts | 12 +- .../BalanceAssets/BalanceAssets.tsx | 12 +- .../BalanceWidget/BalanceWidget.stories.tsx | 2 + .../BalanceWidget/BalanceWidget.test.tsx | 4 +- .../BalanceWidget/BalanceWidget.tsx | 38 +++-- .../src/systems/Account/services/account.ts | 31 +++- .../src/systems/Asset/cache/AssetsCache.ts | 24 +-- .../Asset/components/AssetItem/AssetItem.tsx | 14 +- .../components/AssetItem/AssetItemAmount.tsx | 59 ++++++-- .../Asset/components/AssetList/AssetList.tsx | 12 +- .../components/AssetsAmount/AssetsAmount.tsx | 16 +- .../Asset/components/AssetsAmount/styles.ts | 21 ++- .../UnknownAssetsButton.tsx | 31 ++++ .../app/src/systems/Asset/constants/index.ts | 20 +++ packages/app/src/systems/Asset/types.ts | 4 + .../AmountVisibility/AmountVisibility.tsx | 2 +- .../components/InputAmount/InputAmount.tsx | 65 ++++++-- .../systems/Core/utils/convertToUsd.test.ts | 53 +++++++ .../src/systems/Core/utils/convertToUsd.ts | 39 +++++ .../components/HomeActions/HomeActions.tsx | 4 +- .../app/src/systems/Home/pages/Home/Home.tsx | 2 +- .../Send/components/SendSelect/SendSelect.tsx | 19 ++- .../components/TxFee/TxFee.test.tsx | 19 --- .../Transaction/components/TxFee/TxFee.tsx | 92 ++++++++++-- .../Transaction/components/TxFee/styles.tsx | 28 +++- .../components/TxFeeOptions/TxFeeOptions.tsx | 33 +++- .../Transaction/services/transaction.tsx | 23 ++- packages/types/src/accounts.ts | 2 + packages/types/src/asset.ts | 1 + packages/types/src/coin.ts | 1 + 34 files changed, 749 insertions(+), 200 deletions(-) create mode 100644 .changeset/brown-wasps-swim.md delete mode 100644 .changeset/tall-suns-carry.md create mode 100644 packages/app/src/systems/Asset/components/UnknownAssetsButton/UnknownAssetsButton.tsx create mode 100644 packages/app/src/systems/Asset/constants/index.ts create mode 100644 packages/app/src/systems/Asset/types.ts create mode 100644 packages/app/src/systems/Core/utils/convertToUsd.test.ts create mode 100644 packages/app/src/systems/Core/utils/convertToUsd.ts delete mode 100644 packages/app/src/systems/Transaction/components/TxFee/TxFee.test.tsx diff --git a/.changeset/brown-wasps-swim.md b/.changeset/brown-wasps-swim.md new file mode 100644 index 0000000000..2e8acae0a8 --- /dev/null +++ b/.changeset/brown-wasps-swim.md @@ -0,0 +1,6 @@ +--- +"@fuel-wallet/types": patch +"fuels-wallet": patch +--- + +feat: show token value converted to USD diff --git a/.changeset/tall-suns-carry.md b/.changeset/tall-suns-carry.md deleted file mode 100644 index a845151cc8..0000000000 --- a/.changeset/tall-suns-carry.md +++ /dev/null @@ -1,2 +0,0 @@ ---- ---- diff --git a/packages/app/jest.setup.ts b/packages/app/jest.setup.ts index 947361ed3f..dfdce5ae8d 100644 --- a/packages/app/jest.setup.ts +++ b/packages/app/jest.setup.ts @@ -2,7 +2,8 @@ import { webcrypto } from 'crypto'; // biome-ignore lint/style/useNodejsImportProtocol: import { TextDecoder, TextEncoder } from 'util'; - +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; import { localStorageMock } from './src/mocks/localStorage'; // biome-ignore lint/suspicious/noExplicitAny: @@ -21,6 +22,120 @@ import 'whatwg-fetch'; import { act } from 'react'; +// Initialize the MSW server with the necessary request handlers +const server = setupServer( + rest.get('/assets.json', (_req, res, ctx) => { + return res( + ctx.status(200), + ctx.json([ + { + name: 'Ethereum', + symbol: 'ETH', + icon: 'https://verified-assets.fuel.network/images/eth.svg', + networks: [ + { + type: 'ethereum', + chain: 'sepolia', + decimals: 18, + chainId: 11155111, + }, + { + type: 'ethereum', + chain: 'foundry', + decimals: 18, + chainId: 31337, + }, + { + type: 'ethereum', + chain: 'mainnet', + decimals: 18, + chainId: 1, + }, + { + type: 'fuel', + chain: 'devnet', + decimals: 9, + assetId: + '0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07', + chainId: 0, + }, + { + type: 'fuel', + chain: 'testnet', + decimals: 9, + assetId: + '0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07', + chainId: 0, + }, + { + type: 'fuel', + chain: 'mainnet', + decimals: 9, + assetId: + '0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07', + chainId: 9889, + }, + ], + }, + { + name: 'Fuel', + symbol: 'FUEL', + icon: 'https://verified-assets.fuel.network/images/fuel.svg', + networks: [ + { + type: 'ethereum', + chain: 'sepolia', + address: '0xd7fc4e8fb2c05567c313f4c9b9e07641a361a550', + decimals: 9, + chainId: 11155111, + }, + { + type: 'ethereum', + chain: 'mainnet', + address: '0x675b68aa4d9c2d3bb3f0397048e62e6b7192079c', + decimals: 9, + chainId: 1, + }, + { + type: 'fuel', + chain: 'testnet', + decimals: 9, + chainId: 0, + contractId: + '0xd02112ef9c39f1cea7c8527c26242ca1f5d26bcfe8d1564bee054d3b04175471', + subId: + '0xede43647e2aad1c0f1696201d6ba913aa67c917c3ac9a4a7d95662962ab25c5b', + assetId: + '0x324d0c35a4299ef88138a656d5272c5a3a9ccde2630ae055dacaf9d13443d53b', + }, + { + type: 'fuel', + chain: 'mainnet', + decimals: 9, + chainId: 9889, + contractId: + '0x4ea6ccef1215d9479f1024dff70fc055ca538215d2c8c348beddffd54583d0e8', + subId: + '0xe81c89b8cf795c7c25e79f6c4f2f1cd233290b58e217ed4e9b6b18538badddaf', + assetId: + '0x1d5d97005e41cae2187a895fd8eab0506111e0e2f3331cd3912c15c24e3c1d82', + }, + ], + }, + ]) + ); + }) +); + +// Establish API mocking before all tests +beforeAll(() => server.listen()); + +// Reset any request handlers that are declared as a part of our tests (i.e., for testing one-time error scenarios) +afterEach(() => server.resetHandlers()); + +// Clean up after the tests are finished +afterAll(() => server.close()); + // Replace ReactDOMTestUtils.act with React.act jest.mock('react-dom/test-utils', () => { const originalModule = jest.requireActual('react-dom/test-utils'); @@ -81,27 +196,3 @@ if (process.env.CI) { logErrorsBeforeRetry: true, }); } - -const _mockNetworks = [ - { - asset_id: 'TKN', - name: 'Token', - type: 'token', - symbol: 'TKN', - decimals: 9, - }, - { - asset_id: 'ETH', - name: 'Ethereum', - type: 'token', - symbol: 'ETH', - decimals: 18, - }, - { - asset_id: 'Fuel', - name: 'Fuel', - type: 'token', - symbol: 'Fuel', - decimals: 9, - }, -]; diff --git a/packages/app/playwright/crx/assets.test.ts b/packages/app/playwright/crx/assets.test.ts index 813bd8d9f3..92fe62438c 100644 --- a/packages/app/playwright/crx/assets.test.ts +++ b/packages/app/playwright/crx/assets.test.ts @@ -41,40 +41,79 @@ test.describe('Check assets', () => { }); test('should show valid asset value 0.002000', async () => { + await expect + .poll( + async () => + await page + .getByLabel('ETH token balance', { exact: true }) + .isVisible(), + { timeout: 10000 } + ) + .toBeTruthy(); expect( - await page.getByText('0.002000', { exact: true }).isVisible() - ).toBeTruthy(); + await page.getByLabel('ETH token balance', { exact: true }).textContent() + ).toContain('0.002000 ETH'); }); test('should show USDCIcon AlertTriangle', async () => { - expect( - await page.getByText('USDCIcon AlertTriangle').isVisible() - ).toBeTruthy(); + await expect + .poll( + async () => await page.getByText('USDCIcon AlertTriangle').isVisible(), + { timeout: 10000 } + ) + .toBeTruthy(); }); test('should show 1 SCAM NFT', async () => { - expect(await page.getByText('1 SCAM').isVisible()).toBeTruthy(); + await expect + .poll(async () => await page.getByText('1 SCAM').isVisible(), { + timeout: 10000, + }) + .toBeTruthy(); }); // Verified assets should never show the (Add) button test('should not show (Add) button for verified assets', async () => { - expect( - await page.getByRole('button', { name: '(Add)' }).isVisible() - ).toBeFalsy(); + await expect + .poll( + async () => + await page.getByRole('button', { name: '(Add)' }).isVisible(), + { timeout: 10000 } + ) + .toBeFalsy(); }); // Verified assets will never be inside of "Hidden assets" part test('should not show verified assets in hidden assets', async () => { // get all h6 text from div.fuel_CardList as an array, and then click Show unknown assets button, and then check if the array added a new element with a name other than Unknown - const h6Texts = await page.$$eval('div.fuel_CardList h6', (els) => - els.map((el) => el.textContent) - ); - await page.getByRole('button', { name: 'Show unknown assets' }).click(); - await page.waitForTimeout(1000); - const h6TextsAfter = await page.$$eval('div.fuel_CardList h6', (els) => - els.map((el) => el.textContent) - ); - expect(h6TextsAfter.length).toBeGreaterThan(h6Texts.length); + let h6Texts: string[] = []; + await expect + .poll( + async () => { + h6Texts = await page.$$eval('div.fuel_CardList h6', (els) => + els.map((el) => el.textContent) + ); + return h6Texts; + }, + { timeout: 10000 } + ) + .toBeTruthy(); + + await page.getByLabel('Show unknown assets').click(); + + let h6TextsAfter: string[] = []; + + await expect + .poll( + async () => { + h6TextsAfter = await page.$$eval('div.fuel_CardList h6', (els) => + els.map((el) => el.textContent) + ); + return h6TextsAfter.length; + }, + { timeout: 10000 } + ) + .toBeGreaterThan(h6Texts.length); for (const el of h6Texts) { expect(el.includes('(Add)')).toBeFalsy(); @@ -96,17 +135,49 @@ test.describe('Check assets', () => { }); test('Should add unknown asset', async () => { - await page.getByRole('button', { name: 'Show unknown assets' }).click(); + await expect + .poll(async () => page.getByLabel('Show unknown assets'), { + timeout: 10000, + }) + .toBeDefined(); + await page.getByLabel('Show unknown assets').click(); + await expect + .poll(async () => page.getByRole('button', { name: '(Add)' }).nth(1), { + timeout: 10000, + }) + .toBeDefined(); await page.getByRole('button', { name: '(Add)' }).nth(1).click(); + + await expect + .poll(async () => await page.getByText('Token 2'), { + timeout: 10000, + }) + .toBeDefined(); await page.getByPlaceholder('Asset name').fill('Token 2'); await page.getByPlaceholder('Asset symbol').fill('TKN2'); await page.getByLabel('Save Asset').click(); await page.waitForTimeout(1000); + await page.reload({ waitUntil: 'domcontentloaded' }); - await page.waitForTimeout(1000); - await waitAriaLabel(page, 'Account 1 selected'); - await page.waitForTimeout(1000); - expect(await page.getByText('1 TKN2').isVisible()).toBeTruthy(); + + await expect + .poll( + async () => { + return page.getByLabel('Account 1 selected').isVisible(); + }, + { timeout: 10000 } + ) + .toBeTruthy(); + + await expect + .poll( + async () => { + const text = await page.getByText('1 TKN2'); + return text.isVisible(); + }, + { timeout: 10000 } + ) + .toBeTruthy(); // The following tests are disabled because the added tokens need a refresh to show up. Fix FE-1122 and enable these. diff --git a/packages/app/playwright/e2e/HomeWallet.test.ts b/packages/app/playwright/e2e/HomeWallet.test.ts index ca8e95d02f..c9fee0c673 100644 --- a/packages/app/playwright/e2e/HomeWallet.test.ts +++ b/packages/app/playwright/e2e/HomeWallet.test.ts @@ -39,7 +39,7 @@ test.describe('HomeWallet', () => { await page.waitForTimeout(2000); await page.reload(); await hasText(page, /Ethereum/i); - await hasText(page, /ETH.0\.002/i); + await hasText(page, /[$]\d{1,}\.\d{1,}/); await getByAriaLabel(page, 'Selected Network').click(); await getByAriaLabel(page, 'fuel_network-item-2').click(); await hasText(page, "You don't have any assets"); @@ -55,14 +55,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/i); + await hasText(page, /[$]\d{1,}\.\d{2,}/i); await getByAriaLabel(page, 'Hide balance').click(); // click on the hide balance - await hasText(page, /ETH.+•••••/i); // should hide balance + await hasText(page, /•••••/i); // should hide balance await reload(page); // reload the page - await hasText(page, /ETH.+•••••/i); // should not show balance + await hasText(page, /•••••/i); // should not show balance await getByAriaLabel(page, 'Show balance').click(); - await hasText(page, /ETH.+0/i); + await hasText(page, /[$]\d{1,}\.\d{2,}/); await reload(page); // reload the page - await hasText(page, /ETH.+0/i); + await hasText(page, /[$]\d{1,}\.\d{2,}/); }); }); diff --git a/packages/app/src/systems/Account/components/BalanceAssets/BalanceAssets.tsx b/packages/app/src/systems/Account/components/BalanceAssets/BalanceAssets.tsx index 00ba9c7a05..97366128a1 100644 --- a/packages/app/src/systems/Account/components/BalanceAssets/BalanceAssets.tsx +++ b/packages/app/src/systems/Account/components/BalanceAssets/BalanceAssets.tsx @@ -4,6 +4,7 @@ import { useMemo, useState } from 'react'; import { isUnknownAsset } from '~/systems/Asset'; import { AssetItem, AssetList } from '~/systems/Asset/components'; import type { AssetListEmptyProps } from '~/systems/Asset/components/AssetList/AssetListEmpty'; +import { UnknownAssetsButton } from '~/systems/Asset/components/UnknownAssetsButton/UnknownAssetsButton'; export type BalanceAssetListProp = { balances?: CoinAsset[]; @@ -64,11 +65,12 @@ export const BalanceAssets = ({ /> ); })} - {!!(!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 fecedaa83c..8d63ab990e 100644 --- a/packages/app/src/systems/Account/components/BalanceWidget/BalanceWidget.stories.tsx +++ b/packages/app/src/systems/Account/components/BalanceWidget/BalanceWidget.stories.tsx @@ -17,6 +17,8 @@ const ACCOUNT: AccountWithBalance = { balance: bn(12008943834), balanceSymbol: '$', balances: [], + amountInUsd: '$38,830.32', + totalBalanceInUsd: 38830.32, }; 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 6b443cd782..b6da25b8ad 100644 --- a/packages/app/src/systems/Account/components/BalanceWidget/BalanceWidget.test.tsx +++ b/packages/app/src/systems/Account/components/BalanceWidget/BalanceWidget.test.tsx @@ -16,6 +16,8 @@ const ACCOUNT: AccountWithBalance = { balance: bn(4999989994), balanceSymbol: 'ETH', balances: [], + amountInUsd: '$16,167.22', + totalBalanceInUsd: 16167.22, }; describe('BalanceWidget', () => { @@ -37,7 +39,7 @@ describe('BalanceWidget', () => { it('should show formatted balance', async () => { renderWithProvider(); - expect(screen.getByText(/4\.999/)).toBeInTheDocument(); + expect(screen.getByText(ACCOUNT.amountInUsd ?? '$0')).toBeInTheDocument(); }); it('should hide balance when user sets his balance to hide', async () => { diff --git a/packages/app/src/systems/Account/components/BalanceWidget/BalanceWidget.tsx b/packages/app/src/systems/Account/components/BalanceWidget/BalanceWidget.tsx index b95be39305..d6ba89567f 100644 --- a/packages/app/src/systems/Account/components/BalanceWidget/BalanceWidget.tsx +++ b/packages/app/src/systems/Account/components/BalanceWidget/BalanceWidget.tsx @@ -3,23 +3,22 @@ import { Avatar, Box, Button, + HStack, Heading, Icon, Text, Tooltip, + VStack, } from '@fuel-ui/react'; import type { AccountWithBalance } from '@fuel-wallet/types'; import { type ReactNode, useMemo } from 'react'; import { FuelAddress } from '~/systems/Account'; -import { - AmountVisibility, - VisibilityButton, - formatBalance, -} from '~/systems/Core'; +import { VisibilityButton, formatBalance } from '~/systems/Core'; import { useAccounts } from '../../hooks'; import { DEFAULT_DECIMAL_UNITS } from 'fuels'; +import { INTL_FORMATTER } from '~/systems/Asset/constants'; import { BalanceWidgetLoader } from './BalanceWidgetLoader'; type BalanceWidgetWrapperProps = { @@ -48,6 +47,7 @@ export type BalanceWidgetProps = { }; const decimals = DEFAULT_DECIMAL_UNITS; + export function BalanceWidget({ account, isLoading, @@ -55,13 +55,15 @@ export function BalanceWidget({ onChangeVisibility, }: BalanceWidgetProps) { const { handlers } = useAccounts(); - + const totalBalanceInUsd = account?.totalBalanceInUsd ?? 0; const { original, tooltip } = useMemo(() => { return formatBalance(account?.balance, decimals); }, [account]); if (isLoading || !account) return ; + const totalValue = INTL_FORMATTER.format(totalBalanceInUsd); + return ( - Balance + Total balance - - {account.balanceSymbol || '$'}  - + + {visibility ? totalValue : '$•••••'} = {}; + const chainId = provider.getChainId(); + const balanceAssets = await AssetsCache.fetchAllAssets( - provider.getChainId(), + chainId, balances.map((balance) => balance.assetId) ); + let totalBalanceInUsd = 0; + + balances.map((asset) => { + const assetBalance = balanceAssets.get(asset.assetId); + // biome-ignore lint/suspicious/noExplicitAny: type not yet updated in this @fuel-ts/account version + const rate = ((assetBalance as any).rate as number) ?? 0; + if (assetBalance?.decimals) { + assetsAmountsInUsd[asset.assetId] = + convertToUsd(asset.amount, assetBalance?.decimals, rate) ?? 0; + totalBalanceInUsd += assetsAmountsInUsd[asset.assetId]?.value ?? 0; + } + }); // includes "asset" prop in balance, centralizing the complexity here instead of in rest of UI const nextBalancesWithAssets = await balances.reduce( async (acc, balance) => { const prev = await acc; - const cachedAsset = balanceAssets.get(balance.assetId); + const cachedAsset = balanceAssets?.get(balance.assetId); + + const amountInUsd = assetsAmountsInUsd[balance.assetId]; return [ ...prev, @@ -123,6 +144,7 @@ export class AccountService { ...balance, amount: balance.amount, asset: cachedAsset, + amountInUsd: amountInUsd ? amountInUsd.formatted : '$0', }, ]; }, @@ -149,8 +171,11 @@ export class AccountService { const ethBalance = ethAsset?.amount; const accountAssets: AccountBalance = { balance: ethBalance ?? bn(0), + amountInUsd: + assetsAmountsInUsd[baseAssetId.toString()]?.formatted ?? '$0', balanceSymbol: 'ETH', balances: nextBalancesWithAssets, + totalBalanceInUsd, }; const result: AccountWithBalance = { @@ -164,6 +189,8 @@ export class AccountService { balance: bn(0), balanceSymbol: 'ETH', balances: [], + amountInUsd: '', + totalBalanceInUsd: 0, }; const result: AccountWithBalance = { ...account, diff --git a/packages/app/src/systems/Asset/cache/AssetsCache.ts b/packages/app/src/systems/Asset/cache/AssetsCache.ts index 2ca1660cb5..ea3eb1fbd3 100644 --- a/packages/app/src/systems/Asset/cache/AssetsCache.ts +++ b/packages/app/src/systems/Asset/cache/AssetsCache.ts @@ -1,14 +1,13 @@ import type { AssetData } from '@fuel-wallet/types'; import type { AssetFuel } from 'fuels'; +import { + ASSET_ENDPOINTS, + DEFAULT_ASSET_ENDPOINT, +} from '~/systems/Asset/constants'; import { AssetService } from '~/systems/Asset/services'; import { getFuelAssetByAssetId } from '~/systems/Asset/utils'; import { type FuelCachedAsset, db } from '~/systems/Core/utils/database'; -type Endpoint = { - chainId: number; - url: string; -}; - const FIVE_MINUTES = 5 * 60 * 1000; export const assetDbKeyFactory = (chainId: number, assetId: string) => `${chainId}/asset/${assetId}`; @@ -23,16 +22,7 @@ export class AssetsCache { [chainId: number]: Array; }; private static instance: AssetsCache; - private endpoints: Endpoint[] = [ - { - chainId: 9889, - url: 'https://explorer-indexer-mainnet.fuel.network', - }, - { - chainId: 0, - url: 'https://explorer-indexer-testnet.fuel.network', - }, - ]; + private storage: IndexedAssetsDB; private constructor() { @@ -48,9 +38,7 @@ export class AssetsCache { }; private getIndexerEndpoint(chainId: number) { - return this.endpoints.find( - (endpoint: Endpoint) => endpoint.chainId === chainId - ); + return ASSET_ENDPOINTS[chainId.toString()] || DEFAULT_ASSET_ENDPOINT; } static async fetchAllAssets(chainId: number, assetsIds: string[]) { diff --git a/packages/app/src/systems/Asset/components/AssetItem/AssetItem.tsx b/packages/app/src/systems/Asset/components/AssetItem/AssetItem.tsx index 067469f2cb..9336167ec0 100644 --- a/packages/app/src/systems/Asset/components/AssetItem/AssetItem.tsx +++ b/packages/app/src/systems/Asset/components/AssetItem/AssetItem.tsx @@ -50,7 +50,6 @@ export const AssetItem: AssetItemComponent = ({ shouldShowCopyAssetAddress, }) => { const navigate = useNavigate(); - const fuelAssetFromInputAsset = useFuelAsset({ asset: inputAsset }); const asset = useMemo(() => { if (!inputFuelAsset && !inputAsset && !fuelAssetFromInputAsset) @@ -65,11 +64,10 @@ export const AssetItem: AssetItemComponent = ({ } ); }, [inputFuelAsset, inputAsset, fuelAssetFromInputAsset]); + const { assetId, name, symbol, icon, decimals, isCustom } = asset ?? {}; if (!asset) return null; - const { assetId, name, symbol, icon, decimals, isCustom } = asset; - function getLeftEl() { if (assetId && shouldShowCopyAssetAddress) { return ( @@ -118,7 +116,12 @@ export const AssetItem: AssetItemComponent = ({ if (amount) { return ( - + ); } @@ -211,6 +214,8 @@ const styles = { assetName: cssObj({ margin: 0, textSize: 'base', + fontWeight: '$medium', + letterSpacing: '$normal', }), assetIdCopy: cssObj({ marginLeft: 2, @@ -240,6 +245,7 @@ const styles = { '.fuel_Button': { px: '$1 !important', color: '$intentsBase8 !important', + height: 44, }, '.fuel_Button:hover': { diff --git a/packages/app/src/systems/Asset/components/AssetItem/AssetItemAmount.tsx b/packages/app/src/systems/Asset/components/AssetItem/AssetItemAmount.tsx index 936c9d9c34..0b089ae45f 100644 --- a/packages/app/src/systems/Asset/components/AssetItem/AssetItemAmount.tsx +++ b/packages/app/src/systems/Asset/components/AssetItem/AssetItemAmount.tsx @@ -1,20 +1,23 @@ import { cssObj } from '@fuel-ui/css'; -import { Box, Text, Tooltip } from '@fuel-ui/react'; -import type { BNInput } from 'fuels'; +import { Box, Text, Tooltip, VStack } from '@fuel-ui/react'; +import { type BNInput, bn } from 'fuels'; import { useEffect, useMemo, useRef, useState } from 'react'; import { AmountVisibility, formatBalance } from '~/systems/Core'; import { useBalanceVisibility } from '~/systems/Core/hooks/useVisibility'; +import { convertToUsd } from '~/systems/Core/utils/convertToUsd'; type AssetItemAmountProps = { amount: BNInput; decimals: number | undefined; symbol: string | undefined; + rate: number | undefined; }; export const AssetItemAmount = ({ amount, decimals, symbol, + rate, }: AssetItemAmountProps) => { const { visibility } = useBalanceVisibility(); const { original, tooltip } = formatBalance(amount, decimals); @@ -27,6 +30,11 @@ export const AssetItemAmount = ({ return false; }, [tooltip, visibility, isTruncated]); + const amountInUsd = useMemo(() => { + if (amount == null || rate == null || decimals == null) return '$0'; + return convertToUsd(bn(amount), decimals, rate).formatted; + }, [amount, rate, decimals]); + useEffect(() => { if (!tooltip && amountRef.current) { const amountEl = amountRef.current; @@ -37,16 +45,32 @@ export const AssetItemAmount = ({ return ( - - - - - {symbol} - + + + + + {symbol} + + + {!!amountInUsd && amountInUsd !== '$0' && ( + + {visibility ? amountInUsd : '$••••'} + + )} + ); @@ -59,17 +83,26 @@ const styles = { minWidth: 0, alignItems: 'center', flexWrap: 'nowrap', - fontSize: '$sm', + textSize: 'base', fontWeight: '$normal', textAlign: 'right', paddingLeft: '$2', + lineHeight: '24px', }), amount: cssObj({ display: 'inline-block', overflow: 'hidden', textOverflow: 'ellipsis', + color: '$textHeading', }), symbol: cssObj({ flexShrink: 0, + color: '$textHeading', + }), + amountInUsd: cssObj({ + textSize: 'sm', + fontWeight: '$normal', + textAlign: 'right', + color: '$primary', }), }; diff --git a/packages/app/src/systems/Asset/components/AssetList/AssetList.tsx b/packages/app/src/systems/Asset/components/AssetList/AssetList.tsx index 6b3db42a23..f3f536243e 100644 --- a/packages/app/src/systems/Asset/components/AssetList/AssetList.tsx +++ b/packages/app/src/systems/Asset/components/AssetList/AssetList.tsx @@ -5,6 +5,7 @@ import { memo, useMemo, useState } from 'react'; import { AssetItem } from '../AssetItem'; +import { UnknownAssetsButton } from '~/systems/Asset/components/UnknownAssetsButton/UnknownAssetsButton'; import type { AssetListEmptyProps } from './AssetListEmpty'; import { AssetListEmpty } from './AssetListEmpty'; import { AssetListLoading } from './AssetListLoading'; @@ -58,11 +59,12 @@ export const AssetList: AssetListComponent = ({ shouldShowCopyAssetAddress={true} /> ))} - {!!(!isLoading && unknownLength) && ( - - )} + ); }; diff --git a/packages/app/src/systems/Asset/components/AssetsAmount/AssetsAmount.tsx b/packages/app/src/systems/Asset/components/AssetsAmount/AssetsAmount.tsx index 125d9e0c43..f2ca9ab3d3 100644 --- a/packages/app/src/systems/Asset/components/AssetsAmount/AssetsAmount.tsx +++ b/packages/app/src/systems/Asset/components/AssetsAmount/AssetsAmount.tsx @@ -6,13 +6,15 @@ import { Grid, Text, Tooltip, + VStack, } from '@fuel-ui/react'; import type { AssetFuelAmount } from '@fuel-wallet/types'; import { bn } from 'fuels'; -import { type FC, useEffect, useRef, useState } from 'react'; +import { type FC, useEffect, useMemo, useRef, useState } from 'react'; import { formatAmount, shortAddress } from '~/systems/Core'; import type { InsufficientInputAmountError } from '~/systems/Transaction'; +import { convertToUsd } from '~/systems/Core/utils/convertToUsd'; import { AssetsAmountLoader } from './AssetsAmountLoader'; import { styles } from './styles'; @@ -98,7 +100,12 @@ const AssetsAmountItem = ({ assetAmount }: AssetsAmountItemProps) => { decimals, amount, isNft, + rate, } = assetAmount || {}; + const amountInUsd = useMemo(() => { + if (amount == null || rate == null || decimals == null) return '$0'; + return convertToUsd(bn(amount), decimals, rate).formatted; + }, [amount, rate, decimals]); const containerRef = useRef(null); const [isTruncated, setIsTruncated] = useState(false); @@ -140,6 +147,7 @@ const AssetsAmountItem = ({ assetAmount }: AssetsAmountItemProps) => { {shortAddress(assetId)} + { open={isTruncated ? undefined : false} > - {formatted} + {formatted} {symbol} - - {symbol} + + {amountInUsd} diff --git a/packages/app/src/systems/Asset/components/AssetsAmount/styles.ts b/packages/app/src/systems/Asset/components/AssetsAmount/styles.ts index 313fa2f92b..35daf41919 100644 --- a/packages/app/src/systems/Asset/components/AssetsAmount/styles.ts +++ b/packages/app/src/systems/Asset/components/AssetsAmount/styles.ts @@ -38,7 +38,8 @@ export const styles = { '& span': { fontSize: '$sm', - color: '$intentsBase12', + fontWeight: '$medium', + color: '$textHeading', }, }), address: cssObj({ @@ -48,8 +49,8 @@ export const styles = { }), amountContainer: cssObj({ columnGap: '$1', - justifyContent: 'flex-end', - alignItems: 'center', + justifyContent: 'center', + alignItems: 'flex-end', flexWrap: 'nowrap', gridRow: '1 / 3', gridColumn: '2 / 3', @@ -57,14 +58,28 @@ export const styles = { textAlign: 'right', fontSize: '$sm', color: '$intentsBase12', + flexDirection: 'column', + rowGap: '0', }), amountValue: cssObj({ display: 'inline-block', overflow: 'hidden', textOverflow: 'ellipsis', + textAlign: 'right', + fontWeight: '$medium', + color: '$textHeading', + }), + amountInUsd: cssObj({ + fontSize: '$sm', + color: '$textSubtext', + display: 'inline-block', + overflow: 'hidden', + textOverflow: 'ellipsis', + textAlign: 'right', }), amountSymbol: cssObj({ flexShrink: 0, + textAlign: 'right', }), title: cssObj({ fontSize: '$sm', diff --git a/packages/app/src/systems/Asset/components/UnknownAssetsButton/UnknownAssetsButton.tsx b/packages/app/src/systems/Asset/components/UnknownAssetsButton/UnknownAssetsButton.tsx new file mode 100644 index 0000000000..69fcc6fbe4 --- /dev/null +++ b/packages/app/src/systems/Asset/components/UnknownAssetsButton/UnknownAssetsButton.tsx @@ -0,0 +1,31 @@ +import { Button } from '@fuel-ui/react'; + +interface UnknownAssetsButtonProps { + showUnknown: boolean; + unknownLength: number | undefined; + isLoading: boolean; + toggle: () => void; +} + +export function UnknownAssetsButton({ + showUnknown, + unknownLength, + isLoading, + toggle, +}: UnknownAssetsButtonProps) { + if (!unknownLength) return null; + + return ( + + ); +} diff --git a/packages/app/src/systems/Asset/constants/index.ts b/packages/app/src/systems/Asset/constants/index.ts new file mode 100644 index 0000000000..938729d25d --- /dev/null +++ b/packages/app/src/systems/Asset/constants/index.ts @@ -0,0 +1,20 @@ +import { CHAIN_IDS } from 'fuels'; +import type { AssetEndpoint } from '~/systems/Asset/types'; + +export const ASSET_ENDPOINTS: Record = { + [CHAIN_IDS.fuel.mainnet]: { + chainId: CHAIN_IDS.fuel.mainnet, + url: 'https://explorer-indexer-mainnet.fuel.network', + }, + [CHAIN_IDS.fuel.testnet]: { + chainId: CHAIN_IDS.fuel.testnet, + url: 'https://explorer-indexer-testnet.fuel.network', + }, +}; + +export const DEFAULT_ASSET_ENDPOINT = ASSET_ENDPOINTS[CHAIN_IDS.fuel.mainnet]; + +export const INTL_FORMATTER = Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', +}); diff --git a/packages/app/src/systems/Asset/types.ts b/packages/app/src/systems/Asset/types.ts new file mode 100644 index 0000000000..c2a081fc85 --- /dev/null +++ b/packages/app/src/systems/Asset/types.ts @@ -0,0 +1,4 @@ +export type AssetEndpoint = { + chainId: number; + url: string; +}; diff --git a/packages/app/src/systems/Core/components/AmountVisibility/AmountVisibility.tsx b/packages/app/src/systems/Core/components/AmountVisibility/AmountVisibility.tsx index 26d05a2052..1d07d76753 100644 --- a/packages/app/src/systems/Core/components/AmountVisibility/AmountVisibility.tsx +++ b/packages/app/src/systems/Core/components/AmountVisibility/AmountVisibility.tsx @@ -14,5 +14,5 @@ export function AmountVisibility({ units, }: AmountVisibilityProps) { const { formatted } = formatBalance(value, units); - return <>{visibility ? formatted.display : '•••••'}; + return visibility ? formatted.display : '•••••'; } diff --git a/packages/app/src/systems/Core/components/InputAmount/InputAmount.tsx b/packages/app/src/systems/Core/components/InputAmount/InputAmount.tsx index 5eb9022f3a..abb0af1e2a 100644 --- a/packages/app/src/systems/Core/components/InputAmount/InputAmount.tsx +++ b/packages/app/src/systems/Core/components/InputAmount/InputAmount.tsx @@ -5,6 +5,7 @@ **/ import { cssObj } from '@fuel-ui/css'; +import { HStack, VStack } from '@fuel-ui/react'; import type { BN } from 'fuels'; import { DEFAULT_DECIMAL_UNITS, bn, format } from 'fuels'; import { useEffect, useState } from 'react'; @@ -81,6 +82,7 @@ export type InputAmountProps = Omit & { units?: number; balancePrecision?: number; asset?: { name?: string; icon?: string; address?: string }; + amountInUsd?: string; assetTooltip?: string; hiddenMaxButton?: boolean; hiddenBalance?: boolean; @@ -111,6 +113,7 @@ export const InputAmount: InputAmountComponent = ({ inputProps, asset, assetTooltip, + amountInUsd = '$0', onClickAsset, ...props }) => { @@ -231,22 +234,35 @@ export const InputAmount: InputAmountComponent = ({ )} - - {!hiddenBalance && ( - - + + + + {!!amountInUsd && ( + + {amountInUsd} + + )} + - Balance: {formattedBalance} - - - )} - + + Balance: {formattedBalance} + + + + + )} ); }; @@ -333,4 +349,23 @@ const styles = { width: '$5', height: '$5', }), + inputAmountBalances: cssObj({ + width: '100%', + justifyContent: 'space-between', + gap: '$1', + alignItems: 'center', + whiteSpace: 'nowrap', + lineHeight: '$tight', + fontSize: '$sm', + fontWeight: '$normal', + }), + dashedHorizontalSeparator: cssObj({ + width: '100%', + height: 0, + backgroundColor: '$intentsBase8', + // Only one side of the border should be visible, to avoid extra "thickness" + borderTop: '1px dashed $intentsBase8', + my: '$2', + color: 'textSubtext', + }), }; diff --git a/packages/app/src/systems/Core/utils/convertToUsd.test.ts b/packages/app/src/systems/Core/utils/convertToUsd.test.ts new file mode 100644 index 0000000000..f4ebe96ede --- /dev/null +++ b/packages/app/src/systems/Core/utils/convertToUsd.test.ts @@ -0,0 +1,53 @@ +import { DECIMAL_FUEL, bn } from 'fuels'; +import { convertToUsd } from './convertToUsd'; + +const MOCK_ETH_RATE = 2742.15; +describe('Convert to USD', () => { + it('should render 0 with no decimals', () => { + const { formatted, value } = convertToUsd( + bn(0), + DECIMAL_FUEL, + MOCK_ETH_RATE + ); + expect(formatted).toBe('$0'); + expect(value).toBe(0); + }); + + it('should render only first valid number after 2 leading zeroes when < 1.00', () => { + const { formatted, value } = convertToUsd( + bn(1265), + DECIMAL_FUEL, + MOCK_ETH_RATE + ); + expect(formatted).toBe('$0.003'); + expect(value).toBe(0.003); + }); + + it('should always render 2 decimal places for numbers >= 1.00', () => { + const { formatted, value } = convertToUsd( + bn(3648500), + DECIMAL_FUEL, + MOCK_ETH_RATE + ); + expect(formatted).toBe('$10.00'); + expect(value).toBe(10); + }); + it('should not lose precision when dealing with really small amounts', () => { + const { formatted, value } = convertToUsd( + bn(1), + DECIMAL_FUEL, + MOCK_ETH_RATE + ); + expect(formatted).toBe('$0.000002'); + expect(value).toBe(0.000002); + }); + it('should be able to handle really large amounts', () => { + const { formatted, value } = convertToUsd( + bn(Number.MAX_SAFE_INTEGER), + DECIMAL_FUEL, + MOCK_ETH_RATE + ); + expect(formatted).toBe('$24,699,091,436.38'); + expect(value).toBe(24699091436.38); + }); +}); diff --git a/packages/app/src/systems/Core/utils/convertToUsd.ts b/packages/app/src/systems/Core/utils/convertToUsd.ts new file mode 100644 index 0000000000..b3ef461e16 --- /dev/null +++ b/packages/app/src/systems/Core/utils/convertToUsd.ts @@ -0,0 +1,39 @@ +import { type BN, bn } from 'fuels'; + +const DEFAULT_MIN_PRECISION = 2; +const EXTRA_PRECISION_DIGITS = 10; +const EXTRA_PRECISION = bn(10).pow(bn(EXTRA_PRECISION_DIGITS)); +const OUT_FACTOR = 10 ** DEFAULT_MIN_PRECISION; // e.g. 100 for 2 decimals + +export function convertToUsd( + amount: BN, + decimals: number, + rate: number +): { value: number; formatted: string } { + if (!rate) return { value: 0, formatted: '$0' }; + + // Convert the rate to a fixed-point integer. + const rateFixed = Math.round(rate * OUT_FACTOR); + + // Calculate the USD value in fixed-point: (amount * rateFixed * EXTRA_PRECISION) / 10^decimals. + const numerator = amount.mul(bn(rateFixed)).mul(EXTRA_PRECISION); + const denominator = bn(10).pow(bn(decimals)); + const scaledUsdBN = numerator.div(denominator); + + // The BN is scaled by EXTRA_PRECISION * OUT_FACTOR, so its total decimal places is: + const totalScaleDigits = EXTRA_PRECISION_DIGITS + DEFAULT_MIN_PRECISION; + + // Use BN.format to insert the decimal point. + const formattedUsd = scaledUsdBN.format({ + minPrecision: DEFAULT_MIN_PRECISION, + precision: DEFAULT_MIN_PRECISION, + units: totalScaleDigits, + }); + + return scaledUsdBN.isZero() + ? { value: 0, formatted: '$0' } + : { + value: Number(formattedUsd.replace(/,/g, '')), + formatted: `$${formattedUsd}`, + }; +} diff --git a/packages/app/src/systems/Home/components/HomeActions/HomeActions.tsx b/packages/app/src/systems/Home/components/HomeActions/HomeActions.tsx index 4bba447e38..716da9eafb 100644 --- a/packages/app/src/systems/Home/components/HomeActions/HomeActions.tsx +++ b/packages/app/src/systems/Home/components/HomeActions/HomeActions.tsx @@ -51,9 +51,9 @@ export const HomeActions = ({ const styles = { wrapper: cssObj({ px: '$4', - pb: '$4', + pb: '$5', mt: '$2', - mb: '$4', + mb: '$5', flexShrink: 0, gap: '$2', borderBottom: '1px solid $border', diff --git a/packages/app/src/systems/Home/pages/Home/Home.tsx b/packages/app/src/systems/Home/pages/Home/Home.tsx index 067d2b7779..23600b7c05 100644 --- a/packages/app/src/systems/Home/pages/Home/Home.tsx +++ b/packages/app/src/systems/Home/pages/Home/Home.tsx @@ -69,7 +69,7 @@ const styles = { }, }), assetsList: cssObj({ - maxHeight: 230, + maxHeight: 200, paddingBottom: '$4', ...scrollable(), overflowY: 'scroll !important', diff --git a/packages/app/src/systems/Send/components/SendSelect/SendSelect.tsx b/packages/app/src/systems/Send/components/SendSelect/SendSelect.tsx index 49d118ca86..51daa97ac1 100644 --- a/packages/app/src/systems/Send/components/SendSelect/SendSelect.tsx +++ b/packages/app/src/systems/Send/components/SendSelect/SendSelect.tsx @@ -1,5 +1,5 @@ import { cssObj } from '@fuel-ui/css'; -import { Alert, Box, Form, Input, Text } from '@fuel-ui/react'; +import { Box, Form, Input, Text } from '@fuel-ui/react'; import { motion } from 'framer-motion'; import { type BN, bn } from 'fuels'; import { useEffect, useMemo, useRef, useState } from 'react'; @@ -14,6 +14,7 @@ import { import { useController, useWatch } from 'react-hook-form'; import { InputAmount } from '~/systems/Core/components/InputAmount/InputAmount'; +import { convertToUsd } from '~/systems/Core/utils/convertToUsd'; import { TxFeeOptions } from '~/systems/Transaction/components/TxFeeOptions/TxFeeOptions'; import type { UseSendReturn } from '../../hooks'; @@ -49,11 +50,17 @@ export function SendSelect({ control: form.control, name: 'asset', }); + const selectedAsset = useMemo( + () => balances?.find((a) => a.asset?.assetId === assetId), + [assetId, balances] + ); - const decimals = useMemo(() => { - const selectedAsset = balances?.find((a) => a.asset?.assetId === assetId); - return selectedAsset?.asset?.decimals; - }, [assetId, balances]); + const decimals = selectedAsset?.asset?.decimals; + const rate = selectedAsset?.asset?.rate; + const amountInUsd = useMemo(() => { + if (amount.value == null || rate == null || decimals == null) return '$0'; + return convertToUsd(bn(amount.value), decimals, rate).formatted; + }, [amount.value, rate, decimals]); const isSendingBaseAssetId = useMemo(() => { return ( @@ -158,6 +165,7 @@ export function SendSelect({ balance={balanceAssetSelected} value={amount.value} units={decimals} + amountInUsd={amountInUsd} onChange={(val) => { if (isAmountFocused.current) { setWatchMax(false); @@ -238,6 +246,7 @@ const styles = { color: '$intentsBase12', fontSize: '$md', fontWeight: '$normal', + width: '48px', }), addressRow: cssObj({ flex: 1, diff --git a/packages/app/src/systems/Transaction/components/TxFee/TxFee.test.tsx b/packages/app/src/systems/Transaction/components/TxFee/TxFee.test.tsx deleted file mode 100644 index 5766a5fc05..0000000000 --- a/packages/app/src/systems/Transaction/components/TxFee/TxFee.test.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { render, screen, testA11y } from '@fuel-ui/test-utils'; -import { bn } from 'fuels'; - -import { TxFee } from './TxFee'; - -describe('TxFee', () => { - it('a11y', async () => { - await testA11y(); - }); - - it('should be able to show the transaction fee', async () => { - const feeCost = bn(6); - render(); - expect(await screen.findByText(/fee \(network\)/i)).toBeInTheDocument(); - const valFee = screen.getByLabelText(/Fee value/i); - expect(valFee).toBeInTheDocument(); - expect(valFee.innerHTML.trim()).toBe(`${feeCost.format()} ETH`); - }); -}); diff --git a/packages/app/src/systems/Transaction/components/TxFee/TxFee.tsx b/packages/app/src/systems/Transaction/components/TxFee/TxFee.tsx index 01bde372be..9322be02a8 100644 --- a/packages/app/src/systems/Transaction/components/TxFee/TxFee.tsx +++ b/packages/app/src/systems/Transaction/components/TxFee/TxFee.tsx @@ -1,7 +1,11 @@ -import { Card, Text } from '@fuel-ui/react'; +import { Card, HStack, Text } from '@fuel-ui/react'; import { type BN, DEFAULT_PRECISION } from 'fuels'; -import type { FC } from 'react'; +import { type FC, useEffect, useMemo, useState } from 'react'; +import type { AssetFuelData } from '@fuel-wallet/types'; +import { AssetsCache } from '~/systems/Asset/cache/AssetsCache'; +import { convertToUsd } from '~/systems/Core/utils/convertToUsd'; +import { useProvider } from '~/systems/Network/hooks/useProvider'; import { TxFeeLoader } from './TxFeeLoader'; import { styles } from './styles'; @@ -22,30 +26,86 @@ export const TxFee: TxFeeComponent = ({ onChecked, title, }: TxFeeProps) => { + const [flag, setFlag] = useState(false); + const provider = useProvider(); + const [baseAsset, setBaseAsset] = useState(); + useEffect(() => { + let abort = false; + const getBaseAsset = async () => { + const [baseAssetId, chainId] = await Promise.all([ + provider?.getBaseAssetId(), + provider?.getChainId(), + ]); + if (abort || baseAssetId == null || chainId == null) return; + const baseAsset = await AssetsCache.getInstance().getAsset({ + chainId: chainId, + assetId: baseAssetId, + dbAssets: [], + save: false, + }); + if (abort) return; + setBaseAsset(baseAsset); + }; + getBaseAsset(); + return () => { + abort = true; + }; + }, [provider]); + + const feeInUsd = useMemo(() => { + if (baseAsset?.rate == null || fee == null) return '$0'; + + return convertToUsd(fee, baseAsset.decimals, baseAsset.rate).formatted; + }, [baseAsset, fee]); + + const ready = !!fee && !!feeInUsd; + + // Horrible workaround to force re-render of this section. + useEffect(() => { + setTimeout(() => { + setFlag((prev) => !prev); + }, 500); + }, [ready]); + + if (!ready) return ; + return ( onChecked?.(true)} > {title || 'Fee (network)'} - - {fee - ? `${fee.format({ - minPrecision: DEFAULT_PRECISION, - precision: DEFAULT_PRECISION, - })} ETH` - : '--'} - + + {!!feeInUsd && ( + + {feeInUsd} + + )} + + ( + {fee + ? `${fee.format({ + minPrecision: DEFAULT_PRECISION, + precision: DEFAULT_PRECISION, + })} ETH` + : '--'} + ) + + ); }; diff --git a/packages/app/src/systems/Transaction/components/TxFee/styles.tsx b/packages/app/src/systems/Transaction/components/TxFee/styles.tsx index 268e1c9c81..aec2e72a0b 100644 --- a/packages/app/src/systems/Transaction/components/TxFee/styles.tsx +++ b/packages/app/src/systems/Transaction/components/TxFee/styles.tsx @@ -1,14 +1,15 @@ import { cssObj } from '@fuel-ui/css'; export const styles = { - detailItem: (active = false, pointer = false) => + detailItem: (active = false, pointer = false, title = false) => cssObj({ - padding: '$3 $4', - flexDirection: 'row', + padding: title ? '$3 $4' : '$2 $6', + flexDirection: title ? 'row' : 'column', justifyContent: 'space-between', - alignItems: 'center', + alignItems: title ? 'center' : 'flex-start', display: 'flex', - columnGap: '$4', + columnGap: title ? '$4' : '$6', + gap: title ? undefined : '$1', position: 'relative', cursor: pointer ? 'pointer' : 'auto', @@ -28,12 +29,25 @@ export const styles = { }), title: cssObj({ fontSize: '$sm', - fontWeight: '$normal', + lineHeight: '20px', + fontWeight: '$medium', textWrap: 'nowrap', }), + fee: (flag = false) => + cssObj({ + height: flag ? '21px' : 'auto', + }), amount: cssObj({ fontSize: '$sm', - fontWeight: '$normal', + lineHeight: '20px', + fontWeight: '$medium', + wordWrap: 'break-word', + minWidth: 0, + }), + usd: cssObj({ + fontSize: '$sm', + lineHeight: '20px', + fontWeight: '600', wordWrap: 'break-word', minWidth: 0, }), diff --git a/packages/app/src/systems/Transaction/components/TxFeeOptions/TxFeeOptions.tsx b/packages/app/src/systems/Transaction/components/TxFeeOptions/TxFeeOptions.tsx index 3c5d032791..f8d160d655 100644 --- a/packages/app/src/systems/Transaction/components/TxFeeOptions/TxFeeOptions.tsx +++ b/packages/app/src/systems/Transaction/components/TxFeeOptions/TxFeeOptions.tsx @@ -1,3 +1,4 @@ +import { cssObj } from '@fuel-ui/css'; import { Box, Button, Form, HStack, Input, Text, VStack } from '@fuel-ui/react'; import { AnimatePresence } from 'framer-motion'; import { type BN, bn } from 'fuels'; @@ -46,10 +47,20 @@ export const TxFeeOptions = ({ name: 'fees.gasLimit', }); + const advancedFee = baseFee.add(tip.value.amount); + const options = useMemo(() => { return [ - { name: 'Regular', fee: baseFee.add(regularTip), tip: regularTip }, - { name: 'Fast', fee: baseFee.add(fastTip), tip: fastTip }, + { + name: 'Regular', + fee: baseFee.add(regularTip), + tip: regularTip, + }, + { + name: 'Fast', + fee: baseFee.add(fastTip), + tip: fastTip, + }, ]; }, [baseFee, regularTip, fastTip]); @@ -88,11 +99,7 @@ export const TxFeeOptions = ({ {isAdvanced ? ( - + @@ -196,7 +203,17 @@ export const TxFeeOptions = ({ direction="column" layout > - diff --git a/packages/app/src/systems/Transaction/services/transaction.tsx b/packages/app/src/systems/Transaction/services/transaction.tsx index 1ec72b9562..d71e533efe 100644 --- a/packages/app/src/systems/Transaction/services/transaction.tsx +++ b/packages/app/src/systems/Transaction/services/transaction.tsx @@ -19,6 +19,8 @@ import { WalletLockedCustom, db } from '~/systems/Core'; import { createProvider } from '@fuel-wallet/connections'; import { AccountService } from '~/systems/Account/services/account'; +import { AssetsCache } from '~/systems/Asset/cache/AssetsCache'; +import { convertToUsd } from '~/systems/Core/utils/convertToUsd'; import { graphqlRequest } from '~/systems/Core/utils/graphql'; import { NetworkService } from '~/systems/Network/services/network'; import type { TransactionCursor } from '../types'; @@ -260,12 +262,31 @@ export class TxService { // Adding 1 magical unit to match the fake unit that is added on TS SDK (.add(1)) const feeAdaptedToSdkDiff = txSummary.fee.add(1); - + const [chainId, baseAssetId] = await Promise.all([ + provider.getChainId(), + provider.getBaseAssetId(), + ]); + const baseAsset = await AssetsCache.getInstance().getAsset({ + chainId: chainId, + assetId: baseAssetId, + dbAssets: [], + save: false, + }); + const feeInUsd = + baseAsset != null + ? convertToUsd( + feeAdaptedToSdkDiff, + baseAsset?.decimals, + // biome-ignore lint/suspicious/noExplicitAny: @fuel-ts/accounts types are not updated + (baseAsset as any)?.rate + ).formatted + : '$0'; return { baseFee, txSummary: { ...txSummary, fee: feeAdaptedToSdkDiff, + feeInUsd, }, proposedTxRequest, }; diff --git a/packages/types/src/accounts.ts b/packages/types/src/accounts.ts index af5541c5be..2adecb41dd 100644 --- a/packages/types/src/accounts.ts +++ b/packages/types/src/accounts.ts @@ -24,6 +24,8 @@ export type Account = { export type AccountBalance = { balance: BN; balanceSymbol: string; + amountInUsd: string | undefined; + totalBalanceInUsd: number; balances: CoinAsset[]; }; diff --git a/packages/types/src/asset.ts b/packages/types/src/asset.ts index b0985e45c9..a72073fbef 100644 --- a/packages/types/src/asset.ts +++ b/packages/types/src/asset.ts @@ -20,6 +20,7 @@ export type AssetFuelData = AssetFuel & { indexed?: boolean; suspicious?: boolean; collection?: string; + rate?: number; isNft?: boolean; verified?: boolean; metadata?: { diff --git a/packages/types/src/coin.ts b/packages/types/src/coin.ts index 412b48d80d..817cbfcc31 100644 --- a/packages/types/src/coin.ts +++ b/packages/types/src/coin.ts @@ -3,4 +3,5 @@ import type { BNInput } from 'fuels'; export type Coin = { assetId: string; amount?: BNInput; + amountInUsd?: string; };