Skip to content

Commit 54bf150

Browse files
committed
fix: add meta() fallback when metaAndAssetCtxs returns null meta
1 parent dc3df2a commit 54bf150

2 files changed

Lines changed: 211 additions & 6 deletions

File tree

app/components/UI/Perps/controllers/providers/HyperLiquidProvider.test.ts

Lines changed: 140 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2968,7 +2968,7 @@ describe('HyperLiquidProvider', () => {
29682968

29692969
describe('Additional Error Handling and Edge Cases', () => {
29702970
describe('ensureReady and buildAssetMapping', () => {
2971-
it('should handle meta fetch failure in buildAssetMapping', async () => {
2971+
it('should handle meta fetch failure in buildAssetMapping when both endpoints fail', async () => {
29722972
// Create a fresh provider to test buildAssetMapping
29732973
const freshProvider = new HyperLiquidProvider();
29742974

@@ -3001,6 +3001,74 @@ describe('HyperLiquidProvider', () => {
30013001
expect(result.error).toContain('Network timeout');
30023002
});
30033003

3004+
it('should fallback to meta() in buildAssetMapping when metaAndAssetCtxs returns null meta', async () => {
3005+
// Create a fresh provider to test buildAssetMapping fallback
3006+
const freshProvider = new HyperLiquidProvider();
3007+
3008+
const mockMeta = {
3009+
universe: [
3010+
{ name: 'BTC', szDecimals: 3, maxLeverage: 50 },
3011+
{ name: 'ETH', szDecimals: 4, maxLeverage: 50 },
3012+
],
3013+
};
3014+
3015+
const mockMetaFn = jest.fn().mockResolvedValue(mockMeta);
3016+
3017+
mockClientService.getInfoClient = jest.fn().mockReturnValue(
3018+
createMockInfoClient({
3019+
// meta() returns valid data as fallback
3020+
meta: mockMetaFn,
3021+
// metaAndAssetCtxs returns null meta (simulating partial failure)
3022+
metaAndAssetCtxs: jest.fn().mockResolvedValue([null, []]),
3023+
allMids: jest.fn().mockResolvedValue({ BTC: '50000', ETH: '3000' }),
3024+
}),
3025+
);
3026+
3027+
MockedHyperLiquidClientService.mockImplementation(
3028+
() => mockClientService,
3029+
);
3030+
3031+
// Trigger ensureReady -> buildAssetMapping by calling getMarkets
3032+
const markets = await freshProvider.getMarkets();
3033+
3034+
// Should have used the meta() fallback
3035+
expect(mockMetaFn).toHaveBeenCalled();
3036+
// Should return the markets from the fallback
3037+
expect(markets.length).toBeGreaterThan(0);
3038+
});
3039+
3040+
it('should handle buildAssetMapping when meta() fallback also fails', async () => {
3041+
// Create a fresh provider to test buildAssetMapping
3042+
const freshProvider = new HyperLiquidProvider();
3043+
3044+
mockClientService.getInfoClient = jest.fn().mockReturnValue(
3045+
createMockInfoClient({
3046+
// meta() also fails
3047+
meta: jest.fn().mockRejectedValue(new Error('Fallback failed')),
3048+
// metaAndAssetCtxs returns null meta
3049+
metaAndAssetCtxs: jest.fn().mockResolvedValue([null, []]),
3050+
}),
3051+
);
3052+
3053+
MockedHyperLiquidClientService.mockImplementation(
3054+
() => mockClientService,
3055+
);
3056+
3057+
// Try to place an order which will trigger ensureReady -> buildAssetMapping
3058+
const orderParams: OrderParams = {
3059+
coin: 'BTC',
3060+
isBuy: true,
3061+
size: '0.1',
3062+
orderType: 'market',
3063+
currentPrice: 50000,
3064+
};
3065+
3066+
const result = await freshProvider.placeOrder(orderParams);
3067+
3068+
// Should fail because asset mapping couldn't be built
3069+
expect(result.success).toBe(false);
3070+
});
3071+
30043072
it('should handle string response from meta endpoint', async () => {
30053073
// Test updatePositionTPSL with string meta response (invalid data type)
30063074
mockClientService.getInfoClient = jest.fn().mockReturnValue(
@@ -3311,7 +3379,7 @@ describe('HyperLiquidProvider', () => {
33113379
});
33123380

33133381
describe('getMarketDataWithPrices error scenarios', () => {
3314-
it('should handle missing perpsMeta', async () => {
3382+
it('should handle missing perpsMeta when both metaAndAssetCtxs and meta() return null', async () => {
33153383
mockClientService.getInfoClient = jest.fn().mockReturnValue(
33163384
createMockInfoClient({
33173385
meta: jest.fn().mockResolvedValue(null),
@@ -3326,6 +3394,76 @@ describe('HyperLiquidProvider', () => {
33263394
);
33273395
});
33283396

3397+
it('should fallback to meta() when metaAndAssetCtxs returns null meta', async () => {
3398+
const mockMeta = {
3399+
universe: [{ name: 'BTC', szDecimals: 3, maxLeverage: 50 }],
3400+
};
3401+
3402+
mockClientService.getInfoClient = jest.fn().mockReturnValue(
3403+
createMockInfoClient({
3404+
// meta() returns valid data as fallback
3405+
meta: jest.fn().mockResolvedValue(mockMeta),
3406+
allMids: jest.fn().mockResolvedValue({ BTC: '50000' }),
3407+
predictedFundings: jest.fn().mockResolvedValue([]),
3408+
// metaAndAssetCtxs returns null meta (simulating partial failure)
3409+
metaAndAssetCtxs: jest.fn().mockResolvedValue([null, []]),
3410+
}),
3411+
);
3412+
3413+
// Create fresh provider to avoid cached state from other tests
3414+
const freshProvider = new HyperLiquidProvider();
3415+
3416+
// Should succeed by falling back to meta()
3417+
const result = await freshProvider.getMarketDataWithPrices();
3418+
expect(Array.isArray(result)).toBe(true);
3419+
expect(result.length).toBeGreaterThan(0);
3420+
expect(result[0].symbol).toBe('BTC');
3421+
});
3422+
3423+
it('should fallback to meta() when metaAndAssetCtxs returns undefined meta', async () => {
3424+
const mockMeta = {
3425+
universe: [{ name: 'ETH', szDecimals: 4, maxLeverage: 50 }],
3426+
};
3427+
3428+
mockClientService.getInfoClient = jest.fn().mockReturnValue(
3429+
createMockInfoClient({
3430+
// meta() returns valid data as fallback
3431+
meta: jest.fn().mockResolvedValue(mockMeta),
3432+
allMids: jest.fn().mockResolvedValue({ ETH: '3000' }),
3433+
predictedFundings: jest.fn().mockResolvedValue([]),
3434+
// metaAndAssetCtxs returns undefined (simulating malformed response)
3435+
metaAndAssetCtxs: jest.fn().mockResolvedValue([undefined, []]),
3436+
}),
3437+
);
3438+
3439+
// Create fresh provider to avoid cached state from other tests
3440+
const freshProvider = new HyperLiquidProvider();
3441+
3442+
// Should succeed by falling back to meta()
3443+
const result = await freshProvider.getMarketDataWithPrices();
3444+
expect(Array.isArray(result)).toBe(true);
3445+
expect(result.length).toBeGreaterThan(0);
3446+
expect(result[0].symbol).toBe('ETH');
3447+
});
3448+
3449+
it('should handle meta() fallback failure gracefully', async () => {
3450+
mockClientService.getInfoClient = jest.fn().mockReturnValue(
3451+
createMockInfoClient({
3452+
// meta() also fails
3453+
meta: jest.fn().mockRejectedValue(new Error('Network timeout')),
3454+
allMids: jest.fn().mockResolvedValue({ BTC: '50000' }),
3455+
predictedFundings: jest.fn().mockResolvedValue([]),
3456+
// metaAndAssetCtxs returns null meta
3457+
metaAndAssetCtxs: jest.fn().mockResolvedValue([null, []]),
3458+
}),
3459+
);
3460+
3461+
// Should still throw the "no markets available" error
3462+
await expect(provider.getMarketDataWithPrices()).rejects.toThrow(
3463+
'Failed to fetch market data - no markets available',
3464+
);
3465+
});
3466+
33293467
it('should handle missing allMids', async () => {
33303468
// Set up mock BEFORE creating fresh provider
33313469
mockClientService.getInfoClient = jest.fn().mockReturnValue(

app/components/UI/Perps/controllers/providers/HyperLiquidProvider.ts

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1483,9 +1483,41 @@ export class HyperLiquidProvider implements IPerpsProvider {
14831483
const dexParam = dex || undefined;
14841484
return infoClient
14851485
.metaAndAssetCtxs(dexParam ? { dex: dexParam } : undefined)
1486-
.then((result) => {
1487-
const meta = result?.[0] || null;
1486+
.then(async (result) => {
1487+
let meta = result?.[0] || null;
14881488
const assetCtxs = result?.[1] || [];
1489+
1490+
// FALLBACK: If combined endpoint returns null/empty meta, try dedicated meta endpoint
1491+
if (!meta?.universe) {
1492+
Logger.error(
1493+
new Error(
1494+
'metaAndAssetCtxs returned null meta, attempting fallback to meta()',
1495+
),
1496+
this.getErrorContext('buildAssetMapping.metaFallback', {
1497+
dex: dex ?? 'main',
1498+
hasMetaAndCtxsResponse: !!result,
1499+
hasMetaAtIndex0: !!result?.[0],
1500+
assetCtxsLength: assetCtxs?.length ?? 0,
1501+
}),
1502+
);
1503+
try {
1504+
meta = await infoClient.meta(
1505+
dexParam ? { dex: dexParam } : undefined,
1506+
);
1507+
if (meta?.universe) {
1508+
DevLogger.log(
1509+
`[buildAssetMapping] meta() fallback succeeded for ${dex || 'main'}`,
1510+
{ universeSize: meta.universe.length },
1511+
);
1512+
}
1513+
} catch (fallbackError) {
1514+
DevLogger.log(
1515+
`[buildAssetMapping] meta() fallback also failed for ${dex || 'main'}`,
1516+
fallbackError,
1517+
);
1518+
}
1519+
}
1520+
14891521
// Cache meta for later use by getCachedMeta
14901522
if (meta?.universe) {
14911523
this.cachedMetaByDex.set(dexKey, meta);
@@ -1494,7 +1526,10 @@ export class HyperLiquidProvider implements IPerpsProvider {
14941526
// Cache assetCtxs for getMarketDataWithPrices (avoids duplicate metaAndAssetCtxs calls)
14951527
this.subscriptionService.setDexAssetCtxsCache(dexKey, assetCtxs);
14961528
}
1497-
return { dex, meta, success: true as const };
1529+
1530+
// Return success only if we have valid meta with universe
1531+
const success = !!meta?.universe;
1532+
return { dex, meta, success: success as typeof success };
14981533
})
14991534
.catch((error) => {
15001535
DevLogger.log(
@@ -4894,6 +4929,37 @@ export class HyperLiquidProvider implements IPerpsProvider {
48944929
meta = metaAndCtxs?.[0] || null;
48954930
assetCtxs = metaAndCtxs?.[1] || [];
48964931

4932+
// FALLBACK: If combined endpoint returns null/empty meta, try dedicated meta endpoint
4933+
if (!meta?.universe) {
4934+
Logger.error(
4935+
new Error(
4936+
'metaAndAssetCtxs returned null meta, attempting fallback to meta()',
4937+
),
4938+
this.getErrorContext('getMarketDataWithPrices.metaFallback', {
4939+
dex: dex ?? 'main',
4940+
hasMetaAndCtxsResponse: !!metaAndCtxs,
4941+
hasMetaAtIndex0: !!metaAndCtxs?.[0],
4942+
assetCtxsLength: assetCtxs?.length ?? 0,
4943+
}),
4944+
);
4945+
try {
4946+
meta = await infoClient.meta(
4947+
dexParam ? { dex: dexParam } : undefined,
4948+
);
4949+
if (meta?.universe) {
4950+
DevLogger.log(
4951+
`[getMarketDataWithPrices] meta() fallback succeeded for ${dex || 'main'}`,
4952+
{ universeSize: meta.universe.length },
4953+
);
4954+
}
4955+
} catch (fallbackError) {
4956+
DevLogger.log(
4957+
`[getMarketDataWithPrices] meta() fallback also failed for ${dex || 'main'}`,
4958+
fallbackError,
4959+
);
4960+
}
4961+
}
4962+
48974963
// IMPORTANT: Populate cache for buildAssetMapping and other methods to reuse
48984964
if (meta?.universe) {
48994965
this.cachedMetaByDex.set(dexKey, meta);
@@ -4912,12 +4978,13 @@ export class HyperLiquidProvider implements IPerpsProvider {
49124978
dexParam ? { dex: dexParam } : undefined,
49134979
);
49144980

4981+
// Return success only if we have valid meta with universe
49154982
return {
49164983
dex,
49174984
meta,
49184985
assetCtxs,
49194986
allMids: dexAllMids || {},
4920-
success: true,
4987+
success: !!meta?.universe,
49214988
};
49224989
} catch (error) {
49234990
Logger.error(

0 commit comments

Comments
 (0)