From ab8d7d12bc8c8e63b0f62bda83a65f3e3b03c046 Mon Sep 17 00:00:00 2001 From: abretonc7s <107169956+abretonc7s@users.noreply.github.com> Date: Tue, 24 Mar 2026 11:50:24 +0000 Subject: [PATCH] chore(runway): cherry-pick fix(perps): fix HIP-3 asset ID lookup failure from dual-cache desync cp-7.70.1 (#27854) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Fix HIP-3 asset ID lookup failure (`"Asset ID not found for xyz:BRENTOIL"`) that blocked trading on HIP-3 markets when navigating via the old Perps tab layout. **Root cause**: Dual-cache desync between `#cachedValidatedDexs` (string DEX names) and `#cachedAllPerpDexs` (raw API objects for `perpDexIndex` computation). The standalone preload path (`#getStandaloneValidatedDexs`) populated one cache but not the other. When `#buildAssetMapping` later ran, it found "xyz" in `dexsToMap` but couldn't compute its `perpDexIndex` because `#cachedAllPerpDexs` was null. **Why old Perps tab vs new Homepage Sections**: Both layouts sit inside `Wallet/index.tsx`, which calls `startMarketDataPreload()` on mount. This fires standalone HTTP calls that populate `#cachedValidatedDexs` but not `#cachedAllPerpDexs`. - **New homepage sections**: `PerpsSectionWithProvider` mounts immediately. Stream hooks fire `ensureReady()` before or concurrently with the standalone preload. Since `#cachedValidatedDexs` is often still null, `fetchValidatedDexsInternal` runs fresh and sets **both** caches correctly. - **Old tab layout**: The Perps tab doesn't mount until the user taps it. By that time, `startMarketDataPreload()` has already completed → `#cachedValidatedDexs` is populated by standalone. When the tab mounts → `getValidatedDexs()` → **cache hit** → `fetchValidatedDexsInternal` is never called → `#cachedAllPerpDexs` stays null → `buildAssetMapping` can't find "xyz". **Changes (1 file, 3 sites)**: 1. **Root cause fix**: `#getStandaloneValidatedDexs` now sets `this.#cachedAllPerpDexs = allDexs` after a successful `perpDexs()` call, keeping both caches in sync. 2. **Cache poisoning fix**: Removed `this.#cachedAllPerpDexs = this.#cachedAllPerpDexs ?? [null]` from the catch block in `#buildAssetMapping`. 3. **Cache poisoning fix**: Replaced persistent `if (!cache) { cache = [null] }` with local `const allPerpDexs = cache ?? [null]` — consumers read the cache, only the owner writes it. ## **Changelog** CHANGELOG entry: Fixed a bug where closing positions on HIP-3 markets (e.g., xyz:BRENTOIL) failed with "Asset ID not found" when navigating via the Perps tab ## **Related issues** Fixes: HIP-3 asset ID lookup failure on old Perps tab layout ## **Manual testing steps** ```gherkin Feature: HIP-3 position management via Perps tab Scenario: user closes a HIP-3 position from the old Perps tab Given user has an open position on a HIP-3 market (e.g., xyz:BRENTOIL) And user is using the old tab layout (homepage redesign v1 disabled) When user navigates to the Perps tab And user taps close on the xyz:BRENTOIL position Then the position closes successfully without "Asset ID not found" error Scenario: user opens a HIP-3 position from the old Perps tab Given user is on the Perps tab (old layout) When user navigates to xyz:BRENTOIL market and places a market order Then the order executes successfully with correct asset ID routing ``` ## **Screenshots/Recordings** ### **Before** Metro logs show the desync: ``` getValidatedDexs CACHE HIT {"cachedAllNull": true, "dexs": [null, "xyz"]} buildAssetMapping state {"allPerpDexsLen": 1, "cachedAllNull": true} Could not find perpDexIndex for DEX xyz Asset ID not found for xyz:BRENTOIL ``` ### **After** Metro logs show both caches in sync: ``` buildAssetMapping state {"allPerpDexsLen": 8, "cachedAllNull": false, "dexsToMap": [null, "xyz"]} Asset map state at order time {"assetExistsInMap": true, "hip3AssetsCount": 54, "totalAssetsInMap": 283} Resolved DEX-specific asset ID {"assetId": 110049, "coin": "xyz:BRENTOIL"} usePerpsClosePosition: Close result {"success": true, "orderId": "359617825254"} ``` ## **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. --- > [!NOTE] > **Medium Risk** > Touches HIP-3 market routing/asset-ID mapping in `HyperLiquidProvider`, so a mistake could break trading on some perps markets; scope is small and localized to cache population/fallback behavior. > > **Overview** > Fixes a HIP-3 asset mapping failure where `#cachedValidatedDexs` could be populated via the standalone preload path while `#cachedAllPerpDexs` stayed `null`, leading to missing `perpDexIndex` during `#buildAssetMapping`. > > `#getStandaloneValidatedDexs()` now also populates `#cachedAllPerpDexs` after a successful `perpDexs()` call, and `#buildAssetMapping()` no longer “poisons” the shared cache with a persistent `[null]` fallback (it uses a local fallback instead). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit c925609ab6e324afaf50556d96abf4acca2460ee. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../perps/providers/HyperLiquidProvider.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/app/controllers/perps/providers/HyperLiquidProvider.ts b/app/controllers/perps/providers/HyperLiquidProvider.ts index 764f4e42ea1..480ac72df40 100644 --- a/app/controllers/perps/providers/HyperLiquidProvider.ts +++ b/app/controllers/perps/providers/HyperLiquidProvider.ts @@ -2119,16 +2119,13 @@ export class HyperLiquidProvider implements PerpsProvider { '[buildAssetMapping] getValidatedDexs failed, falling back to main DEX', { error: String(dexError) }, ); - this.#cachedAllPerpDexs = this.#cachedAllPerpDexs ?? [null]; dexsToMap = [null]; } - // Use cached perpDexs array (populated by getValidatedDexs) - // Defensive: ensure non-null even if getValidatedDexs had an unexpected issue - if (!this.#cachedAllPerpDexs) { - this.#cachedAllPerpDexs = [null]; - } - const allPerpDexs = this.#cachedAllPerpDexs; + // Local fallback only — never write [null] into #cachedAllPerpDexs here. + // That cache is owned exclusively by #fetchValidatedDexsInternal; writing a + // fallback here would prevent subsequent callers from retrying perpDexs(). + const allPerpDexs = this.#cachedAllPerpDexs ?? [null]; this.#deps.debugLogger.log( 'HyperLiquidProvider: Starting asset mapping rebuild', @@ -4772,6 +4769,12 @@ export class HyperLiquidProvider implements PerpsProvider { return [null]; } + // Populate #cachedAllPerpDexs so buildAssetMapping can compute perpDexIndex. + // Without this, getValidatedDexs returns from #cachedValidatedDexs (string names) + // but #cachedAllPerpDexs (raw objects for index computation) stays null, + // causing "Could not find perpDexIndex for DEX xyz" failures. + this.#cachedAllPerpDexs = allDexs; + // Extract HIP-3 DEX names (filter out null which represents main DEX) const availableHip3Dexs: string[] = []; allDexs.forEach((dex) => {