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
110 changes: 110 additions & 0 deletions app/components/UI/Card/Views/SpendingLimit/SpendingLimit.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1022,6 +1022,116 @@ describe('SpendingLimit Component', () => {
);
});
});

it('does not call updateTokenPriority when delegation amount is zero', async () => {
const mockExternalWalletDetails = {
walletDetails: [
{
id: 1,
walletAddress: '0xwallet123',
currency: 'USDC',
balance: '1000',
allowance: '1000000',
priority: 1,
tokenDetails: {
address: '0x123',
symbol: 'USDC',
name: 'USD Coin',
decimals: 6,
},
caipChainId: 'eip155:59144' as `${string}:${string}`,
network: 'linea' as const,
},
] as unknown as CardExternalWalletDetailsResponse,
mappedWalletDetails: [mockPriorityToken],
priorityWalletDetail: mockPriorityToken,
};

const routeWithWalletDetails: MockRoute = {
params: {
...mockRoute.params,
externalWalletDetailsData: mockExternalWalletDetails,
},
};

render(routeWithWalletDetails);

const setLimitButton = screen.getByText('Set a limit');
fireEvent.press(setLimitButton);

const restrictedOption = screen.getByText('Restricted');
fireEvent.press(restrictedOption);

const input = screen.getByPlaceholderText('0');
fireEvent.changeText(input, '0');

const confirmButton = screen.getByText('Confirm');
fireEvent.press(confirmButton);

await waitFor(() => {
expect(mockSubmitDelegation).toHaveBeenCalled();
});

expect(mockUpdateTokenPriority).not.toHaveBeenCalled();
expect(mockDispatch).toHaveBeenCalledWith(
expect.objectContaining({
type: expect.stringContaining('clearCacheData'),
payload: 'card-external-wallet-details',
}),
);
});

it('does not call updateTokenPriority when delegation amount is 0x0', async () => {
const mockExternalWalletDetails = {
walletDetails: [
{
id: 1,
walletAddress: '0xwallet123',
currency: 'USDC',
balance: '1000',
allowance: '1000000',
priority: 1,
tokenDetails: {
address: '0x123',
symbol: 'USDC',
name: 'USD Coin',
decimals: 6,
},
caipChainId: 'eip155:59144' as `${string}:${string}`,
network: 'linea' as const,
},
] as unknown as CardExternalWalletDetailsResponse,
mappedWalletDetails: [mockPriorityToken],
priorityWalletDetail: mockPriorityToken,
};

const routeWithWalletDetails: MockRoute = {
params: {
...mockRoute.params,
externalWalletDetailsData: mockExternalWalletDetails,
},
};

render(routeWithWalletDetails);

const setLimitButton = screen.getByText('Set a limit');
fireEvent.press(setLimitButton);

const restrictedOption = screen.getByText('Restricted');
fireEvent.press(restrictedOption);

const input = screen.getByPlaceholderText('0');
fireEvent.changeText(input, '0x0');

const confirmButton = screen.getByText('Confirm');
fireEvent.press(confirmButton);

await waitFor(() => {
expect(mockSubmitDelegation).toHaveBeenCalled();
});

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

describe('Cancel Behavior', () => {
Expand Down
7 changes: 4 additions & 3 deletions app/components/UI/Card/Views/SpendingLimit/SpendingLimit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import { useDispatch } from 'react-redux';
import Routes from '../../../../../constants/navigation/Routes';
import { SafeAreaView } from 'react-native-safe-area-context';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
import { isZeroValue } from '../../../../../util/number';

const getNetworkFromCaipChainId = (caipChainId: string): CardNetwork => {
if (caipChainId === SolScope.Mainnet || caipChainId.startsWith('solana:')) {
Expand Down Expand Up @@ -271,10 +272,11 @@ const SpendingLimit = ({
network,
});

// Update token priority if external wallet details are available
// Update token priority if external wallet details are available and delegation is more than 0
if (
externalWalletDetailsData?.walletDetails &&
externalWalletDetailsData.walletDetails.length > 0
externalWalletDetailsData.walletDetails.length > 0 &&
!isZeroValue(parseFloat(delegationAmount))
) {
const tokenWithWallet = tokenToUse || priorityToken;
if (tokenWithWallet) {
Expand All @@ -284,7 +286,6 @@ const SpendingLimit = ({
);
}
} else {
// If no external wallet details, just invalidate cache
dispatch(clearCacheData('card-external-wallet-details'));
}

Expand Down
82 changes: 79 additions & 3 deletions app/components/UI/Card/hooks/useCardDelegation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
useMetrics,
} from '../../../hooks/useMetrics';
import { toTokenMinimalUnit } from '../../../../util/number';
import { safeToChecksumAddress } from '../../../../util/address';
import { ARBITRARY_ALLOWANCE } from '../constants';
import {
TransactionType,
Expand Down Expand Up @@ -47,6 +48,10 @@ jest.mock('../../../../util/number', () => ({
toTokenMinimalUnit: jest.fn(),
}));

jest.mock('../../../../util/address', () => ({
safeToChecksumAddress: jest.fn(),
}));

jest.mock('../../../../core/Engine', () => ({
context: {
KeyringController: {
Expand All @@ -70,6 +75,9 @@ const mockUseMetrics = useMetrics as jest.MockedFunction<typeof useMetrics>;
const mockToTokenMinimalUnit = toTokenMinimalUnit as jest.MockedFunction<
typeof toTokenMinimalUnit
>;
const mockSafeToChecksumAddress = safeToChecksumAddress as jest.MockedFunction<
typeof safeToChecksumAddress
>;

// Helper functions
const createMockToken = (
Expand Down Expand Up @@ -191,6 +199,9 @@ describe('useCardDelegation', () => {

// Setup utility mocks
mockToTokenMinimalUnit.mockReturnValue('100000000000000000000');
mockSafeToChecksumAddress.mockImplementation(
(address?: string) => (address as `0x${string}`) || undefined,
);

// Setup SDK method mocks
mockSDK.generateDelegationToken.mockResolvedValue({
Expand Down Expand Up @@ -1086,26 +1097,91 @@ describe('useCardDelegation', () => {
});
});

it('handles solana network selection', async () => {
it('uses raw address for solana network without checksum', async () => {
const mockToken = createMockToken();
const mockSolanaAddress = 'SolanaAddress123ABC';
const params = {
...createMockDelegationParams(),
network: 'solana' as const,
};

mockUseSelector.mockReturnValue(
jest.fn().mockReturnValue({
address: mockAddress,
address: mockSolanaAddress,
}),
);

const { result } = renderHook(() => useCardDelegation(mockToken));

await act(async () => {
await result.current.submitDelegation(params);
});

expect(mockSafeToChecksumAddress).not.toHaveBeenCalled();
expect(mockSDK.generateDelegationToken).toHaveBeenCalledWith(
'solana',
mockSolanaAddress,
);
});

it('uses checksummed address for linea network', async () => {
const mockToken = createMockToken();
const mockRawAddress = '0xABCDEF123456';
const mockChecksummedAddress = '0xabcdef123456' as `0x${string}`;
const params = createMockDelegationParams();

mockUseSelector.mockReturnValue(
jest.fn().mockReturnValue({
address: mockRawAddress,
}),
);

mockSafeToChecksumAddress.mockReturnValue(mockChecksummedAddress);

const { result } = renderHook(() => useCardDelegation(mockToken));

await act(async () => {
await result.current.submitDelegation(params);
});

expect(mockSafeToChecksumAddress).toHaveBeenCalledWith(mockRawAddress);
expect(mockSDK.generateDelegationToken).toHaveBeenCalledWith(
'linea',
mockChecksummedAddress,
);
});

it('uses checksummed address for non-solana networks', async () => {
const mockToken = createMockToken();
const mockRawAddress = '0x1234567890ABCDEF';
const mockChecksummedAddress = '0x1234567890abcdef' as `0x${string}`;
const params = {
...createMockDelegationParams(),
network: 'linea' as const,
};

mockUseSelector.mockReturnValue(
jest.fn().mockReturnValue({
address: mockRawAddress,
}),
);

mockSafeToChecksumAddress.mockReturnValue(mockChecksummedAddress);

const { result } = renderHook(() => useCardDelegation(mockToken));

await act(async () => {
await result.current.submitDelegation(params);
});

expect(mockUseSelector).toHaveBeenCalled();
expect(mockSafeToChecksumAddress).toHaveBeenCalledWith(mockRawAddress);
expect(
Engine.context.KeyringController.signPersonalMessage,
).toHaveBeenCalledWith(
expect.objectContaining({
from: mockChecksummedAddress,
}),
);
});

it('handles very large allowance amounts', async () => {
Expand Down
6 changes: 5 additions & 1 deletion app/components/UI/Card/hooks/useCardDelegation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { MetaMetricsEvents, useMetrics } from '../../../hooks/useMetrics';
import { ARBITRARY_ALLOWANCE } from '../constants';
import { toTokenMinimalUnit } from '../../../../util/number';
import AppConstants from '../../../../core/AppConstants';
import { safeToChecksumAddress } from '../../../../util/address';

/**
* Custom error class for user-initiated cancellations
Expand Down Expand Up @@ -238,7 +239,10 @@ export const useCardDelegation = (token?: CardTokenAllowance | null) => {
const userAccount = selectAccountByScope(
params.network === 'solana' ? SolScope.Mainnet : 'eip155:0',
);
const address = userAccount?.address;
const address =
params.network === 'solana'
? userAccount?.address
: safeToChecksumAddress(userAccount?.address);

if (!address) {
throw new Error('No account found');
Expand Down
Loading
Loading