Commit c602ef7
feat(perps): add lightweight position display on token details page (MetaMask#25685)
## **Description**
This PR optimizes the token details page by implementing a lightweight
way to display open perps positions without requiring full
PerpsController initialization, and adds one-click Long/Short trading
buttons that navigate directly to the order confirmation flow.
**Problem:** Previously, checking if a user had an open position on a
token required initializing the entire PerpsController with WebSocket
subscriptions, causing slow page loads. Additionally, cached position
data could become stale when users closed positions in the perps
environment. There was also no way to initiate a perps trade directly
from the token details page.
**Solution:** Added a `readOnly` mode pattern that allows querying perps
data (positions, account state, markets) via lightweight HTTP API calls
without WebSocket, wallet, or full controller initialization. Combined
with a `PerpsCacheInvalidator` service that ensures cached data is
refreshed when positions change. Added Long/Short buttons on token
details that navigate through a new `PerpsOrderRedirect` screen to
handle WebSocket initialization and order creation seamlessly.
### Key Changes
**New Hook:**
- `usePerpsPositionForAsset` - Fetches position data via single HTTP API
call with 30-second client-side cache, subscribes to cache invalidation
events
**ReadOnly Mode (Controller/Provider):**
- `getPositions({ readOnly: true, userAddress })` - Query positions
without full init
- `getAccountState({ readOnly: true, userAddress })` - Query account
state without full init
- Uses `createStandaloneInfoClient` for lightweight HTTP-only queries
**Cache Invalidation Service:**
- `PerpsCacheInvalidator` - Singleton service for loosely-coupled cache
invalidation
- Hooks subscribe to invalidation events (`positions`, `accountState`)
- Services (TradingService, AccountService) call `invalidate()` after
successful operations
- Ensures position display is always accurate even after user actions in
perps
**One-Click Trade from Token Details:**
- New `PerpsOrderRedirect` screen - a redirect/loader that initializes
the WebSocket connection inside the Perps stack, calls
`depositWithOrder()`, then navigates to the confirmation screen
- `usePerpsActions` hook - provides `handlePerpsAction(direction)`
callback for Long/Short buttons, navigates to `PerpsOrderRedirect` with
direction and asset params
- `AssetOverviewContent` integrates Long/Short buttons via
`usePerpsActions` when a perps market exists for the token
**Dynamic Button Layout:**
- Token details action buttons (Buy, Sell, Swap, Bridge, etc.) now
include Long/Short when a perps market exists
- Buttons dynamically overflow into a "More" menu when there are too
many actions
**Documentation:**
- Added "ReadOnly Mode (Lightweight Queries)" section to
`docs/perps/perps-architecture.md`
- Added "Cache Invalidation" subsection documenting the pattern
### Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ PerpsCacheInvalidator │
│ (Singleton service) │
├─────────────────────────────────────────────────────────────────┤
│ - invalidate('positions') → notifies position subscribers │
│ - invalidate('accountState') → notifies account subscribers │
│ - subscribe(type, callback) → returns unsubscribe function │
└─────────────────────────────────────────────────────────────────┘
▲ ▲
│ subscribe │ invalidate
│ │
┌─────────┴─────────┐ ┌──────────┴──────────┐
│ usePerpsPosition │ │ TradingService │
│ ForAsset │ │ (closePosition, │
│ │ │ placeOrder) │
│ (30s cache + │ │ │
│ invalidation) │ │ AccountService │
└───────────────────┘ │ (withdraw) │
└─────────────────────┘
Order Redirect Flow (Long/Short buttons):
┌────────────────┐ navigate ┌──────────────────┐ WS ready ┌─────────────────┐
│ Token Details │ ──────────────→ │PerpsOrderRedirect│ ────────────→ │ Confirmation │
│ (Long/Short) │ │ (Perps stack) │ depositWith │ Screen │
│ │ │ - init WebSocket │ Order() │ │
└────────────────┘ │ - show loader │ └─────────────────┘
└──────────────────┘
│ on error
▼
goBack() + toast
```
## **Changelog**
CHANGELOG entry: Added lightweight position display and one-click
Long/Short trading on token details page for perps-enabled assets
## **Related issues**
Fixes:
[TAT-2427](https://consensyssoftware.atlassian.net/browse/TAT-2427)
## **Manual testing steps**
```gherkin
Feature: Perps position display on token details
Scenario: User views token with open perps position
Given user has an open perps position for ETH
And user is on the wallet home screen
When user navigates to ETH token details page
Then user should see their open position displayed
And the page should load quickly without full perps initialization
Scenario: User taps on position to view details
Given user is viewing token details with an open position displayed
When user taps on the position card
Then user should be navigated to the perps market detail page
And the full perps environment should load
Scenario: User with no perps position
Given user does not have any perps positions
When user navigates to any token details page
Then no perps position card should be displayed
And no perps API calls should be made (if user is not eligible)
Scenario: Position display updates after closing position
Given user has an open perps position for ETH
And user is viewing the ETH token details page
And user sees their open position displayed
When user navigates to perps and closes their ETH position
And user navigates back to the ETH token details page
Then the position card should no longer be displayed
And the cache should have been invalidated automatically
Scenario: User taps Long button on token with perps market
Given user is viewing ETH token details
And ETH has a perps market available
When user taps the "Long" button
Then a loader screen should appear ("Preparing order...")
And the WebSocket connection should initialize
And depositWithOrder should be called
And user should be navigated to the order confirmation screen
Scenario: User taps Short button on token with perps market
Given user is viewing ETH token details
And ETH has a perps market available
When user taps the "Short" button
Then a loader screen should appear ("Preparing order...")
And the WebSocket connection should initialize
And depositWithOrder should be called
And user should be navigated to the order confirmation screen
Scenario: Long/Short buttons not shown for token without perps market
Given user is viewing a token details page for a token without a perps market (e.g. USDC)
Then no Long or Short buttons should be displayed in the action bar
Scenario: Order redirect handles error gracefully
Given user taps Long or Short on a perps-enabled token
And the depositWithOrder call fails
Then user should see an error toast notification
And user should be navigated back to the token details page
```
## **Screenshots/Recordings**
### **Before**
<!-- [screenshots/recordings] -->
### **After**
<!-- [screenshots/recordings] -->
https://github.com/user-attachments/assets/245b905f-e9ab-4b65-a96b-49f81fe51afe
## **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
- [ ] 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.
[TAT-2427]:
https://consensyssoftware.atlassian.net/browse/TAT-2427?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Introduces new read-only provider/controller code paths and cache
invalidation wiring, plus a new navigation redirect that triggers
`depositWithOrder`; mistakes could lead to stale UI or incorrect
trade/confirmation routing, but changes are scoped and covered by new
tests.
>
> **Overview**
> Adds a *read-only perps discovery path* so token details can query
perps `markets`/`positions`/`accountState` via lightweight HTTP (no full
controller/WebSocket init) by extending
`PerpsController`/`HyperLiquidProvider` with `readOnly` + `userAddress`
params and a standalone info client.
>
> Introduces `PerpsCacheInvalidator` (and platform dependency plumbing)
and wires it into `TradingService` and `AccountService` to invalidate
read-only caches after successful state-changing actions, enabling hooks
like the new `usePerpsPositionForAsset` (30s TTL + invalidation
subscriptions) to stay fresh.
>
> Updates token details (`AssetOverviewContent`) to display either a
compact `PerpsPositionCard` or a `PerpsDiscoveryBanner` based on the
read-only position lookup, adds geo-eligibility modal handling for
Long/Short, and implements a one-click trade flow via new
`PerpsOrderRedirect` route/screen that waits for perps connection, calls
`depositWithOrder`, then `StackActions.replace`s into redesigned
confirmations (or toasts + goes back on failure).
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
4a032ad. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: sahar-fehri <sahar.fehri@consensys.net>1 parent a0a4047 commit c602ef7
29 files changed
Lines changed: 3234 additions & 77 deletions
File tree
- app
- components/UI
- AssetOverview
- Perps
- Views
- __mocks__
- adapters
- components/PerpsPositionCard
- controllers
- providers
- services
- types
- hooks
- routes
- services
- types
- TokenDetails
- components
- hooks
- docs/perps
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| 14 | + | |
| 15 | + | |
14 | 16 | | |
15 | 17 | | |
16 | 18 | | |
| |||
Lines changed: 237 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
57 | 57 | | |
58 | 58 | | |
59 | 59 | | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
60 | 66 | | |
61 | 67 | | |
62 | 68 | | |
| |||
0 commit comments