Skip to content

Latest commit

 

History

History

README.md

@metamask/perps-controller

Controller for perpetual trading functionality in MetaMask.

Installation

yarn add @metamask/perps-controller

or

npm install @metamask/perps-controller

Usage

PerpsController provides a provider-agnostic API for perpetual trading. It normalizes market data, account state, trading, funding, transfers, risk calculations, and live subscriptions across enabled providers.

Applications construct the controller with a messenger and their platform-specific dependencies:

import {
  PerpsController,
  type PerpsControllerOptions,
} from '@metamask/perps-controller';

export async function createPerpsController(
  options: PerpsControllerOptions,
): Promise<PerpsController> {
  const controller = new PerpsController(options);
  await controller.init();
  return controller;
}

The controller registers its public operations as PerpsController:* messenger actions, and clients can call the same methods directly. Its main capabilities are:

  • Provider and network lifecycle management.
  • Normalized market, account, position, order, and history reads.
  • Trading, position, margin, deposit, and withdrawal operations with preflight validation and risk calculations.
  • Callback-based live prices, positions, orders, fills, order books, and candles. Each subscription returns an unsubscribe function.

The package exports the controller's parameter, result, provider, and messenger types for client integrations. Provider availability and aggregated routing are controlled by client configuration and feature flags.

Error codes

PERPS_ERROR_CODES / PerpsErrorCode are the structured codes returned to the UI for translation. New codes are additive and ship as a minor. Clients should keep a catch-all for unrecognized codes instead of an exhaustive map, so a bump does not fail to compile when a code is added.

Explicit HyperLiquid margin mode

Pass marginMode: 'cross' or marginMode: 'isolated' with an integer leverage to placeOrder. Omitted mode retains the existing isolated-leverage behavior. Explicit mode requests are validated before signing: Cross is unavailable on isolated-only markets and HIP-3 markets, and an order cannot change the mode of an asset with an open position, resting order, or active native TWAP schedule, including schedules whose first slice has not filled. Orders in the same mode may increase or reduce the existing position.

Explicit Lighter margin mode and collateral adjustments

Lighter accepts explicit cross or isolated mode with a positive integer leverage on ordinary native orders and attached protection orders for active perpetual markets. Existing positions and resting, pending or position-tied orders prevent changing their market mode. Omitted mode preserves existing selection behavior. Scale and strategy probes continue to reject explicit mode.

A mode selection must have an exactly identified executed transaction and a fresh account row showing the selected mode before dependent exposure is signed or dispatched. Transport acceptance alone does not establish execution.

updateMargin requires an open isolated position and exact micro-USDC precision. Positive amounts are bounded by fresh available account collateral; negative amounts require authoritative allocated position margin. The original position identity and bounds are checked again before signing and final dispatch. The venue still enforces its own position-risk limits. Success requires exact transaction execution. Unresolved collateral and mode transactions remain blocked across restart, nonce advance and expiry until exact terminal proof; refresh recovered outcomes before explicitly acknowledging them.

Signing without a KeyringController

By default the controller signs through the KeyringController:* messenger actions. A client without a keyring passes accountSigner in its platform dependencies (signTypedData, signPersonalMessage, optional isReady, requiresSignatureConfirmation and getChainId); the signing address still comes from the selected account, and a signer that is not ready fails with KEYRING_LOCKED. HyperLiquid user-signed actions are signed for the chain getChainId returns, or for chain 1 without it.

HyperLiquid L1 actions (orders, cancels, leverage, ...) can be signed by a client-owned agent key: return it from providerCredentials.hyperliquid.getAgentSigner(account), or bind it to an account and network with PerpsController:setAgentSigner. User-signed actions (builder fee, withdrawals) stay on the main account, and approving the agent is the client's job. When the venue rejects an agent (revoked or expired), the write fails with KEYRING_LOCKED, the agent is dropped and providerCredentials.hyperliquid.onAgentRejected is called. A "Must deposit before performing actions" answer to a request the agent signed counts as a rejection only when extraAgents no longer lists the agent, which needs a named agent. Call PerpsController:clearAgentSigners when the agent key locks.

PerpsController:prepareTradingWallet runs the setup that needs signatures (HyperLiquid account migration, builder fee and referral; Lighter key registration) before the first order, so a hardware or external wallet signs it in one guided session.

Lighter trading keys

Lighter orders are not signed by the wallet. A Lighter account (owned by the wallet's address) holds trading keys, called API keys, in numbered slots. The client's signer bridge generates the key for the slot set in providerCredentials.lighter.apiKeyIndex (default 7) and keeps its private half on the device. The wallet signs one personal_sign message to register it in that slot, during PerpsController:prepareTradingWallet or before the first order; after that, orders are signed with the key and need no wallet prompt.

A key only works where it was generated, so give each device or app instance its own slot. When the slot already holds a key this signer did not create, the provider stops with "Lighter API key slot N already contains a different key" instead of replacing it, since that key may still be in use elsewhere. Use a free slot instead: the Lighter API answers "api key not found" for GET /api/v1/apikeys?account_index=<account>&api_key_index=<slot> when the slot is free.

Lighter standalone trigger orders

Lighter supports stop_market, stop_limit, take_profit_market and take_profit_limit through placeOrder. Supply a positive triggerPrice on the market's fixed price grid; limit execution also requires price. Market execution applies the caller's slippage protection to the trigger level, and USD sizing uses that level rather than the current market price. Protection defaults to 5% when neither maxSlippageBps nor slippage is supplied. Both trigger and execution prices must fit the venue's wire range.

Leave timeInForce unset: pending triggers use the signer's default 28-day expiry, with IOC execution for trigger markets and GTT execution for trigger limits. reduceOnly retains the requested quantity. Below-minimum trigger limits are refused even when they would currently close the full position; a position can grow before activation. Attached TP/SL and strategy fields are unsupported on these standalone orders.

Position TP/SL replacement and removal recognize Core-created protection by durable client/venue IDs, even after position growth, shrinkage, provider restart or trading-key recovery into another slot. IDs are recorded before dispatch in wallet/account/network/market-scoped storage and retained through uncertain settlement. Resolved cancellations and exact terminal history prune them; storage read errors or corrupt records fail closed before protection changes. Ownership bookkeeping reads at most one recent history page. New records include the exact client's signed absolute order expiry. When the active book no longer contains an ID, it can be reclaimed after that expiry plus 30 seconds of clock slack, even if terminal history is buried or unavailable. Transaction expiry does not prove an order has expired. Active IDs and unexpired missing IDs remain owned; legacy records with unknown order expiry need exact terminal history. Bookkeeping never deep-scans history. Ownership is capped at 256 entries per wallet/account/network/market. If verified cleanup cannot make room, new protection creation refuses before dispatch rather than forgetting uncertain orders. Known live orders can still be explicitly cancelled. Journal settlement remains strict: an observed accepted submission cannot become never-landed because of a later missing lookup, and expiry proofs are recomputed on each reconciliation rather than persisted across restarts.

Legacy unrecorded reduce-only trigger-market orders on the closing side with exactly the current position quantity retain their position-protection contract, including standalone orders placed through placeOrder. Protection created before this ownership upgrade, or after its local records are lost, has no recorded ownership IDs. If that position has resized, the old trigger remains legacy and may need explicit cancellation before new protection is established. Classification reads the current position inside the write lock. Known IOC and GTT wire intents can be replaced or removed; an unknown time-in-force refuses the change. Independent partial triggers and trigger limits are preserved. Use explicit cancellation to remove those orders. Quantity does not determine ownership of Core-created TP/SL.

Managed Lighter TP/SL removal does not require an integerizable position quantity after preflight: exact recorded protection IDs can still be cancelled when an authoritative positions array shows no position, zero size or a size below the tick. Account-read failures or malformed position envelopes still refuse the operation. Unrecorded legacy protection still requires a valid live quantity and side for classification; replacement retains its size and side checks. A supplied expectedPosition must still match for managed removal, including after signing.

getOrderCapabilities reports these types only for active, known perpetual markets. Capabilities and trigger preflight refresh public metadata and fail closed on read errors instead of relying on a session's old active-market snapshot. MarketInfo.priceDecimals exposes Lighter's fixed price grid for callers deriving thresholds. Missing precision is unknown, not a zero-decimal grid.

Explicit dispatch reconciliation

reviewRecoveryVenue({ providerId }) reads fresh authoritative positions and orders for one provider, wallet, network and venue account. A ready response carries that scope and reviewedAt; transport, identity, authentication or metadata failures reject instead of returning an empty account. The read can sign an authentication token with a matching locally retained registered key. It never registers a key, allocates a venue slot, signs a financial transaction, starts protection recovery, or acknowledges or clears an obligation. Missing local read authority requires reconnecting a matching key. Aggregated callers must name the owning provider; unsupported providers return status: 'unsupported'.

A manual protection row may include an opaque recoveryId. After review, an explicit resolveRecoveryProtection({ providerId, recoveryId, symbol, expectedPosition, takeProfitPrice, stopLossPrice }) selects that exact obligation and requests new protection (omit both prices for removal). Preserve the ID verbatim. Lighter reconciles pending original-slot attempts by their exact venue identity before using a matching current registered key; the original private key is not required when this evidence is authoritative. The successor only replaces orders owned by the selected obligation. Its durable source relationship survives response loss and restart; failed or ambiguous settlement leaves recovery visible. The result distinguishes settled, unresolved, and unsupported. This is an explicit financial operation and must never run as part of review or rendering.

getRecoveredDispatches() and getPendingManualRecoveries() list local recovery state. Confirmed account absence and known Premium accounts retain their local rows without signer setup or venue reconciliation; a wallet with no recorded obligations returns an empty inventory. Premium trading remains unsupported. Transport failures, corrupt or unavailable storage, wrong-wallet accounts and unknown account types still reject. Verified accounts are indexed before venue mutation so nonce-only obligations remain discoverable after restart even if the venue reports account absence. Existing protection indices also preserve older account identities. Legacy nonce-only obligations without an account or protection index become discoverable when the venue account returns; they are never deleted or reset during absence. Account capacity is bounded by LIGHTER_RECOVERY_ACCOUNT_INDEX_LIMIT, exported through constants and constants/lighterConfig.

Call reconcileRecoveredDispatches() only when the user requests a status check. This non-financial operation reads venue evidence and updates local ledgers. It does not initialize a signer, register a key, sign, submit, cancel, acknowledge or retry an intent. Lighter checks owner-null dispatches across all trading slots; TP/SL-owned entries and their journals remain pending for their separate recovery flow. Existing quarantines do not prevent checking other unresolved entries.

Replace the displayed list with the returned list, including its opaque IDs. Pending rows and all local-only absent or Premium rows have acknowledgeable: false. Acknowledgment requires a supported current account. A pending row disappearing can mean its exact transaction was proven absent, not successful execution. Unknown outcomes remain unknown. Acknowledgment still requires explicit user review and never grants permission to resubmit an ambiguous intent. Providers without this capability return their local listing, or an empty list if they have no recovery state. Aggregation rejects when any provider fails; consumers should retain their last known rows alongside that error.

Lighter fixed partial position protection

updatePositionTPSL accepts a positive takeProfitSize or stopLossSize for one trigger, or both sizes for an equal-quantity OCO pair. Explicit quantities normalize downward on the market size grid without increasing the request. Values below one tick or above the exact current position are rejected. With the default native OCO linkage, unequal normalized quantities or only one supplied size are rejected before mutation. Omitting both sizes retains the existing full-position snapshot behavior. Explicit sizes never become the venue's dynamic zero-quantity sentinel.

Partial replacement cancels only managed or explicitly selected protection, then proves exact cancellations before creating the replacement. Unrelated independent orders remain untouched. This leaves a protection gap if creation fails. The durable operation records the original fixed quantities and client IDs before cancellation; ambiguous creation is reconciled without replay.

A proven-unsent operation releases its journal and permits fresh intent. Listing keeps unsent journals selectable until a fresh update retires them. Current-key journals with attempted transactions may appear while their operation is still running; wait for the issuing operation to finish before choosing recovery.

After a dispatched cancellation or interrupted creation, recovery never attaches the saved quantity automatically. Lighter does not expose an immutable position lifecycle ID, so even an identical-looking position could have been closed and reopened. Ordinary retries cannot change or dispatch the stored intent. Inspect the exact recovery and call resolveRecoveryProtection with its recovery ID and a fresh explicit intent for the current position. The capability metadata reports this boundary, snapshot coverage, and supported pair linkages. Hyperliquid continues to report independent fixed partial triggers and dynamic whole-position coverage through its existing implementation.

Explicit partialPairLinkage: 'independent' supports equal or unequal TP/SL quantities, including an omitted sibling captured from the full current position. At least one size and both prices are required. The default remains native equal OCO; explicit OCO is never converted to independent orders. supportedPairs reports the available choices while pair retains the provider default. Independent triggers are submitted TP then SL with separate durable identities. Uncertainty, rejection, a fired first leg or changed position stops the sibling. Failures retain accepted coverage and expose observed role-bound positionProtection receipts; saved requests are never automatically replayed. An omitted requestedSize in a receipt means captured full-position coverage. Venue activation and sibling cancellation are not implied by independent linkage.

Native Lighter attached orders

placeOrder accepts a market or limit parent with takeProfitPrice, stopLossPrice, or both. Omit attached child sizes. tpslLinkage defaults to order. The native OTO or OTOCO transaction contains the opening parent and zero-size, opposite-side, reduce-only trigger-market children. An explicit child quantity, position linkage, reduce-only parent, caller-supplied client ID, or invalid price or size grid is refused before signer setup. IOC and resting GTC limit parents are supported. The caller's slippage also bounds child market execution prices.

OrderResult.attachedOrderGroup separates signed client IDs from observed venue IDs and returns an opaque group handle. getAttachedOrderGroups lists durable local identities across restarts and trading-key changes without signer setup. Confirmed absent or known Premium accounts retain local groups through the wallet-scoped recovery account index; listing never acknowledges or replays them. reviewAttachedOrderGroups uses an existing registered local key for read-only venue authentication, then matches exact signed IDs against bounded active and bounded older inactive history for missing legs. A preparation-time cutoff stops reads before the group could have existed. The page/row budget returns historyStatus: bounded with unknown missing legs rather than failing reviews of other groups. Older journals without a preparation time retain the explicit budget. Missing orders or linkage stay unknown. A successful submission reports acceptance, not activation, a fill, or protected quantity. If a pending placeAttached: dispatch blocks writes, call reviewAttachedOrderGroups to refresh exact venue evidence, then reconcileRecoveredDispatches and acknowledge only a resolved outcome. A pending, non-acknowledgeable dispatch cannot be cleared by acknowledgment alone.

Pass the exact groupId as cancelOrder.orderId, with its symbol and provider, to cancel the owned parent and children. Cancellation rereads exact IDs after each leg, preserves unrelated triggers and reports success only when all legs are terminal. Missing identities retain the group for later explicit review. The group handle never cancels a position or creates replacement protection. Attached children are excluded from ordinary position TP/SL replacement/removal.

Unsigned intent is persisted before signing and transaction identity before dispatch. Ambiguous acceptance remains quarantined across restart and key migration. There is no automatic financial replay or attachment to a later position. Explicitly cancel a prepared group before submitting fresh intent; uncertain dispatches also require the existing exact-transaction reconciliation and acknowledgment flow. Exact failure observed by review remains durable until nonce-ledger retirement succeeds, including retries after storage failure. When the exact hash stays absent but the nonce advanced, explicit review can transfer fresh complete exact-leg observations into an acknowledgeable succeeded outcome. Saved IDs alone never authorize this transfer. The group remains owned; acknowledgment permits later intentional writes and exact cancellation without replay. Reconciliation persists exact failed, expired or nonce-consumed non-acceptance before retiring the nonce evidence; explicit group cancellation can then abandon that intent without a cancellation signature or replay of the grouped order. Signer setup and authentication still apply. Nonce advance before signed expiry remains ambiguous. Abandonment rereads exact transaction and bounded order history, and refuses recorded or freshly correlated legs. Missing history alone never proves non-acceptance. Up to 64 groups are retained per account. Only canceled groups or completed groups with all legs exactly correlated as terminal can be evicted. Review reads one bounded snapshot per market and skips unchanged persistence. Terminal groups normally return local identities without new order observations or linkage; a retained dispatch ledger keeps them eligible for fresh review and settlement.

Mobile and Extension must gate attached forwarding on attachedTpsl plus their own rollout policy. This package change does not adopt the feature in either client. attachedTpsl.lifecycleVerification remains pending. Native activation, partial-parent-fill coverage, and automatic parent/child cancellation guarantees require venue execution evidence. The current capability only describes the implemented grouped submission, read review and explicit exact-ID cancellation.

Durable Lighter Scale groups

Lighter advertises Scale as a provider source capability for active markets. Preview, validation and placement share fixed-grid ladder normalization and reject collapsed price ticks, underfunded rungs and unsupported execution fields. Sizes conserve whole base lots; USD sizing bounds the sum of every rung's limit notional. Margin and reduce-only reservations are checked before placement and again before each send. Caller-supplied slippage, trigger, attached-protection, time-in-force, full-close and other strategy fields are rejected rather than ignored.

getScalePriceLadder accepts optional sizing with either an exact base size or a maximum quote usdAmount, plus optional skew. Lighter returns sizingPreview with exact per-rung sizes, totals, size precision and the venue's maker minimums. Preview uses the placement builder without account reads, key setup or signing. It rounds base size down and never exceeds the quote budget; account collateral and reservations still require placement validation. Omitting sizing preserves the price-only result. Providers without sizing support leave sizingPreview absent, so clients must check before using it.

Each group persists wallet, network, account, trading slot, immutable rung intent and signed dispatch identity before transport. Placement polls through bounded venue visibility lag and stops after unresolved acceptance; reconnection never resumes or replays it. getScaleOrderGroups() lists local ownership, including groups without any venue order ID. reviewScaleOrderGroups({ providerId }) reconciles with registered read authority and may sign authentication, but does not register a trading key or send financial transactions. Aggregated review requires an explicit provider. Controller calls reject changes to the issuing account, network or provider while awaiting readiness or results.

OrderResult.submittedSize includes attempted children whose acceptance is unknown. acceptedSize and weightedAverageLimitPrice are omitted while any attempted child remains uncertain. acceptedChildren retains confirmed accepted children, including canceled children; childOrderIds contains resting children only. Filled quantities require authoritative venue data; limit prices never stand in for execution prices. Wholly unknown groups never create synthetic Order rows. Real venue rows that match the persisted intent carry strategyGroupId. A live row that contradicts a durably rejected child remains an ordinary order without that group's attribution.

Cancel with { orderType: 'scale', orderId: groupId, symbol }. Only exact group children are canceled, using the currently registered trading slot even when placement belonged to a previous slot. Unknown dispatches remain protected until exact terminal transaction proof, or durable expiry proof followed by fresh absent transaction and order history, allows settlement. Explicit group review can retire expired absent children without canceling accepted siblings. Fresh exact rows can recover a pending child's receipt; recovered outcomes still require explicit acknowledgment before another financial write. Proven terminal cancellation never initializes a trading signer. Order history traversal is bounded and incomplete or inconsistent history fails closed. The local journal retains at most 64 groups and only reclaims proven-terminal groups. Local disk loss cannot reconstruct an unknown group's identity; consumers must preserve the configured durable storage.

Clients must gate availability on getOrderCapabilities and route review and cancellation to the owning provider. An HTTP acknowledgment or group ID never proves a fill or successful cancellation.

Lighter native TWAP preparation

Lighter TWAP remains unavailable in getOrderCapabilities. Default placement refuses before signer setup. getTwapOrders rejects rather than reporting an empty inventory, and cancelOrder with orderType: 'twap' refuses before signing. A generic cancel transaction acknowledgment cannot establish that a native schedule has terminated or that no later slices will execute.

The utils/lighterTwap preparation primitive validates exact amount and price bounds, rounds price protection inward by side, converts whole minutes to a future millisecond expiry, and rejects randomization. It does not sign, submit, persist an operation, or expose a supported strategy. It does not impose the Hyperliquid duration rules or claim that the venue accepts every prepared value. The caller must supply a millisecond clock and current authoritative market precision and reference price before any future lifecycle integration.

Enabling TWAP requires authoritative cumulative parent-fill semantics, complete child-fill attribution and terminal cancellation evidence. Persisted ownership, restart reconciliation and uncertain-dispatch recovery must then be integrated and validated before enabling placement. Reducing a position to zero is not termination: Lighter documents that reduce-only schedules keep attempting slices until expiry.

Gated Lighter native TWAP lifecycle

The provider's default-off nativeTwapTestnetProbe constructor option enables bounded source-validation placement and cancellation on testnet. It rejects mainnet construction, requires explicit and independently observed 1x leverage, exact size within the venue's parent minimums and a maximum 20 USD initial protected notional, and refuses unsupported strategy fields. Normal controller construction does not enable this option. getOrderCapabilities and product getTwapOrders remain unavailable pending native mapping proof.

Before signing a native schedule, a separate durable journal records immutable wallet, network, account, original key slot, market, exact intent and client ID. Signed nonce, hash and transaction expiry are recorded separately from schedule expiry, and dispatch attempt is persisted before transport. Lost responses and restarts retain obligations without replaying CreateOrder. An unresolved owned schedule prevents another on the same market. The journal is bounded to 64 records and 16 cancellation attempts per record; it does not silently prune unresolved or historical records.

getNativeTwapObservations() uses existing registered read authority, never key registration or financial dispatch. It reports exact parent and child rows, unaggregated child trades, matching transaction outcomes, candidate normalized fills and explicit mapping issues. Parent identity includes account, market, client ID, side, size, price, expiry and nonce. Pagination must exhaust, repeated cursors fail, snapshots must remain stable, and parent/child/trade quantities must agree. A failed read or an omitted parent is uncertainty, not an empty schedule inventory or a completed order. Unrelated orders are excluded from parent-child reconciliation.

accountOrders has a documented retention window: last 10K active orders or last 1K inactive orders within 24 hours. History scans retain raw evidence when an exact row is absent, but canonical projection still requires a stable exact parent reread. Missing retained history therefore remains an explicit obligation. The current native mapping gate prevents terminal observations from releasing cleanup obligations. The settlement code additionally requires a matching executed cancellation for canceled parents and elapsed expiry for expired parents. Disconnecting the provider never completes or deletes schedules.

Native slice minimums, cumulative parent/child semantics, terminal timestamps and cancellation race ordering still require testnet evidence. Candidate observations and unit fixtures are not venue proof. The constructor probe option is not exposed as a production feature flag or controller action.

Lighter post-only limits

Ordinary limit orders accept timeInForce: 'ALO'. Validation and placement require active market metadata, native price/size grids and maker minimums. Unsupported strategy, attached protection and caller client-ID fields are rejected before signer setup. Fresh public native book reads reject a crossing price before setup and again inside the serialized write section. A book read older than five seconds fails closed. Native TIF 2 is the final venue guard if the book changes after local validation; GTT and IOC behavior is unchanged.

A successful ordinary placement receipt means the transaction was submitted; it does not assert that a post-only order rested or filled. A racing book can still produce native canceled-post-only history. Reconcile exact order state through ordinary venue reads. Response loss retains the exact client handle in the failure receipt and the inherited durable nonce/transaction journal blocks replay across restart until authoritative recovery. No automatic retry is added. This transport support does not advertise Chase or establish its venue proof.

Bounded Lighter Chase lifecycle

The default leaves Chase unavailable. Set clientConfig.providerCredentials.lighter.chaseTestnetProbe: true to enable the bounded probe on the actual testnet provider with an injected signer. Controller registration omits this setting on mainnet; direct mainnet construction rejects it. Enabled testnet capabilities include Chase. Production remains unavailable; client integration and live venue acceptance are still required for rollout.

The probe uses the existing orderType: 'chase' contract with exact base size, explicit existing 1x leverage, a 20 USD aggregate notional ceiling, an interval of at least 1,000 ms, duration from one interval through 300,000 ms, at most 20 reprices, and an adverse distance strictly between 0 and 10,000 bps. Defaults are 15,000 ms interval, 60,000 ms duration, one reprice and 100 bps distance. It starts from a flat account with no unrelated open orders or exposure. Every replacement requires account position quantity to match the exact cumulative owned fills, plus no outstanding prior child. Existing position value plus replacement notional must still fit the same ceiling. Native minimums apply to each remaining child; a remainder below minimum stops rather than enlarging the order.

Probe transport currently supports opening orders only; reduce-only, USD sizing, attached protection and other strategy fields remain refused at the probe boundary. The dedicated coordinator retains reduce-only intent in its internal contract, but no product or probe support is claimed for that route. No automatic leverage change, new slot allocation policy, key replacement, fee or PnL invention is introduced.

The native public order book supplies individual owner-tagged orders. Quotes remove every same-side account order, preserve opposing liquidity, and use the market's fixed price tick. Missing external liquidity, stale responses, malformed identity or a crossed book stop financial continuation. Each replacement uses newly read quotes and native post-only TIF 2.

A durable journal binds wallet, network, account, original key slot, market, immutable budget and session handle. Each exact child is persisted before signing, its hash/nonce/expiry before dispatch, and attempted state before send. A serial tick cannot replace a child until exact terminal order state, exhaustive unaggregated fill history and a stable reread agree. Exact terminal child state settles cleanup even when a fill beats its cancel transaction. Cancel transaction outcomes provide attribution and retire definitively failed attempts for explicit retry. Fills observed during cancellation reduce the next size. Acknowledgments and missing rows never prove termination. An attempted placement absent after signed expiry plus clock slack can settle only with definitive transaction and exact-order evidence; acknowledged or previously visible placements remain owned. There are at most 64 retained sessions, 21 children per session and 16 cancel attempts per child; records are never silently pruned.

getChaseOrders() returns provider-bound public state, or an empty list for an unbound or recordless wallet, including with the probe off. If current venue discovery reports no account, previously verified wallet/network journals remain visible. Suspension retains their pending cleanup locally without enabling venue reads, signing or registration. Repeated local suspension and cancellation preserve the child's cancellation capacity and existing dispatch identities, including across provider restart. Cleanup remains termination_pending with an explanatory native record error until the original account and key authority can reconcile it. PerpsController:getChaseOrderOwnership({handle, providerId, owner?}) reads one exact stable handle from validated local storage. available includes its original wallet, provider, network, account and trading-key slot, every durable child in order, placement/cancellation phases and public transaction identities, and each child's last exact persisted venue ID, terminal flag and decimal fill quantities. Superseded children remain included. An absent observation leaves attempted or acknowledged placement unresolved. These observations are not a fresh venue reconciliation. The optional owner binds a repeat read to the full original identity.

This read works with the probe off, without a signer and after venue authority has disappeared. It does not bind/rebuild transport, allocate keys, sign, resume, cancel or write storage. Prepared/signed attempts retain their recorded phases. Account, network, provider, key/session or controller lifetime changes across awaits reject the read. Missing handles return unavailable with not_found; a mismatched expected owner returns owner_mismatch. Missing storage never becomes an available empty inventory, and malformed/ambiguous records reject. Returned IDs grant no cleanup authority under another wallet, account, network or key.

Pass the exact OrderResult.orderId as handle, preserve all opaque IDs and reconcile each returned child independently before explicit cleanup for that handle. Aggregated mode routes only the requested providerId. HyperLiquid reports unsupported for this durable read and retains its existing getChaseOrders() behavior. Full Lighter Chase readiness still requires client integration and venue proof; the default-off/testnet gates remain unchanged. TWAP remains separate: Lighter's public TWAP lifecycle read refuses support until authoritative parent fills and terminal semantics are established. Local children do not establish a complete TWAP schedule.

PerpsController:reconcileChaseOrderCancellation({handle, providerId, owner, clientOrderId, cancellation: {nonce, txHash, expiresAt}}) reconciles the latest already attempted cancellation of a stopped handle. Preserve the full original owner and exact latest child/cancellation transaction identity from durable history. Lighter observes venue evidence and persists management settlement under the existing owner lock and session fence. The service receives only observation, clock and session-check functions, so it cannot allocate a nonce, sign or send another cancellation even when an acknowledged cancellation becomes failed and the child remains open. Existing registered read authority may create an authentication client and read nonce metadata, but it never registers a key or submits a transaction.

A proven terminal handle returns settled with its public order; uncertain visibility, missing acknowledgement or failed/lost venue replies return unresolved. A missing original cancellation, active handle, wrong original identity or malformed inventory rejects before venue observation. Providers without this operation return unsupported; aggregated routing never substitutes ordinary cancellation. A lost public reconciliation reply remains unresolved and grants no authority to replay cancelOrder. Ordinary explicit cancellation retains its existing behavior.

getNativeChaseRecords() exposes exact local cleanup identities for diagnostics. cancelOrder({orderType: 'chase', orderId: handle, symbol, providerId: 'lighter'}) explicitly terminates an owned handle. A wrong-symbol request leaves its active duration cleanup intact. Confirmed explicit cleanup succeeds even when the record retains an earlier failure cause and terminal status. suspendChaseOrders() interrupts starts waiting on setup as well as in-flight and scheduled continuation before attempting exact cleanup. This Lighter probe cancels the last child on backgrounding and duration, repricing or distance limits. Confirmed cleanup reports canceled with no resting order ID; the native record retains the first cause in stopReason. Unknown cleanup reports termination_pending. HyperLiquid suspension leaves its child resting. The Lighter timer uses the absolute duration deadline; network latency and suspended execution can delay venue cleanup. Account changes and provider teardown interrupt synchronously. Reconnect exposes interrupted records as termination_pending and never restarts the financial loop. Unknown cancel or lost-response ownership remains visible, including across process restart and original-slot changes; cleanup requires the original available authority and existing recovery-ledger conditions.

Exact order lookup has the venue's limited retention window. Missing retained history or unsafe numeric IDs fail closed and may require separate manual recovery; they do not authorize a replacement. All lifecycle checks above are source behavior. Live post-only crossing, fill/cancel races and bounded reprice behavior require venue acceptance checks.

utils/lighterTwapReconciliation.reconcileLighterTwapObservation reconciles complete native parent/child rows and unaggregated trades against persisted signed intent. identifyLighterTwapParent validates immutable parent ownership without requiring fill reconciliation, so incomplete history does not block exact cancellation. Neither utility proves venue termination. Numeric and string trade IDs must agree; trade provenance allows 30 seconds of clock skew.

Native TWAP and Chase probes share one unresolved-account exclusion, including across provider instances and restarts. Unverified TWAP terminal observations retain that exclusion. The provider class is not a package export, controller registration omits probe constructor options, and observation collection needs a direct Lighter provider rather than the aggregated provider. Probe callers must supply a development integration; these are not enabled product features.

Contributing

This package is part of a monorepo. Instructions for contributing can be found in the monorepo README.

Native resting-order edits

Lighter editOrder modifies the same exact venue order with transaction type 17. It supports price or size changes to verified unfilled ordinary active perpetual limits with GTC or post-only time-in-force. Side, type, time-in-force, reduce-only and expiry stay fixed. Trigger, linked protection and durably recorded Scale, Chase and position-protection children are refused. Partially filled orders are also refused because the pinned signer proof does not establish total-versus- remaining amount semantics for those orders.

Pass unsafe int64 venue IDs as canonical decimal strings. Core decodes raw venue and signed JSON with ordinary string parsing, preserving integer identities without Node reviver extensions or rewriting the signed transaction. It checks the refreshed market grid, minimums and native integer widths before signing, and rereads the target after signing. The pinned seven-position _signModifyOrder tuple has no order-version or resting-expiry argument, so a fill or cancel can still race after that final read.

OrderResult.orderEdit reports requested price and size, a durable status and an optional exact same-order observation. success:true requires exact transaction execution and an unfilled open observation with the requested price, size and remaining size. HTTP acceptance, unchanged fields, missing rows, lookup failures and partial-fill races remain pending. An exactly observed filled or canceled order after proved execution is terminal with success:false; it is not an edit success or a fabricated fill receipt. No cancel/re-place fallback runs.

Core persists unsigned intent before signing and records the exact nonce, hash, expiry and original wallet/network/account/key before dispatch. It never stores signed payloads or keys in the edit journal. After interruption or restart, calling editOrder on that ID reconciles the retained intent using the original registered local key before financial setup. The call returns that prior outcome without submitting another edit, including when the new desired fields differ. Pending records cannot be overwritten by retries. Exact failed or proved-unsent attempts, and unaccepted exact hashes confirmed absent after signed expiry plus clock slack and an exact target reread, permit a subsequent explicit retry. Earlier acceptance or execution proof remains pending through later lookup loss.

This Core contract does not establish venue edit effects, Mobile bridge support or delivered client parity. Mobile adoption and testnet same-order price/size lifecycle proof remain separate validation work.

The Chase probe accepts Mobile's explicit usdAmount only when its downward-grid size agrees with the captured current price and fresh venue quote. The USD amount becomes the durable aggregate budget for original and replacement children, within the 20 USD limit. Preparation is repeated after signer readiness, and existing late dispatch, 1x, opening-only, fill and child-ownership checks remain required.