Skip to content

CP-14878: add limit price to perps place order + open orders/activity tabs - #4007

Open
bogdandobritoiu wants to merge 22 commits into
mainfrom
CP-14878
Open

CP-14878: add limit price to perps place order + open orders/activity tabs#4007
bogdandobritoiu wants to merge 22 commits into
mainfrom
CP-14878

Conversation

@bogdandobritoiu

@bogdandobritoiu bogdandobritoiu commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Description

Ticket: CP-14878

Adds limit orders to the perps place-order flow and a tabbed My positions screen (positions / open orders / activity).

Place order — limit price

  • "Set limit price" toggle at the top of the amount card; toggling on opens a limit-price editor (/perpetualsPlaceOrder/limitPrice, same pattern as the TP/SL trigger screen). The set price shows in a drill row with its % relation to the current price
  • Limit state lives in PlaceOrderContext with the same semantics as TP/SL: enabled only once a price exists, toggling off keeps the price for re-enable
  • Submits orderKind: 'limit' + limitPx through the existing usePerpsOrderSubmit plumbing (Gtc, TP/SL attachment supported)
  • Order sizing, position capacity (dial max / "Maximum position" / exceeds-gate) and the liquidation estimate are all denominated at the effective entry price (limit when set, mark otherwise), so a full dial can never exceed HL's max contract size
  • TP/SL validation and projected P&L reference the limit entry price when one is set
  • No directional constraint on the limit price (deliberate): any positive price is a legal order; a marketable limit just fills immediately

My positions — tabs

  • Bottom segmented control (All positions / Open orders / Activity) driving CollapsibleTabs, following the Portfolio screen pattern; positions list extracted into PositionsList with the filter chips in its list header
  • Open orders tab: resting orders live over WS (main dex + HIP-3 merged), TP/SL trigger legs excluded, per-row Cancel via the existing cancelOrder action
  • Activity tab: all fills in one chronological feed (parity with core-web); the standalone PositionsHistory screen/route is removed as superseded
  • Pull-to-refresh on all three tabs via a shared usePerpsPullToRefresh hook

No new dependencies. Figma: Place bet limit / Open positions

Screenshots/Videos

Simulator.Screen.Recording.-.iPhone.17.Pro.-.2026-07-23.at.13.49.10.mov

Testing

iOS - 9395
Android - 9396

Dev Testing

Happy path:

  1. Perps → pick a market → Long/Short → toggle "Set limit price" → editor opens; enter a price below mark (long) → Done
  2. The Price row shows the price and "-x% below current price"; dial max reflects maxOpenSizeCoin × limit price
  3. Slide to submit → order lands as a resting limit on Hyperliquid ("Order submitted")
  4. My positions → Open orders tab → the resting order is listed; Cancel removes it ("Order cancelled")
  5. Activity tab shows fills; pull-to-refresh works on all three tabs

Edge cases:

  • Toggle on and back out of the editor without Done → toggle stays off; toggle off and on again → previous price restored
  • Limit + TP/SL attached: TP between limit and mark is accepted (validated vs the limit entry, not mark); projected P&L is measured from the limit price
  • Marketable limit (long limit above mark) fills immediately — expected, no directional error
  • Geo-block and enable-trading gates still run before a limit submit

Bitrise:

iOS - 9391
Android - 9391

QA Testing

  • Place resting limit orders both sides (long below mark / short above mark), with and without TP/SL; verify fill behavior and that attached TP/SL trigger correctly after the limit fills
  • Verify dial capacity: with a limit far below mark, max out the dial and confirm HL accepts the order (size never exceeds the per-asset max)
  • Open orders tab: rows show coin, side, size @ limit price and notional; cancel each; HIP-3 (builder-dex) orders appear alongside main-dex ones; TP/SL legs of open positions do NOT appear here
  • Activity tab matches the fills history previously shown in the History modal (screen was removed); newest first
  • Segmented control ↔ swipe stay in sync; pull-to-refresh spinner settles on all tabs; empty states per tab
  • Regression: market orders (toggle off) unchanged; manage-position flow (TP/SL editing, leverage, margin mode) unaffected

Checklist

Please check all that apply (if applicable)

  • I have performed a self-review of my code
  • I have verified the code works
  • I have included screenshots / videos of android and ios
  • I have added testing steps
  • I have added/updated necessary unit tests
  • I have updated the documentation

🤖 Generated with Claude Code

bogdandobritoiu and others added 13 commits July 22, 2026 19:57
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ve history screen

- Extract PositionsList from PerpetualsPositionsScreen; filters live in the list header
- Add pull-to-refresh to positions, open orders and activity via usePerpsPullToRefresh
- Add Activity segment showing all fills in one feed (parity with core-web)
- Remove the PositionsHistory screen/route, superseded by the Activity tab
- Place-order card styling polish; Positions carousel always renders

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts:
#	packages/core-mobile/app/new/features/trade/perpetuals/screens/PerpetualsPlaceOrderScreen.test.tsx
#	packages/core-mobile/app/new/features/trade/perpetuals/screens/PerpetualsPlaceOrderScreen.tsx
Copilot AI review requested due to automatic review settings July 22, 2026 20:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds limit-order support to the Perps place-order flow (including sizing/liquidation semantics keyed off the effective entry price) and refactors the “My positions” experience into a tabbed screen (Positions / Open orders / Activity) with shared pull-to-refresh, removing the superseded History modal route.

Changes:

  • Introduces limit-price state in PlaceOrderContext, a dedicated limit-price editor screen/route, and passes orderKind: 'limit' + limitPx through order submission.
  • Reworks the Positions screen into CollapsibleTabs with a bottom SegmentedControl, extracting PositionsList and adding new OpenOrdersList + PerpsActivityList.
  • Adds shared perps pull-to-refresh hook and expands unit tests around the new behavior.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/core-mobile/app/new/routes/(signedIn)/(modals)/perpetualsPositionsHistory/index.tsx Removes the positions history modal route entrypoint.
packages/core-mobile/app/new/routes/(signedIn)/(modals)/perpetualsPositionsHistory/_layout.tsx Removes the positions history modal stack layout.
packages/core-mobile/app/new/routes/(signedIn)/(modals)/perpetualsPlaceOrder/limitPrice.tsx Adds routing entrypoint for the new limit-price editor screen.
packages/core-mobile/app/new/routes/(signedIn)/(modals)/perpetualsPlaceOrder/_layout.tsx Registers the new limitPrice modal screen in the place-order stack.
packages/core-mobile/app/new/routes/(signedIn)/_layout.tsx Removes the modal registration for positions history.
packages/core-mobile/app/new/features/trade/perpetuals/utils/economics.ts Extracts pctParts formatting helper for reused “% above/below …” copy.
packages/core-mobile/app/new/features/trade/perpetuals/utils/economics.test.ts Adds unit tests covering pctParts.
packages/core-mobile/app/new/features/trade/perpetuals/screens/PerpetualsTriggerScreen.tsx Updates trigger reference/sizing logic to use limit entry when enabled.
packages/core-mobile/app/new/features/trade/perpetuals/screens/PerpetualsTriggerScreen.test.tsx Adds tests ensuring projected P&L uses seeded entry vs limit entry appropriately.
packages/core-mobile/app/new/features/trade/perpetuals/screens/PerpetualsScreen.tsx Adjusts layout around the positions carousel (padding wrapper).
packages/core-mobile/app/new/features/trade/perpetuals/screens/PerpetualsPositionsScreen.tsx Refactors “My positions” into tabbed UI and adds bottom SegmentedControl overlay.
packages/core-mobile/app/new/features/trade/perpetuals/screens/PerpetualsPositionsHistoryScreen.tsx Removes the old History screen implementation.
packages/core-mobile/app/new/features/trade/perpetuals/screens/PerpetualsPlaceOrderScreen.tsx Adds limit toggle/drill row, limit sizing, and limit submission parameters.
packages/core-mobile/app/new/features/trade/perpetuals/screens/PerpetualsPlaceOrderScreen.test.tsx Adds/extends tests for limit toggle behavior, sizing, and submit params.
packages/core-mobile/app/new/features/trade/perpetuals/screens/PerpetualsLimitPriceScreen.tsx Adds the dedicated limit-price editor screen for the place-order flow.
packages/core-mobile/app/new/features/trade/perpetuals/screens/PerpetualsLimitPriceScreen.test.tsx Adds tests for limit price validation, seeding, and commit-on-done behavior.
packages/core-mobile/app/new/features/trade/perpetuals/hooks/usePerpsUserFills.ts Updates refetch typing to return a promise so callers can await refresh completion.
packages/core-mobile/app/new/features/trade/perpetuals/hooks/usePerpsPullToRefresh.ts Introduces shared pull-to-refresh hook coordinating nonce bump + query invalidation + optional extra refresh.
packages/core-mobile/app/new/features/trade/perpetuals/hooks/usePerpsPullToRefresh.test.tsx Adds tests for refresh settling and extra refresh rejection handling.
packages/core-mobile/app/new/features/trade/perpetuals/contexts/PlaceOrderContext.tsx Adds limit price state and keys liquidation estimate off effective entry price.
packages/core-mobile/app/new/features/trade/perpetuals/components/PositionsList.tsx Extracts the positions list + filter chips + pull-to-refresh + lazy load into a standalone tab component.
packages/core-mobile/app/new/features/trade/perpetuals/components/Positions.tsx Simplifies carousel rendering (removes initial-load hiding behavior).
packages/core-mobile/app/new/features/trade/perpetuals/components/PerpsActivityList.tsx Adds Activity tab list rendering all fills with pull-to-refresh.
packages/core-mobile/app/new/features/trade/perpetuals/components/OpenOrdersList.tsx Adds Open orders tab list rendering resting orders with per-row cancel and pull-to-refresh.
packages/core-mobile/app/new/features/trade/perpetuals/components/OpenOrdersList.test.tsx Adds tests for open-order row mapping and cancel action wiring.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown

Coverage report ✅

2/2 packages passed thresholds
Thresholds are shown inline against each package baseline.

🟢 @avalabs/core-mobile

St. Category Percentage Covered / Total
🟢 Statements 28.14% (+10.14% vs 18% ▲) 12465/44296
🟢 Branches 23.75% (+11.75% vs 12% ▲) 6118/25753
🟢 Functions 22.32% (+9.32% vs 13% ▲) 2283/10227
🟢 Lines 28.22% (+10.22% vs 18% ▲) 11939/42299

🟢 @avalabs/k2-alpine

St. Category Percentage Covered / Total
🟢 Statements 7.62% (+5.62% vs 2% ▲) 270/3539
🟢 Branches 7.02% (+6.02% vs 1% ▲) 151/2148
🟢 Functions 4.52% (+3.52% vs 1% ▲) 37/817
🟢 Lines 7.07% (+5.07% vs 2% ▲) 230/3251
Artifacts and threshold sources
  • @avalabs/core-mobile: summary core-mobile/coverage/coverage-summary.json, thresholds core-mobile/coverage-thresholds.json
  • @avalabs/k2-alpine: summary k2-alpine/coverage/coverage-summary.json, thresholds k2-alpine/coverage-thresholds.json

Source run: Mobile PR

Extract OpenOrderListItem; per-row cancelling state keyed by order id in the
parent (FlashList recycling-safe) replaces the shared busy flag that dimmed
every Cancel button while one cancel was in flight.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 22, 2026 20:54
Move the in-flight state into OpenOrderListItem (button swaps to a spinner
for that row only); recycling-safe via render-phase reset on item.id change
and a shown-id guard on the async settle. Drops the parent's cancelling-ids
set and extraData.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated 5 comments.

Copilot AI review requested due to automatic review settings July 22, 2026 21:03
Web-parity chips under the input: ±1/5/10% and Mid, side-aware (long rests
below the market, short above), anchored to the live mid with mark/entry
fallbacks, snapped to Hyperliquid's price grid via roundToHyperliquidPrice.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

packages/core-mobile/app/new/features/trade/perpetuals/hooks/usePerpsPullToRefresh.ts:27

  • onRefresh can be triggered again while a previous refresh is still in flight; because each call schedules its own setIsRefreshing(false) when it settles, the spinner can turn off early. Guard against re-entrancy (or otherwise ensure only the latest refresh completion clears isRefreshing).
  const onRefresh = useCallback(() => {
    setIsRefreshing(true)
    refreshAfterTrade()
    const waits: Promise<unknown>[] = [
      queryClient.invalidateQueries({

…ditor

Redundant now that the preset chips express the relation to the market.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 22, 2026 21:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

packages/core-mobile/app/new/features/trade/perpetuals/components/OpenOrderListItem.tsx:45

  • setRenderedId/setCancelling are called during render when item.id changes. Updating state during render can trigger React warnings and can lead to render loops under certain conditions. Consider resetting the transient cancel state in a layout effect keyed on item.id instead, while keeping the shownIdRef guard to avoid stale promises toggling state.
  const [renderedId, setRenderedId] = useState(item.id)
  if (renderedId !== item.id) {
    setRenderedId(item.id)
    setCancelling(false)
  }

FiatAmountInput only seeds from the amount prop; the chips were updating
state (Done committed the right price) while the on-screen number stayed
stale. Presets now also push the value through the widget's imperative
setValue handle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 22, 2026 21:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

packages/core-mobile/app/new/features/trade/perpetuals/components/OpenOrderListItem.tsx:45

  • This component calls setState during render when item.id changes (setRenderedId / setCancelling inside the if (renderedId !== item.id) block). React warns against this pattern and it can cause unexpected re-render loops. A safer approach is to key the cancelling state by the row id (derived during render) and only update state from event handlers.
  const [cancelling, setCancelling] = useState(false)
  // FlashList recycles cells: this instance can be handed a different order
  // mid-flight. Reset the transient state during render (so the wrong row
  // never paints a spinner) and track the shown id so a stale promise can't
  // flip state for an order this cell no longer displays.
  const [renderedId, setRenderedId] = useState(item.id)
  if (renderedId !== item.id) {
    setRenderedId(item.id)
    setCancelling(false)
  }

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 22, 2026 21:46
- Drop the double-counted safe-area inset from the positions tabs padding
  (the measured segmented-control height already contains it)
- Pull-to-refresh: guard re-entrant pulls, use refreshClearinghouse instead
  of refreshAfterTrade (nonce is in the clearinghouse query key, so the
  extra invalidations were redundant), fix the docstring
- pctParts: zero is neither above nor below — 'At current price'

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

packages/core-mobile/app/new/features/trade/perpetuals/components/OpenOrderListItem.tsx:45

  • OpenOrderListItem calls setState during render when item.id changes (setRenderedId / setCancelling). This is an anti-pattern that can trigger React warnings and can cause extra renders. Instead, track the cancelling state keyed by the current item.id so recycled cells never show a stale spinner without mutating state during render.
  const [cancelling, setCancelling] = useState(false)
  // FlashList recycles cells: this instance can be handed a different order
  // mid-flight. Reset the transient state during render (so the wrong row
  // never paints a spinner) and track the shown id so a stale promise can't
  // flip state for an order this cell no longer displays.
  const [renderedId, setRenderedId] = useState(item.id)
  if (renderedId !== item.id) {
    setRenderedId(item.id)
    setCancelling(false)
  }

packages/core-mobile/app/new/features/trade/perpetuals/screens/PerpetualsTriggerScreen.tsx:108

  • currentPrice is no longer always the live/"current" mark: when a limit entry is enabled it becomes the intended entry price. This name is misleading and makes it easy for later UI copy (e.g., "above/below current price") to become incorrect. Consider renaming this to something like referencePrice/triggerReferencePrice and updating any user-facing strings that mention "current price" when the reference is the limit entry.
  const currentPrice =
    limitPriceEnabled && limitPrice !== undefined
      ? limitPrice
      : liveMarkPrice > 0
      ? liveMarkPrice

Copilot AI review requested due to automatic review settings July 22, 2026 21:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

packages/core-mobile/app/new/features/trade/perpetuals/components/OpenOrderListItem.tsx:45

  • This component calls setRenderedId / setCancelling during render when item.id changes. Updating state during render is not supported in React and can cause warnings or render loops; it’s better to reset transient state in an effect keyed on item.id (FlashList cell recycling case) while keeping the ref used to guard stale promises.
  const [renderedId, setRenderedId] = useState(item.id)
  if (renderedId !== item.id) {
    setRenderedId(item.id)
    setCancelling(false)
  }

Copilot AI review requested due to automatic review settings July 23, 2026 10:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

packages/core-mobile/app/new/features/trade/perpetuals/hooks/usePerpsPullToRefresh.ts:27

  • The re-entrancy guard currently relies on isRefreshing state, which can be stale within the same render (e.g. if onRefresh() is called twice synchronously before React re-renders). This means re-entrant refreshes aren’t strictly ignored as the docstring claims. Use a ref-backed lock for an immediate guard and avoid reading isRefreshing inside the callback.
  const [isRefreshing, setIsRefreshing] = useState(false)

  const onRefresh = useCallback(() => {
    if (isRefreshing) {
      return

@@ -0,0 +1,43 @@
import { useQueryClient } from '@tanstack/react-query'
import { ReactQueryKeys } from 'consts/reactQueryKeys'
import { useCallback, useState } from 'react'
…ions

HL rejects actions signed by an agent it no longer knows ('User or API
Wallet 0x… does not exist') — until now every order/cancel/close dead-ended
in that raw toast. Detect it in both failure shapes (business-reject string
and thrown error; the SDK matcher misses this wording), clear the stored
agent key so the manager falls back to master-wallet signing (retry works,
next enable-trading gate re-prompts), and show a session-expired message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 23, 2026 10:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 30 out of 30 changed files in this pull request and generated 1 comment.

Comment on lines +83 to +86
const referencePrice = markPrice > 0 ? markPrice : entryPrice
const { percent, suffix } = pctParts(pctFromEntry(limitPrice, referencePrice))
return `${percent}${suffix}`
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants