Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions app/components/UI/Perps/Perps.testIds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ export const PerpsWithdrawViewSelectorsIDs = {
RECEIVE_VALUE: 'perps-withdraw-receive-value',
FEE_VALUE: 'perps-withdraw-fee-value',
TIME_VALUE: 'perps-withdraw-time-value',
AVAILABLE_BALANCE_TEXT: 'perps-withdraw-available-balance-text',
};

// ========================================
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import Engine from '../../../../../core/Engine';
import { useTransactionPayToken } from '../../../../Views/confirmations/hooks/pay/useTransactionPayToken';
import { usePerpsPayWithToken } from '../../hooks/useIsPerpsBalanceSelected';
import {
arePaymentTokensEqual,
useDefaultPayWithTokenWhenNoPerpsBalance,
useHasNativeTradeablePerpsBalance,
usePreferredFallbackPayTokenCandidate,
Expand All @@ -15,7 +14,14 @@ jest.mock('../../../../Views/confirmations/hooks/pay/useTransactionPayToken');
jest.mock('../../hooks/useIsPerpsBalanceSelected', () => ({
usePerpsPayWithToken: jest.fn(),
}));
jest.mock('../../hooks/useDefaultPayWithTokenWhenNoPerpsBalance');
jest.mock('../../hooks/useDefaultPayWithTokenWhenNoPerpsBalance', () => ({
useDefaultPayWithTokenWhenNoPerpsBalance: jest.fn(),
useHasNativeTradeablePerpsBalance: jest.fn(),
usePreferredFallbackPayTokenCandidate: jest.fn(),
arePaymentTokensEqual: jest.fn(
(a, b) => !!a && !!b && a.address === b.address && a.chainId === b.chainId,
),
}));
jest.mock('../../hooks/usePerpsSelector');
jest.mock('../../../../../core/Engine', () => ({
context: {
Expand Down Expand Up @@ -43,9 +49,6 @@ const mockUsePreferredFallbackPayTokenCandidate =
usePreferredFallbackPayTokenCandidate as jest.MockedFunction<
typeof usePreferredFallbackPayTokenCandidate
>;
const mockArePaymentTokensEqual = arePaymentTokensEqual as jest.MockedFunction<
typeof arePaymentTokensEqual
>;
const mockUsePerpsSelector = usePerpsSelector as jest.MockedFunction<
typeof usePerpsSelector
>;
Expand All @@ -57,8 +60,6 @@ function setupDefaults({
selectedPaymentToken = null,
defaultPayToken = null,
pendingConfig = undefined,
hasNativeTradeablePerpsBalance = false,
fallbackPayTokenCandidate = null,
}: {
payToken?: ReturnType<typeof useTransactionPayToken>['payToken'];
selectedPaymentToken?: ReturnType<typeof usePerpsPayWithToken>;
Expand All @@ -70,32 +71,18 @@ function setupDefaults({
chainId: string;
description?: string;
};
selectedPaymentTokenSource?: 'explicit' | 'autoNoPerpsBalance';
}
| undefined;
hasNativeTradeablePerpsBalance?: boolean;
fallbackPayTokenCandidate?: ReturnType<
typeof usePreferredFallbackPayTokenCandidate
>;
} = {}) {
mockUseTransactionPayToken.mockReturnValue({
payToken,
setPayToken: mockSetPayToken,
} as unknown as ReturnType<typeof useTransactionPayToken>);
mockUsePerpsPayWithToken.mockReturnValue(selectedPaymentToken);
mockUseDefaultPayToken.mockReturnValue(defaultPayToken);
mockUseHasNativeTradeablePerpsBalance.mockReturnValue(
hasNativeTradeablePerpsBalance,
);
mockUsePreferredFallbackPayTokenCandidate.mockReturnValue(
fallbackPayTokenCandidate,
);
mockArePaymentTokensEqual.mockImplementation(
(tokenA, tokenB) =>
tokenA?.address === tokenB?.address &&
tokenA?.chainId === tokenB?.chainId,
);
mockUsePerpsSelector.mockReturnValue(pendingConfig);
mockUseHasNativeTradeablePerpsBalance.mockReturnValue(false);
mockUsePreferredFallbackPayTokenCandidate.mockReturnValue(null);
}

describe('useInitPerpsPaymentToken', () => {
Expand Down Expand Up @@ -186,74 +173,6 @@ describe('useInitPerpsPaymentToken', () => {
});
});

it('does not restore auto fallback pending token when native tradeable balance exists', () => {
const pendingToken = {
address: '0xusdc',
chainId: '0xa4b1',
description: 'USDC',
};
setupDefaults({
hasNativeTradeablePerpsBalance: true,
fallbackPayTokenCandidate: pendingToken,
pendingConfig: {
selectedPaymentToken: pendingToken,
selectedPaymentTokenSource: 'autoNoPerpsBalance',
},
});

renderHook(() => useInitPerpsPaymentToken('BTC'));

expect(mockSetPayToken).not.toHaveBeenCalled();
expect(mockSetSelectedPaymentToken).toHaveBeenCalledWith(null);
});

it('does not restore legacy auto fallback token when native tradeable balance exists', () => {
const pendingToken = {
address: '0xusdc',
chainId: '0xa4b1',
description: 'USDC',
};
setupDefaults({
hasNativeTradeablePerpsBalance: true,
fallbackPayTokenCandidate: pendingToken,
pendingConfig: {
selectedPaymentToken: pendingToken,
},
});

renderHook(() => useInitPerpsPaymentToken('BTC'));

expect(mockSetPayToken).not.toHaveBeenCalled();
expect(mockSetSelectedPaymentToken).toHaveBeenCalledWith(null);
});

it('restores explicit pending token when native tradeable balance exists', () => {
const pendingToken = {
address: '0xdai',
chainId: '0x1',
description: 'DAI',
};
setupDefaults({
hasNativeTradeablePerpsBalance: true,
pendingConfig: {
selectedPaymentToken: pendingToken,
selectedPaymentTokenSource: 'explicit',
},
});

renderHook(() => useInitPerpsPaymentToken('BTC'));

expect(mockSetPayToken).toHaveBeenCalledWith({
address: '0xdai',
chainId: '0x1',
});
expect(mockSetSelectedPaymentToken).toHaveBeenCalledWith({
description: 'DAI',
address: '0xdai',
chainId: '0x1',
});
});

it('does not reapply pending config when already matching', () => {
const pendingToken = {
address: '0xdai',
Expand Down Expand Up @@ -327,4 +246,54 @@ describe('useInitPerpsPaymentToken', () => {

expect(mockSetPayToken).not.toHaveBeenCalled();
});

it('clears saved pay token on mount when it was an auto-fallback and the user now has native perps balance', () => {
const fallbackToken = {
address: '0xusdc',
chainId: '0xa4b1',
description: 'USDC',
};
setupDefaults({
pendingConfig: { selectedPaymentToken: fallbackToken },
});
mockUseHasNativeTradeablePerpsBalance.mockReturnValue(true);
mockUsePreferredFallbackPayTokenCandidate.mockReturnValue(fallbackToken);

renderHook(() => useInitPerpsPaymentToken('BTC'));

expect(mockSetPayToken).not.toHaveBeenCalledWith({
address: '0xusdc',
chainId: '0xa4b1',
});
expect(mockSetSelectedPaymentToken).toHaveBeenCalledWith(null);
});

it('preserves saved pay token on mount when it differs from the auto-fallback candidate', () => {
const explicitToken = {
address: '0xdai',
chainId: '0x1',
description: 'DAI',
};
setupDefaults({
pendingConfig: { selectedPaymentToken: explicitToken },
});
mockUseHasNativeTradeablePerpsBalance.mockReturnValue(true);
mockUsePreferredFallbackPayTokenCandidate.mockReturnValue({
address: '0xusdc',
chainId: '0xa4b1',
description: 'USDC',
});

renderHook(() => useInitPerpsPaymentToken('BTC'));

expect(mockSetPayToken).toHaveBeenCalledWith({
address: '0xdai',
chainId: '0x1',
});
expect(mockSetSelectedPaymentToken).toHaveBeenCalledWith({
description: 'DAI',
address: '0xdai',
chainId: '0x1',
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,15 @@ import {
export function useInitPerpsPaymentToken(initialAsset: string) {
const { payToken, setPayToken } = useTransactionPayToken();
const selectedPaymentToken = usePerpsPayWithToken();
const hasNativeTradeablePerpsBalance = useHasNativeTradeablePerpsBalance();
const fallbackPayTokenCandidate = usePreferredFallbackPayTokenCandidate();
const defaultPayTokenWhenNoPerpsBalance =
useDefaultPayWithTokenWhenNoPerpsBalance();
const fallbackPayTokenCandidate = usePreferredFallbackPayTokenCandidate();
const hasNativeTradeablePerpsBalance = useHasNativeTradeablePerpsBalance();

const pendingConfig = usePerpsSelector((state) =>
selectPendingTradeConfiguration(state, initialAsset),
);
const pendingConfigSelectedPaymentToken = pendingConfig?.selectedPaymentToken;
const pendingConfigSelectedPaymentTokenSource =
pendingConfig?.selectedPaymentTokenSource;

const appliedPendingTokenRef = useRef<
{ address: string; chainId: string } | null | undefined
Expand Down Expand Up @@ -83,22 +81,6 @@ export function useInitPerpsPaymentToken(initialAsset: string) {

const pendingAddr = pendingConfigSelectedPaymentToken.address;
const pendingChainId = pendingConfigSelectedPaymentToken.chainId;
// Compatibility shim for pending configs persisted before
// selectedPaymentTokenSource existed. Safe to remove after the 5-minute
// pending-config TTL has cycled past release.
const isLegacyAutoFallbackToken =
pendingConfigSelectedPaymentTokenSource == null &&
arePaymentTokensEqual(
pendingConfigSelectedPaymentToken,
fallbackPayTokenCandidate,
);
const isAutoFallbackToken =
pendingConfigSelectedPaymentTokenSource === 'autoNoPerpsBalance' ||
isLegacyAutoFallbackToken;
const shouldRestorePendingToken =
pendingConfigSelectedPaymentTokenSource === 'explicit' ||
!isAutoFallbackToken ||
!hasNativeTradeablePerpsBalance;
const alreadyApplied =
appliedPendingTokenRef.current !== undefined &&
(appliedPendingTokenRef.current === null
Expand All @@ -107,7 +89,16 @@ export function useInitPerpsPaymentToken(initialAsset: string) {
appliedPendingTokenRef.current.chainId === pendingChainId);
if (alreadyApplied) return;

if (!shouldRestorePendingToken) {
// Saved token was previously auto-selected (pay-with-any-token fallback)
// but the user now has native perps buying power. Clear the stale selection
// so the form defaults to Perps balance.
const isStaleAutoFallback =
hasNativeTradeablePerpsBalance &&
arePaymentTokensEqual(
pendingConfigSelectedPaymentToken,
fallbackPayTokenCandidate,
);
if (isStaleAutoFallback) {
appliedPendingTokenRef.current = {
address: pendingAddr,
chainId: pendingChainId,
Expand Down Expand Up @@ -144,7 +135,6 @@ export function useInitPerpsPaymentToken(initialAsset: string) {
initialAsset,
payToken,
pendingConfigSelectedPaymentToken,
pendingConfigSelectedPaymentTokenSource,
setPayToken,
selectedPaymentToken,
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,11 @@ const PerpsWithdrawView: React.FC = () => {
]}
/>
</Box>
<Text variant={TextVariant.BodyMD} color={TextColor.Alternative}>
<Text
variant={TextVariant.BodyMD}
color={TextColor.Alternative}
testID={PerpsWithdrawViewSelectorsIDs.AVAILABLE_BALANCE_TEXT}
>
{strings('perps.withdrawal.available_balance', {
amount: formattedBalance,
})}
Expand Down
Loading
Loading