Skip to content

Commit 1e36186

Browse files
committed
fix: refactor lowercase sensitivity logic, simpflied fetching positions on homepage
1 parent 9e8a447 commit 1e36186

8 files changed

Lines changed: 182 additions & 276 deletions

File tree

app/components/UI/Predict/controllers/PredictController.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2434,6 +2434,41 @@ describe('PredictController', () => {
24342434
},
24352435
);
24362436
});
2437+
2438+
it('succeeds when state key is checksummed but signer address is lowercase', async () => {
2439+
// Arrange - simulates the real-world case mismatch: selector returns
2440+
// checksummed EIP-55 address, but state was keyed by a differently-cased address
2441+
const checksummedAddress =
2442+
'0x1234567890123456789012345678901234567890'.toUpperCase() as `0x${string}`;
2443+
const mockBatchId = 'claim-batch-case';
2444+
await withController(async ({ controller }) => {
2445+
mockPolymarketProvider.getPositions = jest
2446+
.fn()
2447+
.mockResolvedValue([
2448+
createMockPosition({ id: 'pos-1', claimable: true }),
2449+
]);
2450+
mockPolymarketProvider.prepareClaim = jest
2451+
.fn()
2452+
.mockResolvedValue(mockClaim);
2453+
(addTransactionBatch as jest.Mock).mockResolvedValue({
2454+
batchId: mockBatchId,
2455+
});
2456+
2457+
// Seed state with the checksummed key
2458+
controller.updateStateForTesting((state) => {
2459+
state.claimablePositions[checksummedAddress] = [
2460+
createMockPosition({ id: 'pos-1', claimable: true }),
2461+
];
2462+
});
2463+
2464+
// Act - signer address comes back as lowercase (default mock)
2465+
const result = await controller.claimWithConfirmation({});
2466+
2467+
// Assert - should find the positions despite the case difference
2468+
expect(result.batchId).toBe(mockBatchId);
2469+
expect(result.status).toBe(PredictClaimStatus.PENDING);
2470+
});
2471+
});
24372472
});
24382473

24392474
describe('getUnrealizedPnL', () => {
@@ -4969,6 +5004,38 @@ describe('PredictController', () => {
49695004
expect(controller.state.claimablePositions[testAddress]).toEqual([]);
49705005
});
49715006
});
5007+
5008+
it('matches state key case-insensitively when address casing differs', async () => {
5009+
// Arrange - state keyed by checksummed address, but confirmClaim called with lowercase
5010+
await withController(async ({ controller }) => {
5011+
const checksummedKey = '0xABCDEF1234567890ABCDEF1234567890ABCDEF12';
5012+
const lowercaseArg = checksummedKey.toLowerCase() as `0x${string}`;
5013+
const mockPositions = [
5014+
createMockPosition({
5015+
id: 'position-1',
5016+
status: PredictPositionStatus.WON,
5017+
}),
5018+
];
5019+
5020+
controller.updateStateForTesting((state) => {
5021+
state.claimablePositions[checksummedKey] = mockPositions;
5022+
});
5023+
5024+
mockPolymarketProvider.confirmClaim = jest.fn();
5025+
5026+
// Act
5027+
controller.confirmClaim({ address: lowercaseArg });
5028+
5029+
// Assert - found the positions despite case mismatch; state cleared.
5030+
// getSigner is called with matchedKey (the checksummed state key),
5031+
// so the signer address reflects that casing.
5032+
expect(mockPolymarketProvider.confirmClaim).toHaveBeenCalledWith({
5033+
positions: mockPositions,
5034+
signer: expect.objectContaining({ address: checksummedKey }),
5035+
});
5036+
expect(controller.state.claimablePositions[checksummedKey]).toEqual([]);
5037+
});
5038+
});
49725039
});
49735040

49745041
describe('getPositions', () => {

app/components/UI/Predict/controllers/PredictController.ts

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1566,6 +1566,17 @@ export class PredictController extends BaseController<
15661566
}
15671567
}
15681568

1569+
private getClaimablePositionsByAddress(
1570+
address: string,
1571+
): { positions: PredictPosition[]; matchedKey: string } | undefined {
1572+
const normalizedAddress = address.toLowerCase();
1573+
const matchedKey = Object.keys(this.state.claimablePositions).find(
1574+
(key) => key.toLowerCase() === normalizedAddress,
1575+
);
1576+
if (!matchedKey) return undefined;
1577+
return { positions: this.state.claimablePositions[matchedKey], matchedKey };
1578+
}
1579+
15691580
async claimWithConfirmation(
15701581
_params: ClaimParams = {},
15711582
): Promise<PredictClaim> {
@@ -1596,13 +1607,9 @@ export class PredictController extends BaseController<
15961607
const signer = this.getSigner();
15971608

15981609
// Get claimable positions from state (case-insensitive address match)
1599-
const normalizedSignerAddress = signer.address.toLowerCase();
1600-
const matchedAddress = Object.keys(this.state.claimablePositions).find(
1601-
(addressKey) => addressKey.toLowerCase() === normalizedSignerAddress,
1602-
);
1603-
const claimablePositions = matchedAddress
1604-
? this.state.claimablePositions[matchedAddress]
1605-
: undefined;
1610+
const claimablePositions = this.getClaimablePositionsByAddress(
1611+
signer.address,
1612+
)?.positions;
16061613

16071614
if (!claimablePositions || claimablePositions.length === 0) {
16081615
throw new Error('No claimable positions found');
@@ -1729,30 +1736,23 @@ export class PredictController extends BaseController<
17291736
public confirmClaim({ address }: { address?: string }): void {
17301737
const provider = this.provider;
17311738

1732-
const normalizedAddress = (
1733-
address ?? this.getSigner().address
1734-
).toLowerCase();
1735-
const matchedAddress = Object.keys(this.state.claimablePositions).find(
1736-
(addressKey) => addressKey.toLowerCase() === normalizedAddress,
1737-
);
1739+
const resolvedAddress = address ?? this.getSigner().address;
1740+
const claimResult = this.getClaimablePositionsByAddress(resolvedAddress);
17381741

1739-
if (!matchedAddress) {
1742+
if (!claimResult || claimResult.positions.length === 0) {
17401743
return;
17411744
}
17421745

1743-
const signer = this.getSigner(matchedAddress);
1744-
const claimedPositions = this.state.claimablePositions[matchedAddress];
1745-
if (!claimedPositions || claimedPositions.length === 0) {
1746-
return;
1747-
}
1746+
const { positions: claimedPositions, matchedKey } = claimResult;
1747+
const signer = this.getSigner(matchedKey);
17481748

17491749
provider.confirmClaim?.({
17501750
positions: claimedPositions,
17511751
signer,
17521752
});
17531753

17541754
this.update((state) => {
1755-
state.claimablePositions[matchedAddress] = [];
1755+
state.claimablePositions[matchedKey] = [];
17561756
});
17571757
}
17581758

app/components/UI/Predict/hooks/usePredictClaim.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { ensureError } from '../utils/predictErrorHandler';
1212
import { usePredictTrading } from './usePredictTrading';
1313
import { ConfirmationLoader } from '../../../Views/confirmations/components/confirm/confirm-component';
1414
import Routes from '../../../../constants/navigation/Routes';
15+
import { PredictClaimStatus } from '../types';
1516

1617
export const usePredictClaim = () => {
1718
const { navigateToConfirmation } = useConfirmNavigation();
@@ -20,15 +21,16 @@ export const usePredictClaim = () => {
2021
const { toastRef } = useContext(ToastContext);
2122
const navigation = useNavigation();
2223

23-
const claim = useCallback(async () => {
24+
const claim = useCallback(async (): Promise<{ wasCancelled: boolean }> => {
2425
try {
2526
navigateToConfirmation({
2627
headerShown: false,
2728
loader: ConfirmationLoader.PredictClaim,
2829
// TODO: remove once navigation stack is fixed properly
2930
stack: Routes.PREDICT.ROOT,
3031
});
31-
await claimWinnings({});
32+
const result = await claimWinnings({});
33+
return { wasCancelled: result?.status === PredictClaimStatus.CANCELLED };
3234
} catch (err) {
3335
// Log error with claim context
3436
Logger.error(ensureError(err), {
@@ -70,6 +72,7 @@ export const usePredictClaim = () => {
7072
},
7173
},
7274
});
75+
return { wasCancelled: false };
7376
}
7477
}, [
7578
claimWinnings,

app/components/UI/Predict/hooks/usePredictToastRegistrations.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { selectSelectedAccountGroupId } from '../../../../selectors/multichainAc
2424
import type { Hex } from '@metamask/utils';
2525
import { usePredictClaim } from './usePredictClaim';
2626
import { usePredictDeposit } from './usePredictDeposit';
27+
import { _clearPositionsCache } from '../../../Views/Homepage/Sections/Predictions/hooks/usePredictPositionsForHomepage';
2728
import { usePredictWithdraw } from './usePredictWithdraw';
2829
import { store } from '../../../../store';
2930
import { selectTransactionMetadataById } from '../../../../selectors/transactionController';
@@ -254,6 +255,9 @@ export const usePredictToastRegistrations = (): ToastRegistration[] => {
254255
queryKey: predictQueries.positions.keys.all(),
255256
});
256257

258+
// Clear homepage positions cache so next visit fetches fresh data
259+
_clearPositionsCache();
260+
257261
showSuccessToast({
258262
showToast,
259263
title: strings('predict.deposit.account_ready'),

0 commit comments

Comments
 (0)