diff --git a/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsOrderTransactionView.test.tsx b/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsOrderTransactionView.test.tsx
index eaa8e32e9211..85765781939f 100644
--- a/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsOrderTransactionView.test.tsx
+++ b/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsOrderTransactionView.test.tsx
@@ -94,7 +94,7 @@ describe('PerpsOrderTransactionView', () => {
mockUsePerpsOrderFees.mockReturnValue({
totalFee: 10.5,
protocolFee: 7.5,
- metamaskFee: 3.0,
+ metamaskFee: 3,
protocolFeeRate: 0.1,
metamaskFeeRate: 0.05,
isLoadingMetamaskFee: false,
@@ -162,7 +162,7 @@ describe('PerpsOrderTransactionView', () => {
expect(zeroFees).toHaveLength(3); // All three fees should be $0
});
- it('should handle small fees correctly', () => {
+ it('should show "< $0.01" for fees less than 0.01', () => {
mockUsePerpsOrderFees.mockReturnValue({
totalFee: 0.005,
protocolFee: 0.003,
@@ -173,11 +173,135 @@ describe('PerpsOrderTransactionView', () => {
error: null,
});
- const { getByText } = render();
+ const { getAllByText } = render();
+
+ // All three fees should show "< $0.01" since they're all less than 0.01
+ const smallFeeLabels = getAllByText('< $0.01');
+ expect(smallFeeLabels).toHaveLength(3);
+ });
+
+ it('should format fees normally when they are exactly 0.01', () => {
+ mockUsePerpsOrderFees.mockReturnValue({
+ totalFee: 0.03,
+ protocolFee: 0.01,
+ metamaskFee: 0.01,
+ protocolFeeRate: 0.1,
+ metamaskFeeRate: 0.05,
+ isLoadingMetamaskFee: false,
+ error: null,
+ });
+
+ const { getAllByText, queryByText, getByText } = render(
+ ,
+ );
+
+ // Fees at exactly 0.01 should be formatted normally, not show "< $0.01"
+ expect(queryByText('< $0.01')).toBeNull();
+ // Both metamask and protocol fees are 0.01
+ const fee01Labels = getAllByText('$0.01');
+ expect(fee01Labels.length).toBeGreaterThanOrEqual(2);
+ expect(getByText('$0.03')).toBeTruthy(); // Total fee
+ });
+
+ it('should format fees normally when they are greater than 0.01', () => {
+ mockUsePerpsOrderFees.mockReturnValue({
+ totalFee: 0.015,
+ protocolFee: 0.012,
+ metamaskFee: 0.003,
+ protocolFeeRate: 0.1,
+ metamaskFeeRate: 0.05,
+ isLoadingMetamaskFee: false,
+ error: null,
+ });
+
+ const { getByText, getAllByText } = render();
+
+ // Metamask fee is less than 0.01, should show "< $0.01"
+ expect(getAllByText('< $0.01')).toHaveLength(1);
+ // Protocol and total fees are >= 0.01, should be formatted normally
+ expect(getByText('$0.01')).toBeTruthy(); // Protocol fee formatted
+ expect(getByText('$0.02')).toBeTruthy(); // Total fee formatted (rounded)
+ });
+
+ it('should handle mixed small and large fees correctly', () => {
+ mockUsePerpsOrderFees.mockReturnValue({
+ totalFee: 0.025,
+ protocolFee: 0.02,
+ metamaskFee: 0.005,
+ protocolFeeRate: 0.1,
+ metamaskFeeRate: 0.05,
+ isLoadingMetamaskFee: false,
+ error: null,
+ });
+
+ const { getByText, getAllByText } = render();
+
+ // Metamask fee is less than 0.01
+ const smallFeeLabels = getAllByText('< $0.01');
+ expect(smallFeeLabels).toHaveLength(1);
+ // Protocol and total fees are >= 0.01, should be formatted
+ expect(getByText('$0.02')).toBeTruthy(); // Protocol fee
+ expect(getByText('$0.03')).toBeTruthy(); // Total fee (rounded)
+ });
+
+ it('should handle edge case: fee just below 0.01 threshold', () => {
+ mockUsePerpsOrderFees.mockReturnValue({
+ totalFee: 0.029,
+ protocolFee: 0.0099,
+ metamaskFee: 0.0099,
+ protocolFeeRate: 0.1,
+ metamaskFeeRate: 0.05,
+ isLoadingMetamaskFee: false,
+ error: null,
+ });
+
+ const { getAllByText } = render();
+
+ // Both metamask and protocol fees are just below 0.01
+ const smallFeeLabels = getAllByText('< $0.01');
+ expect(smallFeeLabels).toHaveLength(2);
+ // Total fee is >= 0.01, should be formatted
+ });
+
+ it('should handle edge case: fee just above 0.01 threshold', () => {
+ mockUsePerpsOrderFees.mockReturnValue({
+ totalFee: 0.0201,
+ protocolFee: 0.0101,
+ metamaskFee: 0.01,
+ protocolFeeRate: 0.1,
+ metamaskFeeRate: 0.05,
+ isLoadingMetamaskFee: false,
+ error: null,
+ });
+
+ const { queryByText, getAllByText, getByText } = render(
+ ,
+ );
+
+ // All fees are >= 0.01, should be formatted normally
+ expect(queryByText('< $0.01')).toBeNull();
+ // Metamask fee and protocol fee (rounded) both show $0.01
+ const fee01Labels = getAllByText('$0.01');
+ expect(fee01Labels.length).toBeGreaterThanOrEqual(2);
+ expect(getByText('$0.02')).toBeTruthy(); // Total fee (rounded)
+ });
+
+ it('should show "< $0.01" for all fees when all are below threshold', () => {
+ mockUsePerpsOrderFees.mockReturnValue({
+ totalFee: 0.008,
+ protocolFee: 0.005,
+ metamaskFee: 0.003,
+ protocolFeeRate: 0.1,
+ metamaskFeeRate: 0.05,
+ isLoadingMetamaskFee: false,
+ error: null,
+ });
+
+ const { getAllByText } = render();
- expect(getByText('$0.002')).toBeTruthy();
- expect(getByText('$0.003')).toBeTruthy();
- expect(getByText('$0.005')).toBeTruthy();
+ // All three fees are below 0.01
+ const smallFeeLabels = getAllByText('< $0.01');
+ expect(smallFeeLabels).toHaveLength(3);
});
it('should navigate to block explorer in browser tab when button is pressed', () => {
diff --git a/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsOrderTransactionView.tsx b/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsOrderTransactionView.tsx
index c6eee742d05e..b3c69c164970 100644
--- a/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsOrderTransactionView.tsx
+++ b/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsOrderTransactionView.tsx
@@ -11,7 +11,6 @@ import Text, {
TextVariant,
} from '../../../../../component-library/components/Texts/Text';
-import { BigNumber } from 'bignumber.js';
import { useSelector } from 'react-redux';
import { PerpsTransactionSelectorsIDs } from '../../../../../../e2e/selectors/Perps/Perps.selectors';
import Button, {
@@ -28,6 +27,7 @@ import { usePerpsBlockExplorerUrl, usePerpsOrderFees } from '../../hooks';
import { PerpsNavigationParamList } from '../../types/navigation';
import { PerpsOrderTransactionRouteProp } from '../../types/transactionHistory';
import {
+ formatFee,
formatPerpsFiat,
formatTransactionDate,
} from '../../utils/formatUtils';
@@ -103,44 +103,21 @@ const PerpsOrderTransactionView: React.FC = () => {
];
const isFilled = transaction.order?.text === 'Filled';
+
// Fee breakdown
const feeRows = [
{
label: strings('perps.transactions.order.metamask_fee'),
- value: `${
- isFilled
- ? `${
- BigNumber(metamaskFee).isLessThan(0.01)
- ? `$${metamaskFee}`
- : formatPerpsFiat(metamaskFee)
- }`
- : '$0'
- }`,
+ value: formatFee(isFilled ? metamaskFee : 0),
},
{
label: strings('perps.transactions.order.hyperliquid_fee'),
- value: `${
- isFilled
- ? `${
- BigNumber(protocolFee).isLessThan(0.01)
- ? `$${protocolFee}`
- : formatPerpsFiat(protocolFee)
- }`
- : '$0'
- }`,
+ value: formatFee(isFilled ? protocolFee : 0),
},
{
label: strings('perps.transactions.order.total_fee'),
- value: `${
- isFilled
- ? `${
- BigNumber(totalFee).isLessThan(0.01)
- ? `$${totalFee}`
- : formatPerpsFiat(totalFee)
- }`
- : '$0'
- }`,
+ value: formatFee(isFilled ? totalFee : 0),
},
];
diff --git a/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsPositionTransactionView.test.tsx b/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsPositionTransactionView.test.tsx
index 05e12176fee4..9776b363ee95 100644
--- a/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsPositionTransactionView.test.tsx
+++ b/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsPositionTransactionView.test.tsx
@@ -361,7 +361,7 @@ describe('PerpsPositionTransactionView', () => {
expect(getByText('$5')).toBeOnTheScreen();
});
- it('should display fees with $ prefix directly for amounts < 0.01', () => {
+ it('should display fees with < $0.01 label for amounts < 0.01', () => {
// Given a transaction with fee less than 0.01
const smallFeeTransaction = {
...mockTransaction,
@@ -379,9 +379,9 @@ describe('PerpsPositionTransactionView', () => {
state: mockInitialState,
});
- // Then fee should display with $ prefix directly (not formatted through formatPerpsFiat)
+ // Then fee should display with < $0.01 label (not the actual value)
expect(getByText('Total fees')).toBeOnTheScreen();
- expect(getByText('$0.005')).toBeOnTheScreen();
+ expect(getByText('< $0.01')).toBeOnTheScreen();
});
it('should not render points when not present', () => {
diff --git a/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsPositionTransactionView.tsx b/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsPositionTransactionView.tsx
index 42f1dd396021..6f9db04cfd6f 100644
--- a/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsPositionTransactionView.tsx
+++ b/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsPositionTransactionView.tsx
@@ -30,6 +30,7 @@ import {
PerpsTransaction,
} from '../../types/transactionHistory';
import {
+ formatFee,
formatPerpsFiat,
formatTransactionDate,
PRICE_RANGES_UNIVERSAL,
@@ -115,9 +116,7 @@ const PerpsPositionTransactionView: React.FC = () => {
transaction.fill?.fee !== undefined &&
transaction.fill?.fee !== null && {
label: strings('perps.transactions.position.fees'),
- value: BigNumber(transaction.fill.fee).isGreaterThan(0.01)
- ? formatPerpsFiat(transaction.fill.fee)
- : `$${transaction.fill.fee}`,
+ value: formatFee(transaction.fill.fee),
textColor: TextColor.Default,
},
].filter(Boolean);
diff --git a/app/components/UI/Perps/utils/formatUtils.test.ts b/app/components/UI/Perps/utils/formatUtils.test.ts
index 8d9eafa47de5..cf231c46565b 100644
--- a/app/components/UI/Perps/utils/formatUtils.test.ts
+++ b/app/components/UI/Perps/utils/formatUtils.test.ts
@@ -15,6 +15,7 @@ import {
formatTransactionDate,
formatDateSection,
formatFundingRate,
+ formatFee,
PRICE_RANGES_UNIVERSAL,
PRICE_RANGES_MINIMAL_VIEW,
} from './formatUtils';
@@ -194,6 +195,140 @@ describe('formatUtils', () => {
});
});
+ describe('formatFee', () => {
+ it('returns "$0" when fee is exactly zero', () => {
+ // Given a fee of exactly zero
+ const fee = 0;
+
+ // When formatting the fee
+ const result = formatFee(fee);
+
+ // Then it returns "$0"
+ expect(result).toBe('$0');
+ });
+
+ it('returns "< $0.01" when fee is below threshold', () => {
+ // Given a fee below the 0.01 threshold
+ const fee = 0.005;
+
+ // When formatting the fee
+ const result = formatFee(fee);
+
+ // Then it returns "< $0.01"
+ expect(result).toBe('< $0.01');
+ });
+
+ it('formats fee normally when exactly at 0.01 threshold', () => {
+ // Given a fee at exactly the 0.01 threshold
+ const fee = 0.01;
+
+ // When formatting the fee
+ const result = formatFee(fee);
+
+ // Then it formats normally
+ expect(result).toBe('$0.01');
+ });
+
+ it('formats fee normally when above threshold', () => {
+ // Given a fee above the 0.01 threshold
+ const fee = 1.5;
+
+ // When formatting the fee
+ const result = formatFee(fee);
+
+ // Then it formats normally
+ expect(result).toBe('$1.50');
+ });
+
+ it('returns "< $0.01" for very small positive fees', () => {
+ // Given a very small positive fee
+ const fee = 0.0001;
+
+ // When formatting the fee
+ const result = formatFee(fee);
+
+ // Then it returns "< $0.01"
+ expect(result).toBe('< $0.01');
+ });
+
+ it('returns "< $0.01" for fee just below threshold', () => {
+ // Given a fee just below the 0.01 threshold
+ const fee = 0.0099;
+
+ // When formatting the fee
+ const result = formatFee(fee);
+
+ // Then it returns "< $0.01"
+ expect(result).toBe('< $0.01');
+ });
+
+ it('formats fee normally when just above threshold', () => {
+ // Given a fee just above the 0.01 threshold
+ const fee = 0.0101;
+
+ // When formatting the fee
+ const result = formatFee(fee);
+
+ // Then it formats normally (rounded to $0.01)
+ expect(result).toBe('$0.01');
+ });
+
+ it('formats large fees with proper decimals', () => {
+ // Given a large fee value
+ const fee = 123.45;
+
+ // When formatting the fee
+ const result = formatFee(fee);
+
+ // Then it formats with proper decimals
+ expect(result).toBe('$123.45');
+ });
+
+ it('strips trailing zeros for whole number fees', () => {
+ // Given a whole number fee
+ const fee = 100;
+
+ // When formatting the fee
+ const result = formatFee(fee);
+
+ // Then trailing zeros are stripped
+ expect(result).toBe('$100');
+ });
+
+ it('handles fees with many decimal places', () => {
+ // Given a fee with many decimal places
+ const fee = 1.23456789;
+
+ // When formatting the fee
+ const result = formatFee(fee);
+
+ // Then it rounds appropriately
+ expect(result).toBe('$1.23');
+ });
+
+ it('returns "$0" for negative zero', () => {
+ // Given a negative zero value
+ const fee = -0;
+
+ // When formatting the fee
+ const result = formatFee(fee);
+
+ // Then it returns "$0"
+ expect(result).toBe('$0');
+ });
+
+ it('returns "< $0.01" for smallest representable positive fee', () => {
+ // Given the smallest positive fee
+ const fee = 0.00000001;
+
+ // When formatting the fee
+ const result = formatFee(fee);
+
+ // Then it returns "< $0.01"
+ expect(result).toBe('< $0.01');
+ });
+ });
+
describe('formatPerpsFiat', () => {
it('should format balance with default 2 decimal places (fiat-style stripping)', () => {
expect(formatPerpsFiat(1234.56)).toBe('$1,234.56'); // Has meaningful decimals: preserved
diff --git a/app/components/UI/Perps/utils/formatUtils.ts b/app/components/UI/Perps/utils/formatUtils.ts
index 3470a1fab1bb..8a965b150abc 100644
--- a/app/components/UI/Perps/utils/formatUtils.ts
+++ b/app/components/UI/Perps/utils/formatUtils.ts
@@ -1,6 +1,7 @@
/**
* Shared formatting utilities for Perps components
*/
+import { BigNumber } from 'bignumber.js';
import { formatWithThreshold } from '../../../../util/assets';
import {
FUNDING_RATE_CONFIG,
@@ -341,6 +342,26 @@ export const formatPerpsFiat = (
return formatted;
};
+/**
+ * Formats a fee value as USD currency with appropriate decimal places
+ * @param fee - Raw numeric or string fee value (e.g., 1234.56, not token minimal denomination)
+ * @returns Formatted currency string with variable decimals based on configured ranges
+ * @example formatFee(1234.56) => "$1,234.56"
+ * @example formatFee(0.005) => "< $0.01"
+ * @example formatFee(0) => "$0"
+ */
+export const formatFee = (fee: number | string): string => {
+ const smallFeeThreshold = 0.01;
+
+ if (BigNumber(fee).isEqualTo(0)) {
+ return '$0';
+ }
+ if (BigNumber(fee).isLessThan(smallFeeThreshold)) {
+ return '< $0.01';
+ }
+ return formatPerpsFiat(fee);
+};
+
/**
* Default price range configurations
* Applied in order - first matching condition wins