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