diff --git a/.changeset/brown-wasps-swim.md b/.changeset/brown-wasps-swim.md
new file mode 100644
index 000000000..2e8acae0a
--- /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 a845151cc..000000000
--- a/.changeset/tall-suns-carry.md
+++ /dev/null
@@ -1,2 +0,0 @@
----
----
diff --git a/packages/app/playwright/crx/assets.test.ts b/packages/app/playwright/crx/assets.test.ts
index 813bd8d9f..92fe62438 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 ca8e95d02..c9fee0c67 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 00ba9c7a0..97366128a 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 fecedaa83..8d63ab990 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 6b443cd78..b6da25b8a 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 b95be3930..d6ba89567 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 = await provider.getChainId();
+
const balanceAssets = await AssetsCache.fetchAllAssets(
- await 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 2ca1660cb..ea3eb1fbd 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 067469f2c..9336167ec 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 936c9d9c3..0b089ae45 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 6b3db42a2..f3f536243 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 125d9e0c4..f2ca9ab3d 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 313fa2f92..35daf4191 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 000000000..69fcc6fbe
--- /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 000000000..938729d25
--- /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 000000000..c2a081fc8
--- /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 26d05a205..1d07d7675 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 5eb9022f3..abb0af1e2 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 000000000..f4ebe96ed
--- /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 000000000..b3ef461e1
--- /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 4bba447e3..716da9eaf 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 067d2b777..23600b7c0 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 d19cdf415..695bc3ee6 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';
@@ -50,11 +51,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]);
useEffect(() => {
let abort = false;
@@ -167,6 +174,7 @@ export function SendSelect({
balance={balanceAssetSelected}
value={amount.value}
units={decimals}
+ amountInUsd={amountInUsd}
onChange={(val) => {
if (isAmountFocused.current) {
setWatchMax(false);
@@ -247,6 +255,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 5766a5fc0..000000000
--- 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 01bde372b..9322be02a 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 268e1c9c8..aec2e72a0 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 3c5d03279..f8d160d65 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
>
-