feat(perps): sync PerpsController implementation for npm publishing#7941
feat(perps): sync PerpsController implementation for npm publishing#7941abretonc7s wants to merge 20 commits intomainfrom
Conversation
Add all required dependencies to perps-controller package.json (account-tree-controller, bridge-controller, keyring-controller, network-controller, transaction-controller, etc.) and corresponding tsconfig.build.json project references so the package builds correctly when source files are synced from mobile.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
Caution MetaMask internal reviewing guidelines:
Ignoring alerts on:
|
Synced from mobile PR #26110: - toggleTestnet: check InitializationState.Failed after init() and rollback isTestnet on failure (matching switchProvider pattern) - depositWithConfirmation: replace never-resolving promise with Promise.resolve(transactionMeta.id) when placeOrder=true
Hoist currentDepositId before try block so it is accessible in the outer catch. When a pre-submission error occurs (e.g. missing networkClientId), the deposit request is now marked as 'failed' instead of staying permanently 'pending' in state.
Prevent standalone preload from leaking WebSocket providers by caching the HyperLiquidProvider instance across standalone calls and cleaning it up at lifecycle boundaries (init, disconnect, toggleTestnet, etc.). Reorder switchProvider() to check "already active" before validating the providers map, so it returns a no-op success before init().
Add uuidv4() fallback for currentDepositId which TypeScript cannot narrow inside the update() callback after the variable was hoisted to let binding with string | undefined type.
The base tsconfig.json was missing project references that tsconfig.build.json already had, causing TS2345 errors for imports like @metamask/account-tree-controller during type checking.
Jest exits with code 1 when no test files exist. Add a minimal placeholder test in tests/ (outside src/ so the Mobile sync script does not delete it) and scope collectCoverageFrom to that file only, preventing 0% coverage failures on the synced source.
…cription Register all 34 PerpsControllerActions via registerMethodActionHandlers() so inter-controller communication works through the messenger in core. Store the RemoteFeatureFlagController:stateChange subscription handle and clean it up in disconnect() to prevent leaks.
… and account switch - Remove feature-flag unsubscribe from disconnect() so geo-blocking and HIP-3 flag changes keep propagating after reconnect cycles - Add deposit request lifecycle tracking for the deposit+order flow so requests transition from pending to completed/failed/cancelled - Clear cached user data when switching to a non-EVM account group to prevent stale positions/orders from the previous EVM account
|
@SocketSecurity ignore npm/@nktkas/hyperliquid@0.30.3 |
|
@SocketSecurity ignore npm/@inquirer/external-editor@2.0.3 |
|
@SocketSecurity ignore npm/@myx-trade/sdk@0.1.265 |
|
@SocketSecurity ignore npm/@noble/curves@2.0.1 |
|
@SocketSecurity ignore npm/ox@0.12.1 |
|
@SocketSecurity ignore npm/viem@2.45.3 |
|
@SocketSecurity ignore npm/@scure/bip39@1.6.0 |
|
@SocketSecurity ignore npm/rxjs@7.8.2 |
|
@SocketSecurity ignore npm/wretch@2.11.1 |
Add a type import and use it in the test to satisfy both import-x/unambiguous (requires ES module syntax) and @typescript-eslint/no-unused-vars.
|
The lint job seems to be failing (you need to run Also:
I thought we had discussed how this was a bad idea, and that we should migrate the controller fully to core instead? I'm not following what the plan is here. |
packages/perps-controller/src/PerpsController-method-action-types.ts
Outdated
Show resolved
Hide resolved
## **Description** Defensive hardening for edge cases in `PerpsController` surfaced by static analysis on Core PR [#7941](MetaMask/core#7941). None of these bugs are currently triggerable in production — the app works correctly today — but all represent latent issues that could bite under future changes or unusual network conditions. ### What changed **1. `toggleTestnet()` — false success on silent init failure** `performInitialization()` catches errors internally and sets `InitializationState.Failed` instead of throwing. `toggleTestnet()` was not checking for this, so it could return `{ success: true }` even when the network switch failed. This patch: - Adds the same `InitializationState.Failed` check after `await this.init()` - Rolls back `isTestnet` to its previous value on failure **2. `depositWithConfirmation()` — never-resolving promise when `placeOrder=true`** The `placeOrder` path created `new Promise(() => {})` — a promise that can never be GC'd and would hang any consumer who awaits it. Replaced with `Promise.resolve(transactionMeta.id)` for proper fire-and-forget semantics. **3. `depositWithConfirmation()` — failed deposits remain permanently pending** `depositWithConfirmation` adds a pending entry to `state.depositRequests` before validating `networkClientId`. If the validation throws, the deposit request stays `status: 'pending'` forever. This patch marks the deposit request as `failed` in the outer catch when a pre-submission error occurs. **4. Feature flag listener lost after disconnect** `disconnect()` unsubscribed `RemoteFeatureFlagController:stateChange` but no reconnect path re-subscribed. After disconnect → reconnect, geo-blocking and HIP-3 flag changes stopped propagating. The feature-flag subscription is a controller-lifetime concern (not session-lifetime), so we removed the unsubscribe from `disconnect()` entirely — the subscription now lives for the full controller lifecycle. **5. `depositWithConfirmation()` — deposit+order request stays pending forever** When `placeOrder=true`, the `if (!placeOrder)` guard skips the `.then()/.catch()` lifecycle block that transitions the deposit request to `completed`/`failed`. No other handler exists for this path. Added an `else if` branch that attaches lifecycle tracking to `addResult.result` (the real transaction promise) for the deposit+order flow. **6. Non-EVM account switch leaves stale cache** The account-change handler only cleared cached user data when `currentAddress` is truthy. Switching to an account group with no EVM account (e.g., Bitcoin-only) skipped cache clearing, leaving stale positions/orders visible. Restructured the condition so cache is always cleared when the account group changes, with preload guarded separately behind `if (currentAddress)`. ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: bugbot findings from Core PR [#7941](MetaMask/core#7941) ## **Manual testing steps** These are defensive fixes for edge cases not reachable through normal UI flows. Verification is via unit tests. ```gherkin Feature: Perps network toggle resilience Scenario: toggleTestnet reports accurate status when init fails silently Given user is on mainnet with perps initialized When user toggles to testnet and initialization fails internally Then toggleTestnet returns { success: false } And isTestnet is rolled back to its previous value Feature: Perps deposit promise contract Scenario: depositWithConfirmation result promise resolves when placeOrder is true Given user initiates a deposit with placeOrder=true When the transaction is submitted Then the result promise resolves with the transaction ID Feature: Perps deposit request lifecycle Scenario: deposit request is marked failed on pre-submission error Given user initiates a deposit and the deposit request is added as pending When the networkClientId lookup fails Then the deposit request status is updated to 'failed' Scenario: deposit+order request transitions from pending Given user initiates a deposit with placeOrder=true When the transaction completes or fails Then the deposit request status is updated to 'completed' or 'failed' Feature: Feature flag subscription resilience Scenario: geo-blocking flags propagate after disconnect/reconnect Given perps controller is initialized with feature flag subscription When disconnect() is called and controller reconnects Then feature flag changes still propagate correctly Feature: Account switch cache clearing Scenario: switching to non-EVM account clears stale perps cache Given user has cached perps data from an EVM account When user switches to a Bitcoin-only account group Then cached positions, orders, and account state are cleared ``` ## **Screenshots/Recordings** N/A — no UI changes, logic-only hardening. ## **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.
…cy injection Synced from metamask-mobile-delta. Cross-controller access now goes through minimal interfaces defined in PerpsPlatformDependencies rather than direct package imports. Feature controller deps (network, keyring, transaction, authentication, remote-feature-flag, account-tree, rewards) removed from package.json. Remaining deps are stable utilities only.
| limit, | ||
| endTime, | ||
| context: this.#createServiceContext('fetchHistoricalCandles'), | ||
| }); |
There was a problem hiding this comment.
Deposit request tracking silently broken when ID undefined
High Severity
When #depositService.prepareTransaction() returns undefined for currentDepositId, the deposit request gets a fresh UUID via currentDepositId ?? uuidv4() as its id, but this generated UUID is never assigned back to currentDepositId. All subsequent .find() calls that search req.id === currentDepositId compare against undefined, so the request is never found. Deposit lifecycle updates (success, failure, cancellation) are silently lost, leaving the request permanently stuck as 'pending' in persisted state.
Additional Locations (2)
| state.depositInProgress = false; | ||
| state.lastDepositTransactionId = null; | ||
| // Don't set lastDepositResult - no toast needed | ||
| }); |
There was a problem hiding this comment.
Cancelled deposits stay pending in persisted state forever
Medium Severity
When a user cancels a deposit in the non-placeOrder flow, the result.catch handler clears depositInProgress and lastDepositTransactionId but does not update the corresponding deposit request's status from 'pending' to 'cancelled'. Since depositRequests has persist: true, these orphaned pending requests accumulate in persisted storage and can never be resolved through normal UI flows, only through the manual clearPendingTransactionRequests reset action.
| limitPrice?: string; // Limit price (for limit orders) | ||
| orderType?: OrderType; // Market vs limit | ||
| timestamp: number; // When the config was saved (for expiration check) | ||
| }; |
There was a problem hiding this comment.
State type missing selectedPaymentToken in pendingConfig
Medium Severity
The pendingConfig shape in PerpsControllerState does not include selectedPaymentToken, yet savePendingTradeConfiguration spreads a config object containing selectedPaymentToken into pendingConfig, and getPendingTradeConfiguration declares selectedPaymentToken in its return type. The field is stored and retrieved at runtime but is invisible to the type system, meaning consumers relying on the state type will not see it, and type-safe state serialization or validation could silently drop it.
Additional Locations (1)
## **Description** Defensive hardening for edge cases in `PerpsController` surfaced by static analysis on Core PR [#7941](MetaMask/core#7941). None of these bugs are currently triggerable in production — the app works correctly today — but all represent latent issues that could bite under future changes or unusual network conditions. ### What changed **1. `toggleTestnet()` — false success on silent init failure** `performInitialization()` catches errors internally and sets `InitializationState.Failed` instead of throwing. `toggleTestnet()` was not checking for this, so it could return `{ success: true }` even when the network switch failed. This patch: - Adds the same `InitializationState.Failed` check after `await this.init()` - Rolls back `isTestnet` to its previous value on failure **2. `depositWithConfirmation()` — never-resolving promise when `placeOrder=true`** The `placeOrder` path created `new Promise(() => {})` — a promise that can never be GC'd and would hang any consumer who awaits it. Replaced with `Promise.resolve(transactionMeta.id)` for proper fire-and-forget semantics. **3. `depositWithConfirmation()` — failed deposits remain permanently pending** `depositWithConfirmation` adds a pending entry to `state.depositRequests` before validating `networkClientId`. If the validation throws, the deposit request stays `status: 'pending'` forever. This patch marks the deposit request as `failed` in the outer catch when a pre-submission error occurs. **4. Feature flag listener lost after disconnect** `disconnect()` unsubscribed `RemoteFeatureFlagController:stateChange` but no reconnect path re-subscribed. After disconnect → reconnect, geo-blocking and HIP-3 flag changes stopped propagating. The feature-flag subscription is a controller-lifetime concern (not session-lifetime), so we removed the unsubscribe from `disconnect()` entirely — the subscription now lives for the full controller lifecycle. **5. `depositWithConfirmation()` — deposit+order request stays pending forever** When `placeOrder=true`, the `if (!placeOrder)` guard skips the `.then()/.catch()` lifecycle block that transitions the deposit request to `completed`/`failed`. No other handler exists for this path. Added an `else if` branch that attaches lifecycle tracking to `addResult.result` (the real transaction promise) for the deposit+order flow. **6. Non-EVM account switch leaves stale cache** The account-change handler only cleared cached user data when `currentAddress` is truthy. Switching to an account group with no EVM account (e.g., Bitcoin-only) skipped cache clearing, leaving stale positions/orders visible. Restructured the condition so cache is always cleared when the account group changes, with preload guarded separately behind `if (currentAddress)`. ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: bugbot findings from Core PR [#7941](MetaMask/core#7941) ## **Manual testing steps** These are defensive fixes for edge cases not reachable through normal UI flows. Verification is via unit tests. ```gherkin Feature: Perps network toggle resilience Scenario: toggleTestnet reports accurate status when init fails silently Given user is on mainnet with perps initialized When user toggles to testnet and initialization fails internally Then toggleTestnet returns { success: false } And isTestnet is rolled back to its previous value Feature: Perps deposit promise contract Scenario: depositWithConfirmation result promise resolves when placeOrder is true Given user initiates a deposit with placeOrder=true When the transaction is submitted Then the result promise resolves with the transaction ID Feature: Perps deposit request lifecycle Scenario: deposit request is marked failed on pre-submission error Given user initiates a deposit and the deposit request is added as pending When the networkClientId lookup fails Then the deposit request status is updated to 'failed' Scenario: deposit+order request transitions from pending Given user initiates a deposit with placeOrder=true When the transaction completes or fails Then the deposit request status is updated to 'completed' or 'failed' Feature: Feature flag subscription resilience Scenario: geo-blocking flags propagate after disconnect/reconnect Given perps controller is initialized with feature flag subscription When disconnect() is called and controller reconnects Then feature flag changes still propagate correctly Feature: Account switch cache clearing Scenario: switching to non-EVM account clears stale perps cache Given user has cached perps data from an EVM account When user switches to a Bitcoin-only account group Then cached positions, orders, and account state are cleared ``` ## **Screenshots/Recordings** N/A — no UI changes, logic-only hardening. ## **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.
…-migration-sync # Conflicts: # packages/perps-controller/package.json # yarn.lock
| state.depositInProgress = false; | ||
| state.lastDepositTransactionId = null; | ||
| // Don't set lastDepositResult - no toast needed | ||
| }); |
There was a problem hiding this comment.
Deposit request stays 'pending' forever on user cancellation
Medium Severity
When the user cancels a deposit transaction in the !placeOrder (submit) path, the result.catch() handler clears depositInProgress and lastDepositTransactionId but does not update the corresponding entry in state.depositRequests. The deposit request created earlier remains with status: 'pending' indefinitely. By contrast, the depositOrderResult .catch() handler correctly transitions the request to 'cancelled' or 'failed'. This inconsistency causes orphaned pending deposit records in the persistent transaction history.
Additional Locations (1)
| limitPrice?: string; // Limit price (for limit orders) | ||
| orderType?: OrderType; // Market vs limit | ||
| timestamp: number; // When the config was saved (for expiration check) | ||
| }; |
There was a problem hiding this comment.
State type missing selectedPaymentToken in pendingConfig
Low Severity
The pendingConfig shape in PerpsControllerState (for both testnet and mainnet) does not include selectedPaymentToken, yet savePendingTradeConfiguration writes it via spread and getPendingTradeConfiguration declares it in its return type. The value exists at runtime but the state type is unaware of it, which means direct state reads, serialization validation, or future state migrations could silently drop it.
Additional Locations (1)
|
|
||
| // Reset initialization state to ensure proper reconnection | ||
| this.isInitialized = false; | ||
| this.#initializationPromise = null; |
There was a problem hiding this comment.
disconnect() leaves initializationState stale in persisted state
Medium Severity
disconnect() resets this.isInitialized and this.#initializationPromise but never updates state.initializationState, leaving it as Initialized even though the controller is disconnected. Since initializationState is marked usedInUi: true, UI consumers reading this state would incorrectly show the controller as connected. By contrast, switchProvider() correctly sets state.initializationState to Uninitialized before re-initializing.
Additional Locations (1)
| [PERPS_EVENT_PROPERTY.WITHDRAWAL_AMOUNT]: | ||
| Number.parseFloat(withdrawalAmount), | ||
| }, | ||
| ); |
There was a problem hiding this comment.
Analytics call inside Immer producer risks aborting state update
Low Severity
this.#getMetrics().trackPerpsEvent() is called inside the this.update() Immer producer in updateWithdrawalStatus. If that call throws, Immer rolls back the entire draft, so the withdrawal status change, success flag, and progress reset are all silently lost. The analytics side effect couples a non-critical operation with a critical state mutation.
| requestToUpdate.status = ( | ||
| isCancellation ? 'cancelled' : 'failed' | ||
| ) as TransactionStatus; | ||
| requestToUpdate.success = false; |
There was a problem hiding this comment.
Invalid 'cancelled' status bypasses TransactionStatus type
Medium Severity
The deposit+order cancellation path sets requestToUpdate.status to 'cancelled' via an as TransactionStatus cast, but TransactionStatus is 'pending' | 'bridging' | 'completed' | 'failed' — it does not include 'cancelled'. This means cancelled deposit requests persist in state with an unrecognized status value. Since depositRequests has persist: true, these ghost records accumulate forever and are never cleaned up by clearPendingTransactionRequests (which only filters 'pending' and 'bridging').
Additional Locations (1)
| limitPrice?: string; // Limit price (for limit orders) | ||
| orderType?: OrderType; // Market vs limit | ||
| timestamp: number; // When the config was saved (for expiration check) | ||
| }; |
There was a problem hiding this comment.
State type omits selectedPaymentToken from pendingConfig
Medium Severity
The pendingConfig shape in PerpsControllerState.tradeConfigurations (both testnet and mainnet) doesn't declare selectedPaymentToken, yet savePendingTradeConfiguration stores it and getPendingTradeConfiguration returns it. Since tradeConfigurations has persist: true, the runtime data shape diverges from the declared type, potentially causing issues with state migration, validation, or any downstream consumer relying on the type definition.
Additional Locations (1)
| providerId: this.state.activeProvider, | ||
| error: `Provider ${providerId} not available`, | ||
| }; | ||
| } |
There was a problem hiding this comment.
switchProvider validates against stale empty providers map
Medium Severity
switchProvider validates this.providers.has(providerId) before calling init(), but init() clears and fully rebuilds the providers map. Before init() is first called, this.providers is empty (constructed as new Map() in the constructor), so switching to 'myx' will always be rejected with "Provider myx not available" even when the MYX feature flag is enabled. The validation checks a stale map that gets discarded by the subsequent init() call.
|
@metamaskbot publish-previews |
|
Preview builds have been published. See these instructions for more information about preview builds. Expand for full list of packages and versions. |
## **Description** Addresses 5 confirmed Bugbot findings from [Core PR #7941](MetaMask/core#7941). ### What changed 1. **Missing 'canceled' spelling** — deposit+order `.catch()` only checked `'cancelled'`, not `'canceled'` 2. **Cancelled deposits stay pending** — non-placeOrder cancellation cleared UI state but left the deposit request as `'pending'` 3. **Stale `activeProviderInstance` after disconnect** — standalone reads could route through a disconnected provider 4. **Disconnect skips preload teardown** — preload timer and messenger subscriptions were not stopped, causing background work during teardown 5. **Analytics side effect inside immer producer** — `trackPerpsEvent()` and `#debugLog()` moved out of `this.update()` ### Disconnect design note Fix 4 inlines the preload cleanup logic from `stopMarketDataPreload()` rather than calling it directly, because `stopMarketDataPreload()` calls `#cleanupStandaloneProvider()` fire-and-forget, while `disconnect()` needs the awaited version to prevent races with reconnection. ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: Bugbot findings on MetaMask/core#7941 ## **Manual testing steps** ```gherkin Feature: Perps deposit cancellation and disconnect cleanup Scenario: user cancels a deposit transaction Given user is on the Perps deposit screen with a pending deposit When user rejects the transaction in the wallet prompt Then the deposit request status should be 'cancelled' (not stuck as 'pending') Scenario: user disconnects from Perps Given user is on Perps screens with active market data preload When user navigates away triggering disconnect Then preload timer is stopped, provider instance is nulled, and no stale subscriptions remain ``` ## **Screenshots/Recordings** N/A — internal controller logic, no UI changes. ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches deposit/withdrawal status transitions and disconnect teardown ordering, which can affect transaction UX, metrics emission, and background polling behavior if regressions occur. > > **Overview** > Fixes several PerpsController lifecycle edge cases around deposits, withdrawals, and teardown. > > Deposit tracking now correctly treats both `cancelled` and `canceled` error strings as user cancellation, and ensures non-order deposit cancellations update the corresponding `depositRequests` entry to `cancelled` instead of leaving it `pending`. > > `updateWithdrawalStatus` no longer performs analytics/logging side-effects inside the `this.update()` immer producer; it computes whether tracking/logging is needed during the state update and runs `trackPerpsEvent`/`#debugLog` afterward. > > `disconnect()` now proactively stops market-data preload intervals and unsubscribes messenger listeners, resets cached preload config, clears `activeProviderInstance` to avoid stale routing, and awaits standalone-provider cleanup. A new unit test asserts preload work does not continue after disconnect. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 330b560. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->


Explanation
Syncs `PerpsController` from Mobile into Core as `@metamask/perps-controller`, updated with controller isolation via platform dependency injection.
What changed in the latest sync (metamask-mobile#26394): Cross-controller access has been refactored from direct package imports to minimal interfaces defined by `PerpsPlatformDependencies`. Feature controller dependencies (network, keyring, transaction, authentication, remote-feature-flag, account-tree, rewards) have been removed from `package.json`. The remaining deps are stable utilities (`@metamask/base-controller`, `@metamask/controller-utils`, `@metamask/utils`, `@metamask/abi-utils`, `@metamask/keyring-api`, `@metamask/messenger`) plus the trading SDKs. This directly addresses Mark Stacey's concern from the ADR-42 review: PerpsController no longer creates obstruction for other teams making cross-package changes in Core, since it pins no feature controller versions.
Approach: Mobile remains the source of truth. A sync script copies controller source to this package. The question of whether Core or an independent repo is the right publishing location is still being resolved with the team — see position doc. This PR keeps the Core path viable. The sync script is a transitional mechanism that goes away when Mobile folds into Core as `apps/mobile`.
Key components:
CI / Testing: A minimal placeholder test (`tests/placeholder.test.ts`) is included to satisfy Core's CI requirement. It lives outside `src/` so the sync script (`rsync --delete` on `src/`) does not remove it. Full unit tests remain in Mobile alongside the source of truth and will be migrated when development moves fully to Core.
References
Checklist
Note
High Risk
Large new controller/provider/services surface that manages trading, deposits/withdrawals, feature flags, and background preloading, increasing risk of logic and integration issues despite being largely additive.
Overview
Syncs in a full-featured
PerpsControllerimplementation (replacing the previous stub), including a large persisted state shape, messenger-exposed methods for trading and account operations, provider initialization/reinit (testnet toggle/provider switching), and background market/user data preloading with cached standalone provider support.Adds multi-provider real-time aggregation via
SubscriptionMultiplexer, plus portable chart and analytics event constants, and updates package dependencies to include the HyperLiquid/MYX SDKs and utilities used by the synced implementation. Test setup is adjusted to only collect coverage from a placeholder test, and the oldPerpsController.test.tsis removed.Written by Cursor Bugbot for commit 567bc78. This will update automatically on new commits. Configure here.