Commit 268e164
authored
fix(perps): Perp order failure: Order 0: insufficient margin to place order (#29800)
## **Description**
When placing a limit order, position size, margin, and max order size
were calculated using the current market price instead of the user-set
limit price. This caused "insufficient margin" errors for Short orders
with limit prices above market at 100% slider, and silent order
downsizing for Long orders with low limit prices.
Introduced `effectivePrice` that uses the limit price for limit orders
and market price for market orders. Applied consistently across position
size, margin calculation, max order amount, and order submission params.
## **Changelog**
CHANGELOG entry: Fixed limit order margin calculation to use limit price
instead of market price, preventing "insufficient margin" errors
## **Related issues**
Fixes:
[TAT-3086](https://consensyssoftware.atlassian.net/browse/TAT-3086)
## **Manual testing steps**
```gherkin
Feature: Limit order margin calculation
Scenario: Short limit order at 100% slider with price above market
Given wallet is unlocked and on BTC market
When user taps Short, switches to Limit, sets limit price 5% above market
And sets slider to 100% and taps Open Order
Then order is placed without "insufficient margin" error
Scenario: Long limit order at 100% slider with price below market
Given wallet is unlocked and on BTC market
When user taps Long, switches to Limit, sets limit price below market
And sets slider to 100% and taps Open Order
Then order is placed without error
```
## **Screenshots/Recordings**
Before: Short limit order at $83k (+5% above market) with 100% slider
fails with 'Order failed: Insufficient margin' toast. After: Same flow —
order placed successfully.
**Video**
<table>
<tr><td align="center" width="50%"><em>Before</em><br/><a
href="https://raw.githubusercontent.com/abretonc7s/mm-mobile-farm-artifacts/main/fixes/29800/before.mp4">before.mp4</a></td>
<td align="center" width="50%"><em>After</em><br/><a
href="https://raw.githubusercontent.com/abretonc7s/mm-mobile-farm-artifacts/main/fixes/29800/after.mp4">after.mp4</a></td></tr>
</table>
## **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.
#### Performance checks (if applicable)
- [x] I've tested on Android
- Ideally on a mid-range device; emulator is acceptable
- [x] I've tested with a power user scenario
- Use these [power-user
SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93)
to import wallets with many accounts and tokens
- [x] I've instrumented key operations with Sentry traces for production
performance metrics
- See [`trace()`](/app/util/trace.ts) for usage and
[`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274)
for an example
For performance guidelines and tooling, see the [Performance
Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers).
## **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.
## **Validation Recipe**
<details>
<summary>recipe.json — 60-node workflow validating limit order margin
fix</summary>
```json
{
"pr": "29800",
"title": "Verify limit order margin calculation uses limit price instead of market price",
"jira": "TAT-3086",
"acceptance_criteria": [
"AC1: Max USD order size at 100% slider accounts for limit price, not spot",
"AC2: Changing limit price recalculates max order size and margin in real time",
"AC3: Submitting limit order at 100% slider with diverging price produces no insufficient margin error",
"AC4: UNTESTABLE — Extension-only (slot runs iOS Mobile)",
"AC5: Behavior consistent across Long and Short on Mobile"
],
"validate": {
"workflow": {
"pre_conditions": ["wallet.unlocked", "perps.ready_to_trade"],
"entry": "setup-nav-short",
"nodes": "... (60 nodes — see artifacts/recipe.json for full graph)"
}
}
}
```
</details>
## **Recipe Workflow**
<details>
<summary>workflow.mmd — Mermaid diagram of the validation
workflow</summary>
```mermaid
flowchart TD
%% Verify limit order margin calculation uses limit price instead of market price
__entry__(["ENTRY"]) --> node_setup_nav_short
node_setup_nav_short["setup-nav-short<br/>navigate"]
node_setup_wait_short_btn["setup-wait-short-btn<br/>wait_for"]
node_setup_press_short["setup-press-short<br/>press"]
node_setup_wait_order_type["setup-wait-order-type<br/>wait_for"]
node_setup_press_order_type["setup-press-order-type<br/>press"]
node_setup_wait_limit_option["setup-wait-limit-option<br/>wait_for"]
node_setup_press_limit["setup-press-limit<br/>press"]
node_setup_wait_limit_form["setup-wait-limit-form<br/>wait_for"]
node_setup_press_limit_price_row["setup-press-limit-price-row<br/>press"]
node_setup_wait_limit_keypad["setup-wait-limit-keypad<br/>wait_for"]
node_setup_clear_limit_keypad["setup-clear-limit-keypad<br/>clear_keypad"]
node_setup_type_limit_price["setup-type-limit-price<br/>type_keypad"]
node_setup_confirm_limit_price["setup-confirm-limit-price<br/>press"]
node_setup_wait_amount_display["setup-wait-amount-display<br/>wait_for"]
node_ac1_read_max_amount["ac1-read-max-amount<br/>eval_sync"]
node_ac1_screenshot_short_limit["ac1-screenshot-short-limit<br/>screenshot"]
node_ac2_change_limit_price["ac2-change-limit-price<br/>press"]
node_ac2_wait_limit_keypad["ac2-wait-limit-keypad<br/>wait_for"]
node_ac2_clear_limit_keypad["ac2-clear-limit-keypad<br/>clear_keypad"]
node_ac2_type_new_limit_price["ac2-type-new-limit-price<br/>type_keypad"]
node_ac2_confirm_new_price["ac2-confirm-new-price<br/>press"]
node_ac2_wait_recalc["ac2-wait-recalc<br/>wait"]
node_ac2_read_new_max["ac2-read-new-max<br/>eval_sync"]
node_ac2_screenshot_recalc["ac2-screenshot-recalc<br/>screenshot"]
node_ac3_press_amount["ac3-press-amount<br/>press"]
node_ac3_wait_keypad["ac3-wait-keypad<br/>wait_for"]
node_ac3_press_max["ac3-press-max<br/>press"]
node_ac3_press_done["ac3-press-done<br/>press"]
node_ac3_wait_place_order["ac3-wait-place-order<br/>wait_for"]
node_ac3_read_margin_before_submit["ac3-read-margin-before-submit<br/>eval_sync"]
node_ac3_press_place_order["ac3-press-place-order<br/>press"]
node_ac3_wait_for_result["ac3-wait-for-result<br/>wait"]
node_ac3_check_no_error["ac3-check-no-error<br/>log_watch"]
node_ac3_screenshot_no_error["ac3-screenshot-no-error<br/>screenshot"]
node_teardown_cancel_short[["teardown-cancel-short<br/>perps/order-limit-cancel"]]
node_ac5_nav_long["ac5-nav-long<br/>navigate"]
node_ac5_wait_long_btn["ac5-wait-long-btn<br/>wait_for"]
node_ac5_press_long["ac5-press-long<br/>press"]
node_ac5_wait_order_type["ac5-wait-order-type<br/>wait_for"]
node_ac5_press_order_type["ac5-press-order-type<br/>press"]
node_ac5_wait_limit_option["ac5-wait-limit-option<br/>wait_for"]
node_ac5_press_limit["ac5-press-limit<br/>press"]
node_ac5_wait_limit_form["ac5-wait-limit-form<br/>wait_for"]
node_ac5_press_limit_price["ac5-press-limit-price<br/>press"]
node_ac5_wait_keypad["ac5-wait-keypad<br/>wait_for"]
node_ac5_clear_keypad["ac5-clear-keypad<br/>clear_keypad"]
node_ac5_type_limit_price["ac5-type-limit-price<br/>type_keypad"]
node_ac5_confirm_limit_price["ac5-confirm-limit-price<br/>press"]
node_ac5_wait_amount["ac5-wait-amount<br/>wait_for"]
node_ac5_press_amount["ac5-press-amount<br/>press"]
node_ac5_wait_amount_keypad["ac5-wait-amount-keypad<br/>wait_for"]
node_ac5_press_max["ac5-press-max<br/>press"]
node_ac5_press_done["ac5-press-done<br/>press"]
node_ac5_wait_place_order["ac5-wait-place-order<br/>wait_for"]
node_ac5_read_margin["ac5-read-margin<br/>eval_sync"]
node_ac5_press_place_order["ac5-press-place-order<br/>press"]
node_ac5_wait_result["ac5-wait-result<br/>wait"]
node_ac5_check_no_error["ac5-check-no-error<br/>log_watch"]
node_ac5_screenshot_long_no_error["ac5-screenshot-long-no-error<br/>screenshot"]
node_teardown_cancel_long[["teardown-cancel-long<br/>perps/order-limit-cancel"]]
node_setup_done(["setup-done<br/>PASS"])
node_setup_nav_short --> node_setup_wait_short_btn
node_setup_wait_short_btn --> node_setup_press_short
node_setup_press_short --> node_setup_wait_order_type
node_setup_wait_order_type --> node_setup_press_order_type
node_setup_press_order_type --> node_setup_wait_limit_option
node_setup_wait_limit_option --> node_setup_press_limit
node_setup_press_limit --> node_setup_wait_limit_form
node_setup_wait_limit_form --> node_setup_press_limit_price_row
node_setup_press_limit_price_row --> node_setup_wait_limit_keypad
node_setup_wait_limit_keypad --> node_setup_clear_limit_keypad
node_setup_clear_limit_keypad --> node_setup_type_limit_price
node_setup_type_limit_price --> node_setup_confirm_limit_price
node_setup_confirm_limit_price --> node_setup_wait_amount_display
node_setup_wait_amount_display --> node_ac1_read_max_amount
node_ac1_read_max_amount --> node_ac1_screenshot_short_limit
node_ac1_screenshot_short_limit --> node_ac2_change_limit_price
node_ac2_change_limit_price --> node_ac2_wait_limit_keypad
node_ac2_wait_limit_keypad --> node_ac2_clear_limit_keypad
node_ac2_clear_limit_keypad --> node_ac2_type_new_limit_price
node_ac2_type_new_limit_price --> node_ac2_confirm_new_price
node_ac2_confirm_new_price --> node_ac2_wait_recalc
node_ac2_wait_recalc --> node_ac2_read_new_max
node_ac2_read_new_max --> node_ac2_screenshot_recalc
node_ac2_screenshot_recalc --> node_ac3_press_amount
node_ac3_press_amount --> node_ac3_wait_keypad
node_ac3_wait_keypad --> node_ac3_press_max
node_ac3_press_max --> node_ac3_press_done
node_ac3_press_done --> node_ac3_wait_place_order
node_ac3_wait_place_order --> node_ac3_read_margin_before_submit
node_ac3_read_margin_before_submit --> node_ac3_press_place_order
node_ac3_press_place_order --> node_ac3_wait_for_result
node_ac3_wait_for_result --> node_ac3_check_no_error
node_ac3_check_no_error --> node_ac3_screenshot_no_error
node_ac3_screenshot_no_error --> node_teardown_cancel_short
node_teardown_cancel_short --> node_ac5_nav_long
node_ac5_nav_long --> node_ac5_wait_long_btn
node_ac5_wait_long_btn --> node_ac5_press_long
node_ac5_press_long --> node_ac5_wait_order_type
node_ac5_wait_order_type --> node_ac5_press_order_type
node_ac5_press_order_type --> node_ac5_wait_limit_option
node_ac5_wait_limit_option --> node_ac5_press_limit
node_ac5_press_limit --> node_ac5_wait_limit_form
node_ac5_wait_limit_form --> node_ac5_press_limit_price
node_ac5_press_limit_price --> node_ac5_wait_keypad
node_ac5_wait_keypad --> node_ac5_clear_keypad
node_ac5_clear_keypad --> node_ac5_type_limit_price
node_ac5_type_limit_price --> node_ac5_confirm_limit_price
node_ac5_confirm_limit_price --> node_ac5_wait_amount
node_ac5_wait_amount --> node_ac5_press_amount
node_ac5_press_amount --> node_ac5_wait_amount_keypad
node_ac5_wait_amount_keypad --> node_ac5_press_max
node_ac5_press_max --> node_ac5_press_done
node_ac5_press_done --> node_ac5_wait_place_order
node_ac5_wait_place_order --> node_ac5_read_margin
node_ac5_read_margin --> node_ac5_press_place_order
node_ac5_press_place_order --> node_ac5_wait_result
node_ac5_wait_result --> node_ac5_check_no_error
node_ac5_check_no_error --> node_ac5_screenshot_long_no_error
node_ac5_screenshot_long_no_error --> node_teardown_cancel_long
node_teardown_cancel_long --> node_setup_done
```
</details>
[TAT-3086]:
https://consensyssoftware.atlassian.net/browse/TAT-3086?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Touches core perps order sizing and margin math; mistakes could cause
incorrect max slider values or order rejections for limit orders, but
scope is localized to client-side calculations and covered by new tests.
>
> **Overview**
> Fixes limit-order calculations to **use the user-set limit price as
the effective entry price** across the perps order flow, so the 100%
slider, position size, margin required, liquidation price inputs, and
submission params (`currentPrice`/`priceAtCalculation`) all align with
the limit price rather than spot/mark.
>
> Updates `usePerpsOrderForm` to compute `maxPossibleAmount` using
`limitPrice` when `orderForm.type === 'limit'`, and adds focused unit
tests ensuring max amount recalculates/falls back correctly as limit
price/type changes.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
916a5b4. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->1 parent 721934e commit 268e164
3 files changed
Lines changed: 236 additions & 66 deletions
File tree
- app/components/UI/Perps
- Views/PerpsOrderView
- hooks
Lines changed: 37 additions & 23 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
536 | 536 | | |
537 | 537 | | |
538 | 538 | | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
539 | 552 | | |
540 | 553 | | |
541 | 554 | | |
| |||
545 | 558 | | |
546 | 559 | | |
547 | 560 | | |
548 | | - | |
| 561 | + | |
549 | 562 | | |
550 | 563 | | |
551 | 564 | | |
552 | 565 | | |
553 | | - | |
| 566 | + | |
554 | 567 | | |
555 | 568 | | |
556 | 569 | | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
557 | 579 | | |
558 | | - | |
| 580 | + | |
559 | 581 | | |
560 | 582 | | |
561 | 583 | | |
562 | 584 | | |
563 | 585 | | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
564 | 589 | | |
565 | 590 | | |
566 | 591 | | |
| |||
652 | 677 | | |
653 | 678 | | |
654 | 679 | | |
655 | | - | |
656 | | - | |
657 | | - | |
658 | | - | |
659 | | - | |
660 | | - | |
661 | | - | |
662 | | - | |
663 | | - | |
| 680 | + | |
| 681 | + | |
| 682 | + | |
664 | 683 | | |
665 | 684 | | |
666 | 685 | | |
667 | | - | |
668 | | - | |
669 | | - | |
670 | | - | |
671 | | - | |
672 | | - | |
673 | | - | |
674 | | - | |
675 | | - | |
| 686 | + | |
| 687 | + | |
| 688 | + | |
676 | 689 | | |
677 | 690 | | |
678 | 691 | | |
| |||
1060 | 1073 | | |
1061 | 1074 | | |
1062 | 1075 | | |
1063 | | - | |
| 1076 | + | |
1064 | 1077 | | |
1065 | 1078 | | |
1066 | 1079 | | |
1067 | | - | |
| 1080 | + | |
1068 | 1081 | | |
1069 | 1082 | | |
1070 | 1083 | | |
| |||
1166 | 1179 | | |
1167 | 1180 | | |
1168 | 1181 | | |
| 1182 | + | |
1169 | 1183 | | |
1170 | 1184 | | |
1171 | 1185 | | |
| |||
0 commit comments