Skip to content

Commit 0862685

Browse files
authored
fix(predict): cp-7.61.0 improve bet amount validation with fee-adjusted calculations (MetaMask#23543)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** Add fee percentage tracking and fix max bet amount calculations to properly account for fees in the prediction betting system. Changes: - Add totalFeePercentage field to PredictFees interface - Implement calculateMaxBetAmount utility to account for fees - Update calculateFees to return totalFeePercentage (0 or FEE_PERCENTAGE) - Fix minimum bet validation to include fees (minimumBetWithFees) - Improve error messaging for insufficient funds vs below minimum - Add conditional error message when balance is below minimum bet - Export MINIMUM_BET constant for reusability Test coverage: - Add comprehensive tests for calculateMaxBetAmount utility - Update all existing tests to include totalFeePercentage in mock fees - Add assertions for totalFeePercentage in fee calculation tests This ensures users can accurately see their maximum bet amount after fees are deducted, preventing order failures due to insufficient funds when fees are applied. <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: null ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/PRED-342?atlOrigin=eyJpIjoiMzU5NTJmY2I2NjM5NGQ5ZGIyYmFhMzJjM2VhNTY1ZWQiLCJwIjoiaiJ9 ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** https://www.loom.com/share/05f87ae8888c4e739819a2b5bdc6c4a5 <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Track fee percentage in previews, compute fee-adjusted min/max bet amounts, update UI/error messages, and expand tests accordingly. > > - **Predict UI**: > - Add `MINIMUM_BET` constant and compute `minimumBetWithFees` using `fees.totalFeePercentage`. > - Use new `calculateMaxBetAmount` to determine available amount; refine insufficient-funds vs below-minimum messaging. > - Update `PredictBuyPreview` logic and tests for fee-adjusted validation and display. > - **Utils**: > - New `orders.ts`: `generateOrderId`, `calculateMaxBetAmount` (+ comprehensive tests). > - `polymarket/utils.ts`: `calculateFees` now returns `totalFeePercentage`; waive fees sets it to `0`. > - **Types & Providers**: > - Extend `PredictFees` with `totalFeePercentage` and propagate through previews/tests. > - **Localization**: > - Add `predict.order.no_funds_enough` message. > - **Tests**: > - Update/expand unit tests across hooks, provider, utils, and view to include fee percentage and new validation paths. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 92a617e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 3c902cc commit 0862685

10 files changed

Lines changed: 1141 additions & 23 deletions

File tree

app/components/UI/Predict/hooks/usePredictOrderPreview.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ describe('usePredictOrderPreview', () => {
2525
metamaskFee: 1,
2626
providerFee: 1,
2727
totalFee: 2,
28+
totalFeePercentage: 4,
2829
},
2930
};
3031

app/components/UI/Predict/providers/polymarket/PolymarketProvider.test.ts

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,7 @@ describe('PolymarketProvider', () => {
740740
metamaskFee: 0.02,
741741
providerFee: 0.02,
742742
totalFee: 0.04,
743+
totalFeePercentage: 0.04,
743744
},
744745
...overrides,
745746
};
@@ -1358,7 +1359,12 @@ describe('PolymarketProvider', () => {
13581359
const { provider, mockSigner } = setupPlaceOrderTest();
13591360
const preview = createMockOrderPreview({
13601361
side: Side.BUY,
1361-
fees: { metamaskFee: 0.02, providerFee: 0.02, totalFee: 0.04 },
1362+
fees: {
1363+
metamaskFee: 0.02,
1364+
providerFee: 0.02,
1365+
totalFee: 0.04,
1366+
totalFeePercentage: 0.04,
1367+
},
13621368
});
13631369
const orderParams: PlaceOrderParams = {
13641370
providerId: 'polymarket',
@@ -1374,7 +1380,12 @@ describe('PolymarketProvider', () => {
13741380
const { provider, mockSigner } = setupPlaceOrderTest();
13751381
const preview = createMockOrderPreview({
13761382
side: Side.BUY,
1377-
fees: { metamaskFee: 0.02, providerFee: 0.02, totalFee: 0.04 },
1383+
fees: {
1384+
metamaskFee: 0.02,
1385+
providerFee: 0.02,
1386+
totalFee: 0.04,
1387+
totalFeePercentage: 0.04,
1388+
},
13781389
});
13791390
const orderParams: PlaceOrderParams = {
13801391
providerId: 'polymarket',
@@ -1395,7 +1406,12 @@ describe('PolymarketProvider', () => {
13951406
const { provider, mockSigner } = setupPlaceOrderTest();
13961407
const preview = createMockOrderPreview({
13971408
side: Side.BUY,
1398-
fees: { metamaskFee: 0.02, providerFee: 0.02, totalFee: 0.04 },
1409+
fees: {
1410+
metamaskFee: 0.02,
1411+
providerFee: 0.02,
1412+
totalFee: 0.04,
1413+
totalFeePercentage: 0.04,
1414+
},
13991415
});
14001416
const orderParams: PlaceOrderParams = {
14011417
providerId: 'polymarket',
@@ -1416,7 +1432,12 @@ describe('PolymarketProvider', () => {
14161432
const { provider, mockSigner } = setupPlaceOrderTest();
14171433
const preview = createMockOrderPreview({
14181434
side: Side.BUY,
1419-
fees: { metamaskFee: 0.02, providerFee: 0.02, totalFee: 0.04 },
1435+
fees: {
1436+
metamaskFee: 0.02,
1437+
providerFee: 0.02,
1438+
totalFee: 0.04,
1439+
totalFeePercentage: 0.04,
1440+
},
14201441
});
14211442
const orderParams: PlaceOrderParams = {
14221443
providerId: 'polymarket',
@@ -1447,7 +1468,12 @@ describe('PolymarketProvider', () => {
14471468
const { provider, mockSigner } = setupPlaceOrderTest();
14481469
const preview = createMockOrderPreview({
14491470
side: Side.BUY,
1450-
fees: { metamaskFee: 0.02, providerFee: 0.02, totalFee: 0.04 },
1471+
fees: {
1472+
metamaskFee: 0.02,
1473+
providerFee: 0.02,
1474+
totalFee: 0.04,
1475+
totalFeePercentage: 0.04,
1476+
},
14511477
});
14521478
const orderParams: PlaceOrderParams = {
14531479
providerId: 'polymarket',
@@ -1472,7 +1498,12 @@ describe('PolymarketProvider', () => {
14721498
const { provider, mockSigner } = setupPlaceOrderTest();
14731499
const preview = createMockOrderPreview({
14741500
side: Side.BUY,
1475-
fees: { metamaskFee: 0, providerFee: 0, totalFee: 0 },
1501+
fees: {
1502+
metamaskFee: 0,
1503+
providerFee: 0,
1504+
totalFee: 0,
1505+
totalFeePercentage: 0,
1506+
},
14761507
});
14771508

14781509
await provider.placeOrder({

app/components/UI/Predict/providers/polymarket/utils.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1832,6 +1832,7 @@ describe('polymarket utils', () => {
18321832
expect(fees.totalFee).toBe(expectedTotal);
18331833
expect(fees.providerFee).toBe(expectedEach);
18341834
expect(fees.metamaskFee).toBe(expectedEach);
1835+
expect(fees.totalFeePercentage).toBe(FEE_PERCENTAGE);
18351836
});
18361837

18371838
it('calculates fees correctly for various amounts', async () => {
@@ -1845,6 +1846,7 @@ describe('polymarket utils', () => {
18451846
expect(fees.providerFee).toBeGreaterThanOrEqual(0);
18461847
expect(fees.metamaskFee).toBeGreaterThanOrEqual(0);
18471848
expect(fees.totalFee).toBeGreaterThanOrEqual(0);
1849+
expect(fees.totalFeePercentage).toBe(FEE_PERCENTAGE);
18481850
});
18491851

18501852
it('handles large amounts correctly', async () => {
@@ -1860,6 +1862,7 @@ describe('polymarket utils', () => {
18601862
expect(fees.totalFee).toBe(expectedTotal);
18611863
expect(fees.providerFee).toBe(expectedEach);
18621864
expect(fees.metamaskFee).toBe(expectedEach);
1865+
expect(fees.totalFeePercentage).toBe(FEE_PERCENTAGE);
18631866
});
18641867

18651868
it('handles small amounts correctly', async () => {
@@ -1878,6 +1881,7 @@ describe('polymarket utils', () => {
18781881
expect(fees.totalFee).toBe(expectedTotal);
18791882
expect(fees.providerFee).toBe(expectedEach);
18801883
expect(fees.metamaskFee).toBe(expectedEach);
1884+
expect(fees.totalFeePercentage).toBe(FEE_PERCENTAGE);
18811885
});
18821886

18831887
it('waives fees for markets with middle-east tag', async () => {
@@ -1899,6 +1903,7 @@ describe('polymarket utils', () => {
18991903
expect(fees.providerFee).toBe(0);
19001904
expect(fees.metamaskFee).toBe(0);
19011905
expect(fees.totalFee).toBe(0);
1906+
expect(fees.totalFeePercentage).toBe(0);
19021907
});
19031908
});
19041909

app/components/UI/Predict/providers/polymarket/utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,7 @@ export async function calculateFees({
755755
metamaskFee: 0,
756756
providerFee: 0,
757757
totalFee: 0,
758+
totalFeePercentage: 0,
758759
};
759760
}
760761

@@ -773,6 +774,7 @@ export async function calculateFees({
773774
metamaskFee,
774775
providerFee,
775776
totalFee,
777+
totalFeePercentage: FEE_PERCENTAGE,
776778
};
777779
}
778780

app/components/UI/Predict/providers/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export interface PredictFees {
7272
metamaskFee: number;
7373
providerFee: number;
7474
totalFee: number;
75+
totalFeePercentage: number;
7576
}
7677

7778
export interface GeoBlockResponse {
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { calculateMaxBetAmount, generateOrderId } from './orders';
2+
3+
// Mock react-native-quick-crypto
4+
jest.mock('react-native-quick-crypto', () => ({
5+
randomUUID: jest.fn(() => 'mock-uuid-1234-5678-9012'),
6+
}));
7+
8+
describe('orders utils', () => {
9+
describe('generateOrderId', () => {
10+
it('returns a UUID string', () => {
11+
const orderId = generateOrderId();
12+
13+
expect(typeof orderId).toBe('string');
14+
expect(orderId).toBe('mock-uuid-1234-5678-9012');
15+
});
16+
});
17+
18+
describe('calculateMaxBetAmount', () => {
19+
it('returns the original amount when totalFeePercentage is 0', () => {
20+
const result = calculateMaxBetAmount(100, 0);
21+
22+
expect(result).toBe(100);
23+
});
24+
25+
it('returns reduced amount when totalFeePercentage is applied', () => {
26+
const result = calculateMaxBetAmount(100, 4);
27+
28+
expect(result).toBe(96);
29+
});
30+
31+
it('rounds result to 4 decimal places', () => {
32+
const result = calculateMaxBetAmount(100, 3.333);
33+
34+
// 100 * (1 - 3.333/100) = 100 * 0.96667 = 96.667
35+
// Rounded to 4 decimals = 96.667
36+
expect(result).toBe(96.667);
37+
});
38+
39+
it('handles small amounts correctly', () => {
40+
const result = calculateMaxBetAmount(1, 4);
41+
42+
// 1 * (1 - 4/100) = 1 * 0.96 = 0.96
43+
expect(result).toBe(0.96);
44+
});
45+
46+
it('handles very small fee percentages', () => {
47+
const result = calculateMaxBetAmount(100, 0.1);
48+
49+
// 100 * (1 - 0.1/100) = 100 * 0.999 = 99.9
50+
expect(result).toBe(99.9);
51+
});
52+
53+
it('handles large fee percentages', () => {
54+
const result = calculateMaxBetAmount(100, 50);
55+
56+
// 100 * (1 - 50/100) = 100 * 0.5 = 50
57+
expect(result).toBe(50);
58+
});
59+
60+
it('handles decimal amounts', () => {
61+
const result = calculateMaxBetAmount(50.5, 4);
62+
63+
// 50.5 * (1 - 4/100) = 50.5 * 0.96 = 48.48
64+
expect(result).toBe(48.48);
65+
});
66+
67+
it('handles edge case with 100% fee', () => {
68+
const result = calculateMaxBetAmount(100, 100);
69+
70+
// 100 * (1 - 100/100) = 100 * 0 = 0
71+
expect(result).toBe(0);
72+
});
73+
74+
it('handles zero amount', () => {
75+
const result = calculateMaxBetAmount(0, 4);
76+
77+
expect(result).toBe(0);
78+
});
79+
80+
it('preserves precision for amounts with many decimal places', () => {
81+
const result = calculateMaxBetAmount(100.123456, 4);
82+
83+
// 100.123456 * 0.96 = 96.11851776, rounded to 4 decimals = 96.1185
84+
expect(result).toBe(96.1185);
85+
});
86+
});
87+
});

app/components/UI/Predict/utils/orders.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,15 @@ import QuickCrypto from 'react-native-quick-crypto';
77
export function generateOrderId(): string {
88
return QuickCrypto.randomUUID();
99
}
10+
11+
export function calculateMaxBetAmount(
12+
amount: number,
13+
totalFeePercentage: number,
14+
): number {
15+
if (totalFeePercentage === 0) {
16+
return amount;
17+
}
18+
const maxBetAmount = amount * (1 - totalFeePercentage / 100);
19+
// Round to 4 decimals (same as calculateFees in polymarket/utils.ts)
20+
return Math.round(maxBetAmount * 10000) / 10000;
21+
}

0 commit comments

Comments
 (0)