Commit 338aaf4
authored
feat(predict): optimistic position updates for buy/sell/claim (#22493)
## **Description**
This PR implements optimistic updates for Predict positions to provide
immediate user feedback when placing BUY/SELL orders, significantly
improving the user experience by showing skeleton loaders instead of
stale data while waiting for API confirmation.
### What is the reason for the change?
Previously, when users placed orders in the Predict feature, they had to
wait for the API to confirm the transaction before seeing their position
update. This created a poor UX where:
1. Users saw outdated position values after placing orders
2. No visual feedback indicated that an order was processing
3. Users were unsure if their action succeeded until API confirmed
### What is the improvement/solution?
**Implemented a comprehensive optimistic updates system:**
#### Core Features
- **Optimistic position creation**: When users BUY, create an immediate
optimistic position with expected values
- **Optimistic position updates**: When users BUY more of an existing
position, optimistically update with accumulated values
- **Optimistic position removal**: When users SELL or CLAIM, immediately
hide the position from the list
- **Smart validation**: Compare API responses with expected sizes to
know when to remove optimistic updates
- **Auto-cleanup**: Remove optimistic updates after 1-minute timeout if
API never confirms
- **Defensive checks**: Prevent optimistic updates on claimable
positions
#### Implementation Details
1. **PolymarketProvider.ts** (~450 lines added):
- Added `OptimisticUpdateType` enum (CREATE, UPDATE, REMOVE)
- Added `OptimisticPositionUpdate` interface with type, position data,
and expected size
- Added `#optimisticPositionUpdatesByAddress` Map to track updates per
user address
- Implemented `createOrUpdateOptimisticPosition()` for BUY orders
- Implemented `removeOptimisticPosition()` for SELL/CLAIM orders
- Implemented `applyOptimisticPositionUpdates()` to merge optimistic
data with API responses
- Implemented `isApiPositionUpdated()` with 0.1% tolerance for size
validation
- Added cleanup logic for expired optimistic updates (1-minute timeout)
2. **UI Components**:
- **PredictPosition.tsx**: Added skeleton loaders for current value and
PnL when `optimistic: true`
- **PredictPositionDetail.tsx**: Added skeleton loaders and disabled
"Cash out" button for optimistic positions
- **PredictPosition types**: Added `optimistic?: boolean` flag to
`PredictPosition` type
3. **Controller**:
- **PredictController.ts**: Include fees in optimistic balance
calculation for accurate display
4. **Comprehensive Test Coverage** (~1,830 lines of tests):
- Fixed 8 failing tests from system migration
- Added 21 new tests covering CREATE, UPDATE, REMOVE, integration, and
UI scenarios
- Achieved 91.53% statement coverage and 91.75% line coverage
## **Changelog**
CHANGELOG entry: null
## **Related issues**
Fixes:
[PRED-294](https://consensyssoftware.atlassian.net/browse/PRED-294)
## **Manual testing steps**
```gherkin
Feature: Optimistic updates for Predict positions
Scenario: User buys a new position
Given user is on the Predict Positions screen
And user has no position in a market
When user places a BUY order for a position
Then user immediately sees the new position in their list
And the position shows skeleton loaders for value and PnL
And the initial value and shares are displayed
When the API confirms the order (within 1 minute)
Then the skeleton loaders are replaced with actual values
And the position shows real-time current value and PnL
Scenario: User sells an existing position
Given user is on the Predict Positions screen
And user has an existing position
When user places a SELL order for the position
Then the position immediately disappears from the list
When the API confirms the sale
Then the position remains hidden (optimistic update removed)
Scenario: User buys more of an existing position
Given user has an existing position with 10 shares
When user places another BUY order for the same position
Then the position shows skeleton loaders
And the expected accumulated values are displayed
When the API confirms the order
Then actual updated values replace the skeletons
Scenario: API timeout cleanup
Given user placed a BUY order 1 minute ago
And the API has not yet confirmed
When 1 minute has passed since the order
Then the optimistic update is automatically cleaned up
And user sees either the confirmed API position or no position
```
## **Screenshots/Recordings**
### **Before**
After placing an order, users saw stale position data with no indication
that an order was processing.
### **After**
After placing an order, users immediately see:
- New positions appear with skeleton loaders
- Existing positions update with skeleton loaders
- Sold positions disappear immediately
- Skeleton loaders are replaced with real values when API confirms
https://github.com/user-attachments/assets/a2b08c94-f223-41a0-b85f-615abd800780
## **Pre-merge author checklist**
- [x] 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).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] 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.
---
## **Technical Implementation Summary**
### Files Changed
- `PolymarketProvider.ts`: +450 lines (optimistic update system)
- `PredictPosition.tsx`: +29 lines (skeleton loaders)
- `PredictPositionDetail.tsx`: +27 lines (skeleton loaders, disabled
button)
- `PredictController.ts`: +3 lines (fee calculation)
- `types/index.ts`: +1 line (`optimistic?: boolean` flag)
- Test files: +1,830 lines (comprehensive coverage)
### Optimistic Update Flow
**BUY Order:**
1. User places BUY order → `placeOrder()` called
2. Order submitted to API via `submitClobOrder()`
3. On success, `createOrUpdateOptimisticPosition()` called
4. Optimistic position created with `optimistic: true` flag
5. `getPositions()` merges optimistic position with API data
6. UI shows skeleton loaders for value/PnL
7. When API returns position with expected size → optimistic update
removed
8. If API doesn't confirm within 1 minute → optimistic update expires
**SELL/CLAIM Order:**
1. User places SELL/CLAIM order → `placeOrder()` or `confirmClaim()`
called
2. `removeOptimisticPosition()` marks position for removal
3. `getPositions()` filters out the position
4. Position immediately disappears from UI
5. When API confirms (position gone or size reduced) → optimistic update
removed
### Test Coverage
**Total: 21 new tests added**
**PolymarketProvider (14 tests):**
- CREATE scenarios: 7 tests (position creation, value calculation,
market details)
- UPDATE scenarios: 4 tests (accumulation, avgPrice, preservation)
- Integration: 3 tests (BUY→confirm, timeout, BUY→SELL)
**UI Components (7 tests):**
- PredictPosition: 4 tests (skeleton display, value hiding)
- PredictPositionDetail: 3 tests (skeleton display, button state)
**Coverage Achieved:**
- Statements: 91.53% ✅
- Branches: 83.01% ✅
- Functions: 91.6% ✅
- Lines: 91.75% ✅
All metrics exceed the 80% target.
[PRED-294]:
https://consensyssoftware.atlassian.net/browse/PRED-294?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> Adds full optimistic position create/update/remove flow with UI
skeletons, disables actions during optimism, adjusts balance for fees,
and expands tests.
>
> - **Predict Provider (Polymarket)**:
> - Implement optimistic updates system (`CREATE`/`UPDATE`/`REMOVE`) via
in-memory map keyed by address and `outcomeTokenId`.
> - Merge optimistic state in `getPositions` with timeout cleanup (1
min) and API-size validation tolerance.
> - Block orders on claimable positions; add targeted fetch by
`outcomeId` in `getPositions`.
> - Apply optimistic removal on SELL and CLAIM; create/update optimistic
positions on BUY using market details when available.
> - **UI**:
> - `PredictPosition` and `PredictPositionDetail`: show skeletons for
value/PnL when `position.optimistic` is true; disable "Cash out" for
optimistic positions.
> - **Controller**:
> - Deduct total fees from balance during optimistic BUY balance update.
> - **Types/Providers API**:
> - Add `optimistic?: boolean` to `PredictPosition`; add `outcomeId` to
`GetPositionsParams` and wire through provider method.
> - **Tests**:
> - Extensive new tests covering optimistic create/update/remove flows,
integration scenarios, and UI behavior.
> - **Misc**:
> - Minor hook lint suppression in `usePredictPrices`.
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
144511a. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->1 parent d089aea commit 338aaf4
File tree
12 files changed
+2307
-111
lines changed- app/components/UI/Predict
- components
- PredictPositionDetail
- PredictPosition
- controllers
- hooks
- providers
- polymarket
- types
- views/PredictTransactionsView
12 files changed
+2307
-111
lines changedLines changed: 3 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
39 | 42 | | |
40 | 43 | | |
41 | 44 | | |
| |||
Lines changed: 29 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
211 | 211 | | |
212 | 212 | | |
213 | 213 | | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
214 | 243 | | |
Lines changed: 20 additions & 9 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
| 18 | + | |
18 | 19 | | |
19 | 20 | | |
20 | 21 | | |
| |||
34 | 35 | | |
35 | 36 | | |
36 | 37 | | |
| 38 | + | |
37 | 39 | | |
38 | 40 | | |
39 | 41 | | |
| |||
71 | 73 | | |
72 | 74 | | |
73 | 75 | | |
74 | | - | |
75 | | - | |
76 | | - | |
77 | | - | |
78 | | - | |
79 | | - | |
80 | | - | |
81 | | - | |
82 | | - | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
83 | 94 | | |
84 | 95 | | |
85 | 96 | | |
| |||
Lines changed: 22 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
250 | 250 | | |
251 | 251 | | |
252 | 252 | | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
253 | 275 | | |
Lines changed: 19 additions & 8 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
| 27 | + | |
27 | 28 | | |
28 | 29 | | |
29 | 30 | | |
| |||
46 | 47 | | |
47 | 48 | | |
48 | 49 | | |
| 50 | + | |
49 | 51 | | |
50 | 52 | | |
51 | 53 | | |
| |||
80 | 82 | | |
81 | 83 | | |
82 | 84 | | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
83 | 90 | | |
84 | 91 | | |
85 | 92 | | |
| |||
135 | 142 | | |
136 | 143 | | |
137 | 144 | | |
138 | | - | |
139 | | - | |
140 | | - | |
141 | | - | |
142 | | - | |
143 | | - | |
144 | | - | |
145 | | - | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
146 | 156 | | |
147 | 157 | | |
148 | 158 | | |
| |||
156 | 166 | | |
157 | 167 | | |
158 | 168 | | |
| 169 | + | |
159 | 170 | | |
160 | 171 | | |
161 | 172 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1168 | 1168 | | |
1169 | 1169 | | |
1170 | 1170 | | |
| 1171 | + | |
1171 | 1172 | | |
1172 | 1173 | | |
1173 | 1174 | | |
1174 | 1175 | | |
1175 | 1176 | | |
1176 | 1177 | | |
1177 | 1178 | | |
1178 | | - | |
| 1179 | + | |
1179 | 1180 | | |
1180 | 1181 | | |
1181 | 1182 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
147 | 147 | | |
148 | 148 | | |
149 | 149 | | |
| 150 | + | |
150 | 151 | | |
151 | 152 | | |
152 | 153 | | |
| |||
0 commit comments