From ae84bd04bafce8b3ba7de6ff49d782f692ebedb5 Mon Sep 17 00:00:00 2001 From: Nicholas Smith Date: Thu, 30 Oct 2025 18:49:58 -0500 Subject: [PATCH 1/2] fix: ensure that close position screen uses correct decimals --- .../PerpsClosePositionView.test.tsx | 274 ++++++++++++++++++ .../PerpsClosePositionView.tsx | 8 +- 2 files changed, 280 insertions(+), 2 deletions(-) diff --git a/app/components/UI/Perps/Views/PerpsClosePositionView/PerpsClosePositionView.test.tsx b/app/components/UI/Perps/Views/PerpsClosePositionView/PerpsClosePositionView.test.tsx index ad23260b241a..dfeaa02480c1 100644 --- a/app/components/UI/Perps/Views/PerpsClosePositionView/PerpsClosePositionView.test.tsx +++ b/app/components/UI/Perps/Views/PerpsClosePositionView/PerpsClosePositionView.test.tsx @@ -2246,6 +2246,280 @@ describe('PerpsClosePositionView', () => { }); }); + describe('Market Data and szDecimals Integration', () => { + it('passes szDecimals from market data to formatPositionSize', () => { + // Arrange - Set market data with specific szDecimals + usePerpsMarketDataMock.mockReturnValue({ + marketData: { szDecimals: 5 }, + isLoading: false, + error: null, + }); + + const mockPosition = { + ...defaultPerpsPositionMock, + size: '0.12345', + coin: 'BTC', + }; + useRouteMock.mockReturnValue({ + params: { position: mockPosition }, + }); + + // Act + renderWithProvider( + , + { state: STATE_MOCK }, + true, + ); + + // Assert - Component renders without error and uses market data + expect(usePerpsMarketDataMock).toHaveBeenCalledWith('BTC'); + }); + + it('formats position size with different szDecimals values', () => { + // Arrange - Test with different decimal precision + const testCases = [ + { szDecimals: 1, coin: 'DOGE', size: '123.456789' }, + { szDecimals: 4, coin: 'ETH', size: '1.23456789' }, + { szDecimals: 5, coin: 'BTC', size: '0.123456789' }, + { szDecimals: 6, coin: 'SOL', size: '0.000123456' }, + ]; + + testCases.forEach(({ szDecimals, coin, size }) => { + // Arrange + usePerpsMarketDataMock.mockReturnValue({ + marketData: { szDecimals }, + isLoading: false, + error: null, + }); + + const mockPosition = { + ...defaultPerpsPositionMock, + size, + coin, + }; + useRouteMock.mockReturnValue({ + params: { position: mockPosition }, + }); + + // Act + const { queryByTestId } = renderWithProvider( + , + { state: STATE_MOCK }, + true, + ); + + // Assert - Component renders successfully with szDecimals + expect( + queryByTestId( + PerpsClosePositionViewSelectorsIDs.CLOSE_POSITION_CONFIRM_BUTTON, + ), + ).toBeDefined(); + expect(usePerpsMarketDataMock).toHaveBeenCalledWith(coin); + }); + }); + + it('handles missing market data with undefined szDecimals', () => { + // Arrange - Market data is null + usePerpsMarketDataMock.mockReturnValue({ + marketData: null, + isLoading: false, + error: null, + }); + + const mockPosition = { + ...defaultPerpsPositionMock, + size: '1.5', + coin: 'ETH', + }; + useRouteMock.mockReturnValue({ + params: { position: mockPosition }, + }); + + // Act + const { queryByTestId } = renderWithProvider( + , + { state: STATE_MOCK }, + true, + ); + + // Assert - Component renders and falls back to default formatting + expect( + queryByTestId( + PerpsClosePositionViewSelectorsIDs.CLOSE_POSITION_CONFIRM_BUTTON, + ), + ).toBeDefined(); + }); + + it('handles loading state while fetching market data', () => { + // Arrange - Market data is loading + usePerpsMarketDataMock.mockReturnValue({ + marketData: null, + isLoading: true, + error: null, + }); + + // Act + const { queryByTestId } = renderWithProvider( + , + { state: STATE_MOCK }, + true, + ); + + // Assert - Component renders while loading + expect( + queryByTestId( + PerpsClosePositionViewSelectorsIDs.CLOSE_POSITION_CONFIRM_BUTTON, + ), + ).toBeDefined(); + }); + + it('handles market data fetch error gracefully', () => { + // Arrange - Market data fetch failed + usePerpsMarketDataMock.mockReturnValue({ + marketData: null, + isLoading: false, + error: 'Failed to fetch market data', + }); + + // Act + const { queryByTestId } = renderWithProvider( + , + { state: STATE_MOCK }, + true, + ); + + // Assert - Component still renders with error state + expect( + queryByTestId( + PerpsClosePositionViewSelectorsIDs.CLOSE_POSITION_CONFIRM_BUTTON, + ), + ).toBeDefined(); + }); + + it('uses correct szDecimals for different assets', () => { + // Arrange - Test common crypto assets with their typical szDecimals + const assetConfigs = [ + { coin: 'BTC', szDecimals: 5, size: '0.00123' }, + { coin: 'ETH', szDecimals: 4, size: '1.2345' }, + { coin: 'DOGE', szDecimals: 1, size: '1000.5' }, + { coin: 'SOL', szDecimals: 3, size: '10.123' }, + ]; + + assetConfigs.forEach(({ coin, szDecimals, size }) => { + // Arrange + usePerpsMarketDataMock.mockReturnValue({ + marketData: { szDecimals }, + isLoading: false, + error: null, + }); + + const mockPosition = { + ...defaultPerpsPositionMock, + coin, + size, + }; + useRouteMock.mockReturnValue({ + params: { position: mockPosition }, + }); + + // Act + renderWithProvider( + , + { state: STATE_MOCK }, + true, + ); + + // Assert - Fetches market data for specific asset + expect(usePerpsMarketDataMock).toHaveBeenCalledWith(coin); + }); + }); + + it('formats very small position sizes with high decimal precision', () => { + // Arrange - Test very small amounts with high precision + usePerpsMarketDataMock.mockReturnValue({ + marketData: { szDecimals: 8 }, + isLoading: false, + error: null, + }); + + const mockPosition = { + ...defaultPerpsPositionMock, + size: '0.00000123', + coin: 'BTC', + }; + useRouteMock.mockReturnValue({ + params: { position: mockPosition }, + }); + + // Act + const { queryByTestId } = renderWithProvider( + , + { state: STATE_MOCK }, + true, + ); + + // Assert - Component handles high precision decimals + expect( + queryByTestId( + PerpsClosePositionViewSelectorsIDs.CLOSE_POSITION_CONFIRM_BUTTON, + ), + ).toBeDefined(); + }); + + it('formats large position sizes with low decimal precision', () => { + // Arrange - Test large amounts with low precision + usePerpsMarketDataMock.mockReturnValue({ + marketData: { szDecimals: 1 }, + isLoading: false, + error: null, + }); + + const mockPosition = { + ...defaultPerpsPositionMock, + size: '123456.7', + coin: 'DOGE', + }; + useRouteMock.mockReturnValue({ + params: { position: mockPosition }, + }); + + // Act + const { queryByTestId } = renderWithProvider( + , + { state: STATE_MOCK }, + true, + ); + + // Assert - Component handles large numbers with low precision + expect( + queryByTestId( + PerpsClosePositionViewSelectorsIDs.CLOSE_POSITION_CONFIRM_BUTTON, + ), + ).toBeDefined(); + }); + + it('fetches market data on component mount with position coin', () => { + // Arrange + const mockPosition = { + ...defaultPerpsPositionMock, + coin: 'ETH', + }; + useRouteMock.mockReturnValue({ + params: { position: mockPosition }, + }); + + // Act + renderWithProvider( + , + { state: STATE_MOCK }, + true, + ); + + // Assert - Hook called with correct asset symbol + expect(usePerpsMarketDataMock).toHaveBeenCalledWith('ETH'); + }); + }); + describe('Rewards Points Row', () => { it('should render RewardsAnimations component when rewards are enabled', async () => { // Arrange diff --git a/app/components/UI/Perps/Views/PerpsClosePositionView/PerpsClosePositionView.tsx b/app/components/UI/Perps/Views/PerpsClosePositionView/PerpsClosePositionView.tsx index 4cc9e051f914..247458ca184f 100644 --- a/app/components/UI/Perps/Views/PerpsClosePositionView/PerpsClosePositionView.tsx +++ b/app/components/UI/Perps/Views/PerpsClosePositionView/PerpsClosePositionView.tsx @@ -44,6 +44,7 @@ import { usePerpsOrderFees, usePerpsRewards, usePerpsToasts, + usePerpsMarketData, } from '../../hooks'; import { usePerpsLivePrices } from '../../hooks/stream'; import { usePerpsEventTracking } from '../../hooks/usePerpsEventTracking'; @@ -82,6 +83,9 @@ const PerpsClosePositionView: React.FC = () => { const { showToast, PerpsToastOptions } = usePerpsToasts(); + // Get market data for szDecimals + const { marketData } = usePerpsMarketData(position.coin); + // Track screen load performance with unified hook (immediate measurement) usePerpsMeasurement({ traceName: TraceName.PerpsClosePositionView, @@ -492,7 +496,7 @@ const PerpsClosePositionView: React.FC = () => { showWarning={false} onPress={handleAmountPress} isActive={isInputFocused} - tokenAmount={formatPositionSize(closeAmount)} + tokenAmount={formatPositionSize(closeAmount, marketData?.szDecimals)} hasError={filteredErrors.length > 0} tokenSymbol={position.coin} showMaxAmount={false} @@ -501,7 +505,7 @@ const PerpsClosePositionView: React.FC = () => { {/* Toggle Button for USD/Token Display */} - {`${formatPositionSize(closeAmount)} ${position.coin}`} + {`${formatPositionSize(closeAmount, marketData?.szDecimals)} ${position.coin}`} From 89b30c3cf2853a200db1aa77db368248014c9c40 Mon Sep 17 00:00:00 2001 From: Nicholas Smith Date: Fri, 31 Oct 2025 11:16:09 -0500 Subject: [PATCH 2/2] chore(sonarcloud): address sonarcloud code rating --- .../Views/PerpsClosePositionView/PerpsClosePositionView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/UI/Perps/Views/PerpsClosePositionView/PerpsClosePositionView.tsx b/app/components/UI/Perps/Views/PerpsClosePositionView/PerpsClosePositionView.tsx index 247458ca184f..f50e067040ff 100644 --- a/app/components/UI/Perps/Views/PerpsClosePositionView/PerpsClosePositionView.tsx +++ b/app/components/UI/Perps/Views/PerpsClosePositionView/PerpsClosePositionView.tsx @@ -564,8 +564,8 @@ const PerpsClosePositionView: React.FC = () => { {/* Filter the errors and only show minimum $10 error */} {filteredErrors.length > 0 && ( - {filteredErrors.map((error) => ( - + {filteredErrors.map((error, index) => ( +