Skip to content

Commit 959f015

Browse files
committed
fix(perps): recover HIP-3 DEX discovery on transient failure and guard deposit flow
- HyperLiquidProvider: on transient perpDexs() fetch failure, return [null] without caching so the next #ensureReady() call retries DEX discovery instead of reusing a stale degraded result. Added #dexDiscoveryComplete flag so #ensureReady resets its promise and re-runs #buildAssetMapping until discovery succeeds — trading on main DEX continues uninterrupted in the meantime. Flag is cleared on disconnect for clean reconnection. - usePerpsBalanceTokenFilter: call ensureArbitrumNetworkExists() before depositWithConfirmation() in handlePerpsDepositPress, matching the existing guard in usePerpsHomeActions. Fixes "Invalid chain ID 0xa4b1" for users without Arbitrum configured who tap "Add funds" from the pay-with modal.
1 parent 3fba94e commit 959f015

2 files changed

Lines changed: 46 additions & 17 deletions

File tree

app/components/UI/Perps/hooks/usePerpsBalanceTokenFilter.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { useIsPerpsBalanceSelected } from './useIsPerpsBalanceSelected';
1919
import { usePerpsPaymentToken } from './usePerpsPaymentToken';
2020
import Routes from '../../../../constants/navigation/Routes';
2121
import { usePerpsTrading } from './usePerpsTrading';
22+
import { usePerpsNetworkManagement } from './usePerpsNetworkManagement';
2223
import { useConfirmNavigation } from '../../../Views/confirmations/hooks/useConfirmNavigation';
2324

2425
/** URI for the perps balance token icon, shared with PerpsPayRow and pay-with modal. */
@@ -45,18 +46,28 @@ export function usePerpsBalanceTokenFilter(): (
4546
const formatFiat = useFiatFormatter({ currency: 'usd' });
4647

4748
const { depositWithConfirmation } = usePerpsTrading();
49+
const { ensureArbitrumNetworkExists } = usePerpsNetworkManagement();
4850
const { navigateToConfirmation } = useConfirmNavigation();
4951

5052
const isPerpsDepositAndOrder = hasTransactionType(transactionMeta, [
5153
TransactionType.perpsDepositAndOrder,
5254
]);
5355

5456
const handlePerpsDepositPress = useCallback(() => {
55-
navigateToConfirmation({ stack: Routes.PERPS.ROOT });
56-
depositWithConfirmation().catch(() => {
57-
// Deposit flow handles errors (e.g. user rejection).
58-
});
59-
}, [navigateToConfirmation, depositWithConfirmation]);
57+
ensureArbitrumNetworkExists()
58+
.then(() => {
59+
navigateToConfirmation({ stack: Routes.PERPS.ROOT });
60+
return depositWithConfirmation();
61+
})
62+
.catch((_err) => {
63+
// Deposit flow handles errors (e.g. user rejection or missing network).
64+
// ensureArbitrumNetworkExists errors are logged inside the hook itself.
65+
});
66+
}, [
67+
ensureArbitrumNetworkExists,
68+
navigateToConfirmation,
69+
depositWithConfirmation,
70+
]);
6071

6172
const { onPaymentTokenChange: onPerpsPaymentTokenChange } =
6273
usePerpsPaymentToken();

app/controllers/perps/providers/HyperLiquidProvider.ts

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,11 @@ export class HyperLiquidProvider implements PerpsProvider {
317317

318318
#cachedAllPerpDexs: ({ name: string } | null)[] | null = null;
319319

320+
// True once DEX discovery has succeeded with real data (not a fallback).
321+
// When false, #ensureReadyPromise is reset after each init so the next
322+
// caller retries DEX discovery instead of reusing a degraded mapping.
323+
#dexDiscoveryComplete = false;
324+
320325
// Pending promise to deduplicate concurrent getValidatedDexs() calls
321326
#pendingValidatedDexsPromise: Promise<(string | null)[]> | null = null;
322327

@@ -698,8 +703,8 @@ export class HyperLiquidProvider implements PerpsProvider {
698703
// Verify clients are properly initialized
699704
this.#clientService.ensureInitialized();
700705

701-
// Build asset mapping on first call only (flags are immutable)
702-
if (this.#symbolToAssetId.size === 0) {
706+
// Build asset mapping on first call, or retry if DEX discovery previously failed
707+
if (this.#symbolToAssetId.size === 0 || !this.#dexDiscoveryComplete) {
703708
this.#deps.debugLogger.log(
704709
'HyperLiquidProvider: Building asset mapping',
705710
{
@@ -717,8 +722,15 @@ export class HyperLiquidProvider implements PerpsProvider {
717722
})();
718723

719724
// Await initialization - keep the promise so subsequent calls resolve immediately
720-
// The promise is only reset in disconnect() for clean reconnection
725+
// The promise is only reset in disconnect() for clean reconnection,
726+
// or when DEX discovery was degraded so the next caller retries.
721727
await this.#ensureReadyPromise;
728+
if (!this.#dexDiscoveryComplete) {
729+
// DEX discovery failed transiently — reset so next call retries.
730+
// Trading still works (main DEX mapping is populated), but HIP-3 markets
731+
// will be re-discovered on the next #ensureReady() call.
732+
this.#ensureReadyPromise = null;
733+
}
722734
this.#deps.debugLogger.log('[ensureReady] Initialization complete');
723735
}
724736

@@ -1041,25 +1053,23 @@ export class HyperLiquidProvider implements PerpsProvider {
10411053
ensureError(error, 'HyperLiquidProvider.fetchValidatedDexsInternal'),
10421054
this.#getErrorContext('getValidatedDexs.perpDexs'),
10431055
);
1044-
this.#cachedAllPerpDexs = [null];
1045-
this.#cachedValidatedDexs = [null];
1046-
return this.#cachedValidatedDexs;
1056+
// Do not cache — transient error, allow retry on next call
1057+
return [null];
10471058
}
10481059

1049-
// Cache for buildAssetMapping() to avoid duplicate call
1050-
this.#cachedAllPerpDexs = allDexs;
1051-
10521060
// Validate API response
10531061
if (!allDexs || !Array.isArray(allDexs)) {
10541062
this.#deps.debugLogger.log(
10551063
'HyperLiquidProvider: Failed to fetch DEX list (invalid response), falling back to main DEX only',
10561064
{ allDexs },
10571065
);
1058-
this.#cachedAllPerpDexs = [null];
1059-
this.#cachedValidatedDexs = [null];
1060-
return this.#cachedValidatedDexs;
1066+
// Do not cache — may be transient, allow retry on next call
1067+
return [null];
10611068
}
10621069

1070+
// Cache for buildAssetMapping() to avoid duplicate call
1071+
this.#cachedAllPerpDexs = allDexs;
1072+
10631073
// Extract HIP-3 DEX names (filter out null which represents main DEX)
10641074
const availableHip3Dexs: string[] = [];
10651075
allDexs.forEach((dex) => {
@@ -1943,6 +1953,13 @@ export class HyperLiquidProvider implements PerpsProvider {
19431953
let dexsToMap: (string | null)[];
19441954
try {
19451955
dexsToMap = await this.#getValidatedDexs();
1956+
// Mark DEX discovery as complete only when we got real data (not a fallback).
1957+
// #cachedValidatedDexs is null when #fetchValidatedDexsInternal returned [null]
1958+
// without caching (transient failure) — in that case we stay incomplete so
1959+
// #ensureReady resets its promise and retries on the next call.
1960+
if (this.#cachedValidatedDexs !== null) {
1961+
this.#dexDiscoveryComplete = true;
1962+
}
19461963
} catch (dexError) {
19471964
// If getValidatedDexs fails, fall back to main DEX only to keep the provider
19481965
// functional. Without this, a transient perpDexs() failure would permanently
@@ -7318,6 +7335,7 @@ export class HyperLiquidProvider implements PerpsProvider {
73187335
this.#cachedMetaByDex.clear();
73197336
this.#cachedSpotMeta = null;
73207337
this.#perpDexsCache = { data: null, timestamp: 0 };
7338+
this.#dexDiscoveryComplete = false;
73217339

73227340
// Await pending initialization before clearing to prevent the IIFE from
73237341
// setting clientsInitialized = true after disconnect completes

0 commit comments

Comments
 (0)