Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
09f3a33
feat(perps): add skeleton loading states and fix rewards points display
abretonc7s Nov 6, 2025
8a7c999
feat(perps): implement order amount optimization handling and improve…
abretonc7s Nov 6, 2025
68ab55a
Merge remote-tracking branch 'origin/main' into fix/perps/loading-states
abretonc7s Nov 6, 2025
6c310c1
refactor(perps): streamline order amount optimization handling
abretonc7s Nov 6, 2025
708f40f
Merge branch 'main' into fix/perps/loading-states
abretonc7s Nov 6, 2025
7d80170
feat(perps): enhance validation handling during input focus
abretonc7s Nov 6, 2025
4e6df23
Merge remote-tracking branch 'origin/main' into fix/perps/validation-…
abretonc7s Nov 6, 2025
9379ada
wip
abretonc7s Nov 6, 2025
abd8efb
Merge remote-tracking branch 'origin/main' into fix/perps/validation-…
abretonc7s Nov 10, 2025
988b2c5
Update dependencies and remove FinalizationRegistry polyfill
abretonc7s Nov 10, 2025
2465f34
Merge branch 'main' into fix/perps/validation-precision
dylanbutler1 Nov 10, 2025
19e05fc
Refactor order handling to use USD as the primary source of truth
abretonc7s Nov 11, 2025
227d084
Merge remote-tracking branch 'origin/main' into fix/perps/validation-…
abretonc7s Nov 11, 2025
df39339
Refactor slippage configuration in Perps to use basis points
abretonc7s Nov 11, 2025
ff726c6
Enhance slippage handling in Perps close position workflow
abretonc7s Nov 11, 2025
f490ccb
Update Perps order validation and configuration
abretonc7s Nov 11, 2025
ace3ec1
Merge remote-tracking branch 'origin/main' into fix/perps/validation-…
abretonc7s Nov 11, 2025
2a2869e
Enhance Perps order handling and validation
abretonc7s Nov 11, 2025
0462f68
Enhance order size validation in HyperLiquidProvider
abretonc7s Nov 11, 2025
c304c8b
Enhance PerpsClosePositionView tests and hyperLiquidValidation
abretonc7s Nov 11, 2025
2159511
Merge remote-tracking branch 'origin/main' into fix/perps/validation-…
abretonc7s Nov 11, 2025
cb9c2be
fix: hip3 debug page
abretonc7s Nov 12, 2025
ea694ad
Merge remote-tracking branch 'origin/main' into fix/perps/validation-…
abretonc7s Nov 12, 2025
0c2c631
refactor: update handleClosePosition parameters for improved clarity
abretonc7s Nov 12, 2025
a0cdff7
refactor: enhance PerpsOrderView with improved data handling and UI c…
abretonc7s Nov 12, 2025
991a271
cleanup
abretonc7s Nov 12, 2025
8c6795e
Merge branch 'main' into fix/perps/validation-precision
abretonc7s Nov 12, 2025
7e35241
refactor: enhance market data handling in PerpsClosePositionView and …
abretonc7s Nov 12, 2025
a6a067a
fix: unit tests
abretonc7s Nov 12, 2025
5feeb80
Merge remote-tracking branch 'origin/main' into fix/perps/validation-…
abretonc7s Nov 12, 2025
dee15ef
refactor: improve market data handling and add fallback logic in Perp…
abretonc7s Nov 12, 2025
fda1210
Merge branch 'main' into fix/perps/validation-precision
abretonc7s Nov 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions app/components/UI/Perps/Debug/HIP3DebugView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import styleSheet from './HIP3DebugView.styles';
import Engine from '../../../../core/Engine';
import type { HyperLiquidProvider } from '../controllers/providers/HyperLiquidProvider';
import type { MarketInfo } from '../controllers/types';
import { findOptimalAmount } from '../utils/orderCalculations';

interface TestResult {
status: 'idle' | 'loading' | 'success' | 'error';
Expand Down Expand Up @@ -367,36 +366,37 @@ const HIP3DebugView: React.FC = () => {
const szDecimals = marketInfo.szDecimals;

// Calculate position size for $11 USD notional value
// Use findOptimalAmount to handle rounding correctly and ensure we meet $10 minimum
// USD as source of truth - provider will recalculate size with fresh price
const targetUsdAmount = 11;
const optimalAmount = findOptimalAmount({
targetAmount: targetUsdAmount.toString(),
maxAllowedAmount: 1000, // Reasonable max for test orders
minAllowedAmount: 10, // HyperLiquid minimum order size
price: currentPrice,
szDecimals,
});

// Calculate actual position size from optimal amount
const positionSize = parseFloat(optimalAmount) / currentPrice;
// Calculate position size from USD amount
const positionSize = targetUsdAmount / currentPrice;
const multiplier = Math.pow(10, szDecimals);
const roundedPositionSize =
Math.round(positionSize * multiplier) / multiplier;

DevLogger.log('Order calculation:', {
market: selectedMarket,
currentPrice: currentPrice.toFixed(2),
szDecimals,
targetAmount: targetUsdAmount,
optimalAmount,
calculatedPositionSize: positionSize.toFixed(szDecimals),
expectedNotional: (positionSize * currentPrice).toFixed(2),
calculatedPositionSize: roundedPositionSize.toFixed(szDecimals),
expectedNotional: (roundedPositionSize * currentPrice).toFixed(2),
});

// Place order with calculated size
// USD-as-source-of-truth: provide currentPrice and usdAmount for validation
const result = await provider.placeOrder({
coin: selectedMarket,
isBuy: true,
size: positionSize.toFixed(szDecimals),
size: roundedPositionSize.toFixed(szDecimals),
orderType: 'market',
leverage: 5,
// Required by USD-as-source-of-truth validation
currentPrice,
usdAmount: targetUsdAmount.toString(),
priceAtCalculation: currentPrice,
maxSlippageBps: 100, // 1% slippage tolerance
});

if (result.success) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2072,12 +2072,12 @@ describe('PerpsClosePositionView', () => {
// HyperLiquid's marginUsed already includes PnL
// receivedAmount = marginUsed - fees = 1450 - 45 = 1405
// realizedPnl = unrealizedPnl = 150 (from defaultPerpsPositionMock)
expect(handleClosePosition).toHaveBeenCalledWith(
defaultPerpsPositionMock,
'',
'market',
undefined,
{
expect(handleClosePosition).toHaveBeenCalledWith({
position: defaultPerpsPositionMock,
size: '',
orderType: 'market',
limitPrice: undefined,
trackingData: {
totalFee: 45,
marketPrice: 3000,
receivedAmount: 1405,
Expand All @@ -2088,8 +2088,14 @@ describe('PerpsClosePositionView', () => {
estimatedPoints: undefined,
inputMethod: 'default',
},
'3000.00',
);
marketPrice: '3000.00',
// Slippage parameters added in USD-as-source-of-truth refactor
slippage: {
usdAmount: '4500', // closingValueString: absSize * currentPrice * (closePercentage / 100) = 1.5 * 3000 * 1.0
priceAtCalculation: 3000, // effectivePrice: currentPrice for market orders
maxSlippageBps: 100, // maxSlippageBps: 1% slippage tolerance (100 basis points)
},
});
});

it('validates limit order requires price before confirmation', async () => {
Expand Down Expand Up @@ -2154,12 +2160,12 @@ describe('PerpsClosePositionView', () => {
if (orderType === 'limit' && !limitPrice) {
return; // Should not proceed without price
}
await handleClosePosition(
defaultPerpsPositionMock,
'',
await handleClosePosition({
position: defaultPerpsPositionMock,
size: '',
orderType,
orderType === 'limit' ? limitPrice : undefined,
{
limitPrice: orderType === 'limit' ? limitPrice : undefined,
trackingData: {
totalFee: 45,
marketPrice: 3000,
receivedAmount: 1405,
Expand All @@ -2170,7 +2176,13 @@ describe('PerpsClosePositionView', () => {
estimatedPoints: undefined,
inputMethod: 'default',
},
);
marketPrice: '3000.00',
slippage: {
usdAmount: '4500',
priceAtCalculation: 3000,
maxSlippageBps: 100,
},
});
}}
>
<Text>Confirm</Text>
Expand All @@ -2186,12 +2198,12 @@ describe('PerpsClosePositionView', () => {

// Assert - Should call with limit price and specific calculated values
await waitFor(() => {
expect(handleClosePosition).toHaveBeenCalledWith(
defaultPerpsPositionMock,
'',
'limit',
'50000',
{
expect(handleClosePosition).toHaveBeenCalledWith({
position: defaultPerpsPositionMock,
size: '',
orderType: 'limit',
limitPrice: '50000',
trackingData: {
totalFee: 45,
marketPrice: 3000,
receivedAmount: 1405,
Expand All @@ -2202,7 +2214,13 @@ describe('PerpsClosePositionView', () => {
estimatedPoints: undefined,
inputMethod: 'default',
},
);
marketPrice: '3000.00',
slippage: {
usdAmount: '4500',
priceAtCalculation: 3000,
maxSlippageBps: 100,
},
});
});
});
});
Expand Down Expand Up @@ -2742,7 +2760,10 @@ describe('PerpsClosePositionView', () => {
);

// Assert - Component renders without error and uses market data
expect(usePerpsMarketDataMock).toHaveBeenCalledWith('BTC');
expect(usePerpsMarketDataMock).toHaveBeenCalledWith({
asset: 'BTC',
showErrorToast: true,
});
});

it('formats position size with different szDecimals values', () => {
Expand Down Expand Up @@ -2784,14 +2805,17 @@ describe('PerpsClosePositionView', () => {
PerpsClosePositionViewSelectorsIDs.CLOSE_POSITION_CONFIRM_BUTTON,
),
).toBeDefined();
expect(usePerpsMarketDataMock).toHaveBeenCalledWith(coin);
expect(usePerpsMarketDataMock).toHaveBeenCalledWith({
asset: coin,
showErrorToast: true,
});
});
});

it('handles missing market data with undefined szDecimals', () => {
// Arrange - Market data is null
// Arrange - Market data with minimal required fields (szDecimals is now required)
usePerpsMarketDataMock.mockReturnValue({
marketData: null,
marketData: { szDecimals: 4 }, // Provide required szDecimals
isLoading: false,
error: null,
});
Expand All @@ -2812,7 +2836,7 @@ describe('PerpsClosePositionView', () => {
true,
);

// Assert - Component renders and falls back to default formatting
// Assert - Component renders with minimal market data
expect(
queryByTestId(
PerpsClosePositionViewSelectorsIDs.CLOSE_POSITION_CONFIRM_BUTTON,
Expand Down Expand Up @@ -2844,9 +2868,9 @@ describe('PerpsClosePositionView', () => {
});

it('handles market data fetch error gracefully', () => {
// Arrange - Market data fetch failed
// Arrange - Market data fetch failed but provides minimal required data
usePerpsMarketDataMock.mockReturnValue({
marketData: null,
marketData: { szDecimals: 4 }, // Provide required szDecimals even in error case
isLoading: false,
error: 'Failed to fetch market data',
});
Expand All @@ -2858,7 +2882,7 @@ describe('PerpsClosePositionView', () => {
true,
);

// Assert - Component still renders with error state
// Assert - Component still renders with minimal data despite error
expect(
queryByTestId(
PerpsClosePositionViewSelectorsIDs.CLOSE_POSITION_CONFIRM_BUTTON,
Expand Down Expand Up @@ -2900,7 +2924,10 @@ describe('PerpsClosePositionView', () => {
);

// Assert - Fetches market data for specific asset
expect(usePerpsMarketDataMock).toHaveBeenCalledWith(coin);
expect(usePerpsMarketDataMock).toHaveBeenCalledWith({
asset: coin,
showErrorToast: true,
});
});
});

Expand Down Expand Up @@ -2986,7 +3013,10 @@ describe('PerpsClosePositionView', () => {
);

// Assert - Hook called with correct asset symbol
expect(usePerpsMarketDataMock).toHaveBeenCalledWith('ETH');
expect(usePerpsMarketDataMock).toHaveBeenCalledWith({
asset: 'ETH',
showErrorToast: true,
});
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ import { useTheme } from '../../../../../util/theme';
import Keypad from '../../../../Base/Keypad';
import type { InputMethod, OrderType, Position } from '../../controllers/types';
import type { PerpsNavigationParamList } from '../../types/navigation';
import {
DECIMAL_PRECISION_CONFIG,
ORDER_SLIPPAGE_CONFIG,
} from '../../constants/perpsConfig';
import {
useMinimumOrderAmount,
usePerpsClosePosition,
Expand Down Expand Up @@ -90,8 +94,11 @@ const PerpsClosePositionView: React.FC = () => {

const { showToast, PerpsToastOptions } = usePerpsToasts();

// Get market data for szDecimals
const { marketData } = usePerpsMarketData(position.coin);
// Get market data for szDecimals with automatic error toast handling
const { marketData, isLoading: isLoadingMarketData } = usePerpsMarketData({
asset: position.coin,
showErrorToast: true,
});

// Track screen load performance with unified hook (immediate measurement)
usePerpsMeasurement({
Expand Down Expand Up @@ -157,17 +164,37 @@ const PerpsClosePositionView: React.FC = () => {

// Calculate display values directly from closePercentage for immediate updates
const { closeAmount, calculatedUSDString } = useMemo(() => {
// During loading, return '0' as temporary state (not a default - intentional for loading UX)
if (isLoadingMarketData) {
return {
closeAmount: '0',
calculatedUSDString: '0.00',
};
}

// Defensive fallback if market data fails to load - prevents crashes
// Real szDecimals should come from market data (varies by asset)
const szDecimals =
marketData?.szDecimals ?? DECIMAL_PRECISION_CONFIG.FALLBACK_SIZE_DECIMALS;

const { tokenAmount, usdValue } = calculateCloseAmountFromPercentage({
percentage: closePercentage,
positionSize: absSize,
currentPrice: effectivePrice,
szDecimals,
});

return {
closeAmount: tokenAmount.toString(),
calculatedUSDString: formatCloseAmountUSD(usdValue),
};
}, [closePercentage, absSize, effectivePrice]);
}, [
closePercentage,
absSize,
effectivePrice,
marketData?.szDecimals,
isLoadingMarketData,
]);

// Use calculated USD string when not in input mode, user input when typing
const displayUSDString =
Expand Down Expand Up @@ -287,6 +314,7 @@ const PerpsClosePositionView: React.FC = () => {
remainingPositionValue,
receiveAmount,
isPartialClose,
skipValidation: isInputFocused,
});

const { handleClosePosition, isClosing } = usePerpsClosePosition();
Expand Down Expand Up @@ -349,12 +377,12 @@ const PerpsClosePositionView: React.FC = () => {
// Go back immediately to close the position screen
navigation.goBack();

await handleClosePosition(
livePosition,
sizeToClose || '',
await handleClosePosition({
position: livePosition,
size: sizeToClose || '',
orderType,
orderType === 'limit' ? limitPrice : undefined,
{
limitPrice: orderType === 'limit' ? limitPrice : undefined,
trackingData: {
totalFee: feeResults.totalFee,
marketPrice: currentPrice,
receivedAmount: receiveAmount,
Expand All @@ -365,8 +393,14 @@ const PerpsClosePositionView: React.FC = () => {
estimatedPoints: rewardsState.estimatedPoints,
inputMethod: inputMethodRef.current,
},
priceData[position.coin]?.price,
);
marketPrice: priceData[position.coin]?.price,
// Slippage parameters for consistent validation (same as PerpsOrderView)
slippage: {
usdAmount: closingValueString,
priceAtCalculation: effectivePrice,
maxSlippageBps: ORDER_SLIPPAGE_CONFIG.DEFAULT_SLIPPAGE_BPS,
},
});
};

const handleAmountPress = () => {
Expand Down
Loading
Loading