Skip to content

Commit a497254

Browse files
authored
Merge branch 'main' into ogp/22135
2 parents 8ede07f + 358b4c7 commit a497254

File tree

13 files changed

+437
-75
lines changed

13 files changed

+437
-75
lines changed

app/components/UI/Card/Views/SpendingLimit/SpendingLimit.test.tsx

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,6 +1022,116 @@ describe('SpendingLimit Component', () => {
10221022
);
10231023
});
10241024
});
1025+
1026+
it('does not call updateTokenPriority when delegation amount is zero', async () => {
1027+
const mockExternalWalletDetails = {
1028+
walletDetails: [
1029+
{
1030+
id: 1,
1031+
walletAddress: '0xwallet123',
1032+
currency: 'USDC',
1033+
balance: '1000',
1034+
allowance: '1000000',
1035+
priority: 1,
1036+
tokenDetails: {
1037+
address: '0x123',
1038+
symbol: 'USDC',
1039+
name: 'USD Coin',
1040+
decimals: 6,
1041+
},
1042+
caipChainId: 'eip155:59144' as `${string}:${string}`,
1043+
network: 'linea' as const,
1044+
},
1045+
] as unknown as CardExternalWalletDetailsResponse,
1046+
mappedWalletDetails: [mockPriorityToken],
1047+
priorityWalletDetail: mockPriorityToken,
1048+
};
1049+
1050+
const routeWithWalletDetails: MockRoute = {
1051+
params: {
1052+
...mockRoute.params,
1053+
externalWalletDetailsData: mockExternalWalletDetails,
1054+
},
1055+
};
1056+
1057+
render(routeWithWalletDetails);
1058+
1059+
const setLimitButton = screen.getByText('Set a limit');
1060+
fireEvent.press(setLimitButton);
1061+
1062+
const restrictedOption = screen.getByText('Restricted');
1063+
fireEvent.press(restrictedOption);
1064+
1065+
const input = screen.getByPlaceholderText('0');
1066+
fireEvent.changeText(input, '0');
1067+
1068+
const confirmButton = screen.getByText('Confirm');
1069+
fireEvent.press(confirmButton);
1070+
1071+
await waitFor(() => {
1072+
expect(mockSubmitDelegation).toHaveBeenCalled();
1073+
});
1074+
1075+
expect(mockUpdateTokenPriority).not.toHaveBeenCalled();
1076+
expect(mockDispatch).toHaveBeenCalledWith(
1077+
expect.objectContaining({
1078+
type: expect.stringContaining('clearCacheData'),
1079+
payload: 'card-external-wallet-details',
1080+
}),
1081+
);
1082+
});
1083+
1084+
it('does not call updateTokenPriority when delegation amount is 0x0', async () => {
1085+
const mockExternalWalletDetails = {
1086+
walletDetails: [
1087+
{
1088+
id: 1,
1089+
walletAddress: '0xwallet123',
1090+
currency: 'USDC',
1091+
balance: '1000',
1092+
allowance: '1000000',
1093+
priority: 1,
1094+
tokenDetails: {
1095+
address: '0x123',
1096+
symbol: 'USDC',
1097+
name: 'USD Coin',
1098+
decimals: 6,
1099+
},
1100+
caipChainId: 'eip155:59144' as `${string}:${string}`,
1101+
network: 'linea' as const,
1102+
},
1103+
] as unknown as CardExternalWalletDetailsResponse,
1104+
mappedWalletDetails: [mockPriorityToken],
1105+
priorityWalletDetail: mockPriorityToken,
1106+
};
1107+
1108+
const routeWithWalletDetails: MockRoute = {
1109+
params: {
1110+
...mockRoute.params,
1111+
externalWalletDetailsData: mockExternalWalletDetails,
1112+
},
1113+
};
1114+
1115+
render(routeWithWalletDetails);
1116+
1117+
const setLimitButton = screen.getByText('Set a limit');
1118+
fireEvent.press(setLimitButton);
1119+
1120+
const restrictedOption = screen.getByText('Restricted');
1121+
fireEvent.press(restrictedOption);
1122+
1123+
const input = screen.getByPlaceholderText('0');
1124+
fireEvent.changeText(input, '0x0');
1125+
1126+
const confirmButton = screen.getByText('Confirm');
1127+
fireEvent.press(confirmButton);
1128+
1129+
await waitFor(() => {
1130+
expect(mockSubmitDelegation).toHaveBeenCalled();
1131+
});
1132+
1133+
expect(mockUpdateTokenPriority).not.toHaveBeenCalled();
1134+
});
10251135
});
10261136

10271137
describe('Cancel Behavior', () => {

app/components/UI/Card/Views/SpendingLimit/SpendingLimit.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import { useDispatch } from 'react-redux';
5454
import Routes from '../../../../../constants/navigation/Routes';
5555
import { SafeAreaView } from 'react-native-safe-area-context';
5656
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
57+
import { isZeroValue } from '../../../../../util/number';
5758

5859
const getNetworkFromCaipChainId = (caipChainId: string): CardNetwork => {
5960
if (caipChainId === SolScope.Mainnet || caipChainId.startsWith('solana:')) {
@@ -271,10 +272,11 @@ const SpendingLimit = ({
271272
network,
272273
});
273274

274-
// Update token priority if external wallet details are available
275+
// Update token priority if external wallet details are available and delegation is more than 0
275276
if (
276277
externalWalletDetailsData?.walletDetails &&
277-
externalWalletDetailsData.walletDetails.length > 0
278+
externalWalletDetailsData.walletDetails.length > 0 &&
279+
!isZeroValue(parseFloat(delegationAmount))
278280
) {
279281
const tokenWithWallet = tokenToUse || priorityToken;
280282
if (tokenWithWallet) {
@@ -284,7 +286,6 @@ const SpendingLimit = ({
284286
);
285287
}
286288
} else {
287-
// If no external wallet details, just invalidate cache
288289
dispatch(clearCacheData('card-external-wallet-details'));
289290
}
290291

app/components/UI/Card/hooks/useCardDelegation.test.ts

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
useMetrics,
1313
} from '../../../hooks/useMetrics';
1414
import { toTokenMinimalUnit } from '../../../../util/number';
15+
import { safeToChecksumAddress } from '../../../../util/address';
1516
import { ARBITRARY_ALLOWANCE } from '../constants';
1617
import {
1718
TransactionType,
@@ -47,6 +48,10 @@ jest.mock('../../../../util/number', () => ({
4748
toTokenMinimalUnit: jest.fn(),
4849
}));
4950

51+
jest.mock('../../../../util/address', () => ({
52+
safeToChecksumAddress: jest.fn(),
53+
}));
54+
5055
jest.mock('../../../../core/Engine', () => ({
5156
context: {
5257
KeyringController: {
@@ -70,6 +75,9 @@ const mockUseMetrics = useMetrics as jest.MockedFunction<typeof useMetrics>;
7075
const mockToTokenMinimalUnit = toTokenMinimalUnit as jest.MockedFunction<
7176
typeof toTokenMinimalUnit
7277
>;
78+
const mockSafeToChecksumAddress = safeToChecksumAddress as jest.MockedFunction<
79+
typeof safeToChecksumAddress
80+
>;
7381

7482
// Helper functions
7583
const createMockToken = (
@@ -191,6 +199,9 @@ describe('useCardDelegation', () => {
191199

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

195206
// Setup SDK method mocks
196207
mockSDK.generateDelegationToken.mockResolvedValue({
@@ -1086,26 +1097,91 @@ describe('useCardDelegation', () => {
10861097
});
10871098
});
10881099

1089-
it('handles solana network selection', async () => {
1100+
it('uses raw address for solana network without checksum', async () => {
10901101
const mockToken = createMockToken();
1102+
const mockSolanaAddress = 'SolanaAddress123ABC';
10911103
const params = {
10921104
...createMockDelegationParams(),
10931105
network: 'solana' as const,
10941106
};
10951107

10961108
mockUseSelector.mockReturnValue(
10971109
jest.fn().mockReturnValue({
1098-
address: mockAddress,
1110+
address: mockSolanaAddress,
1111+
}),
1112+
);
1113+
1114+
const { result } = renderHook(() => useCardDelegation(mockToken));
1115+
1116+
await act(async () => {
1117+
await result.current.submitDelegation(params);
1118+
});
1119+
1120+
expect(mockSafeToChecksumAddress).not.toHaveBeenCalled();
1121+
expect(mockSDK.generateDelegationToken).toHaveBeenCalledWith(
1122+
'solana',
1123+
mockSolanaAddress,
1124+
);
1125+
});
1126+
1127+
it('uses checksummed address for linea network', async () => {
1128+
const mockToken = createMockToken();
1129+
const mockRawAddress = '0xABCDEF123456';
1130+
const mockChecksummedAddress = '0xabcdef123456' as `0x${string}`;
1131+
const params = createMockDelegationParams();
1132+
1133+
mockUseSelector.mockReturnValue(
1134+
jest.fn().mockReturnValue({
1135+
address: mockRawAddress,
1136+
}),
1137+
);
1138+
1139+
mockSafeToChecksumAddress.mockReturnValue(mockChecksummedAddress);
1140+
1141+
const { result } = renderHook(() => useCardDelegation(mockToken));
1142+
1143+
await act(async () => {
1144+
await result.current.submitDelegation(params);
1145+
});
1146+
1147+
expect(mockSafeToChecksumAddress).toHaveBeenCalledWith(mockRawAddress);
1148+
expect(mockSDK.generateDelegationToken).toHaveBeenCalledWith(
1149+
'linea',
1150+
mockChecksummedAddress,
1151+
);
1152+
});
1153+
1154+
it('uses checksummed address for non-solana networks', async () => {
1155+
const mockToken = createMockToken();
1156+
const mockRawAddress = '0x1234567890ABCDEF';
1157+
const mockChecksummedAddress = '0x1234567890abcdef' as `0x${string}`;
1158+
const params = {
1159+
...createMockDelegationParams(),
1160+
network: 'linea' as const,
1161+
};
1162+
1163+
mockUseSelector.mockReturnValue(
1164+
jest.fn().mockReturnValue({
1165+
address: mockRawAddress,
10991166
}),
11001167
);
11011168

1169+
mockSafeToChecksumAddress.mockReturnValue(mockChecksummedAddress);
1170+
11021171
const { result } = renderHook(() => useCardDelegation(mockToken));
11031172

11041173
await act(async () => {
11051174
await result.current.submitDelegation(params);
11061175
});
11071176

1108-
expect(mockUseSelector).toHaveBeenCalled();
1177+
expect(mockSafeToChecksumAddress).toHaveBeenCalledWith(mockRawAddress);
1178+
expect(
1179+
Engine.context.KeyringController.signPersonalMessage,
1180+
).toHaveBeenCalledWith(
1181+
expect.objectContaining({
1182+
from: mockChecksummedAddress,
1183+
}),
1184+
);
11091185
});
11101186

11111187
it('handles very large allowance amounts', async () => {

app/components/UI/Card/hooks/useCardDelegation.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { MetaMetricsEvents, useMetrics } from '../../../hooks/useMetrics';
1818
import { ARBITRARY_ALLOWANCE } from '../constants';
1919
import { toTokenMinimalUnit } from '../../../../util/number';
2020
import AppConstants from '../../../../core/AppConstants';
21+
import { safeToChecksumAddress } from '../../../../util/address';
2122

2223
/**
2324
* Custom error class for user-initiated cancellations
@@ -238,7 +239,10 @@ export const useCardDelegation = (token?: CardTokenAllowance | null) => {
238239
const userAccount = selectAccountByScope(
239240
params.network === 'solana' ? SolScope.Mainnet : 'eip155:0',
240241
);
241-
const address = userAccount?.address;
242+
const address =
243+
params.network === 'solana'
244+
? userAccount?.address
245+
: safeToChecksumAddress(userAccount?.address);
242246

243247
if (!address) {
244248
throw new Error('No account found');

0 commit comments

Comments
 (0)