Skip to content

Commit cf57bcc

Browse files
fix(perps): Resolve 429 errors and improve session performance cp-7.59.0 (#22242)
## **Description** This PR fixes 429 rate limit errors and improves Perps trading performance through two complementary changes: 1. **Dual Transport Architecture**: Separates HTTP and WebSocket transports to match their intended use cases 2. **Session-Based Caching**: Eliminates redundant API calls during user sessions ### What is the reason for the change? **Problem 1: 429 Rate Limit Errors After SDK Upgrade (TAT-1974)** HyperLiquid SDK v0.25.9 defaulted all operations to WebSocket transport, causing 429 errors during normal trading. Write operations (orders, cancellations) were exhausting WebSocket rate limits that should be reserved for real-time subscriptions. **Problem 2: Redundant API Calls (TAT-2022)** Builder fee approval, referral setup, and market metadata were called repeatedly during each session, adding unnecessary latency and rate limit pressure. ### What is the improvement/solution? **Solution 1: Dual Transport Architecture (TAT-1974)** Separated HTTP and WebSocket transports to match their intended use: - **HTTP transport**: Request/response operations (orders, queries, account data) - **WebSocket transport**: Real-time subscriptions only (price feeds, position updates) **Benefits:** - Eliminates 429 errors (separate rate limit pools) - Improved reliability (write operations don't compete with subscriptions) **Solution 2: Session-Based Caching (TAT-2022)** Moved repeated API calls to once-per-session initialization: - **Builder fee approval**: Called once during initialization instead of per-order - **Referral setup**: Non-blocking fire-and-forget pattern - **Market metadata**: Single shared cache for all operations **Benefits:** - Reduced API calls (1 vs many per session) - Lower latency (no per-order network overhead) - Improved reliability (referral failures don't block orders) ## **Changelog** CHANGELOG entry: Fixed 429 rate limit errors by separating HTTP/WebSocket transports and improved Perps trading performance through session-based caching ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/TAT-1974 Fixes: https://consensyssoftware.atlassian.net/browse/TAT-2022 ## **Manual testing steps** ```gherkin Feature: Dual transport architecture and session-based caching for Perps trading Scenario: Write operations use HTTP transport Given user opens Perps and initializes connection When user places multiple orders rapidly Then orders should complete successfully without 429 errors And WebSocket subscriptions should remain active for price updates Scenario: Session caching reduces redundant API calls Given user opens Perps and initializes connection When user places multiple orders Then builder fee approval should only be called once And referral setup should happen in background (non-blocking) And meta responses should be cached and reused When user disconnects or switches account Then caches should be cleared and reinitialized ``` ## **Screenshots/Recordings** Backend performance optimization (no UI changes). https://github.com/user-attachments/assets/6b5014e0-2c3f-41c2-b1ce-69ef4171128d Demo on iOS/Android showing functionality still works correctly. ## **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. ## **Technical Implementation Details** <details> <summary>For reviewers: Implementation overview</summary> ### Files Modified: **Transport Architecture (TAT-1974):** 1. `HyperLiquidClientService.ts` - Dual transport creation and injection 2. `HyperLiquidClientService.test.ts` - Transport configuration tests **Session-Based Caching (TAT-2022):** 3. `HyperLiquidProvider.ts` - Session cache implementation and initialization flow 4. `HyperLiquidProvider.test.ts` - Updated test expectations ### Test Coverage: - **Transport Architecture**: 32 tests passing - **Session Caching**: 244 tests passing - **Total**: 276 tests passing </details> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Splits SDK transport into HTTP (Info/Exchange) and WebSocket (Subscription), adds session-scoped caching for meta, referral, and builder-fee approval, refactors provider to use cached meta across methods, updates tests, and bumps SDK. > > - **Perps Backend (HyperLiquidProvider.ts)** > - Introduces shared `getCachedMeta()` and replaces prior market cache; used in `placeOrder`, `editOrder`, `closePositions`, `getMarkets`, `getMaxLeverage`, `getAvailableHip3Dexs`, etc. > - Adds session caches for referral and builder-fee approval; initializes once in `ensureReady()` and clears on `disconnect()`. > - Refactors market fetching API to `{ dex, skipFilters, skipCache }` and updates internal callers. > - Improves meta validation and error messaging; switches TP/SL/referral to non-blocking behavior where applicable. > - Adds HIP‑3 balance handling helpers and post-order rebalance/rollback flow (uses cached meta and session state). > - **SDK Client Service (HyperLiquidClientService.ts)** > - Implements dual transports: `HttpTransport` for `ExchangeClient`/`InfoClient`, `WebSocketTransport` for `SubscriptionClient`. > - Updates logging and disconnect to close only WebSocket transport; exposes connection state helpers. > - **Tests** > - Updates provider tests for new error messages (e.g., "Invalid meta response"), non-blocking referral, and session-cached builder-fee approval (verifies single approval across orders). > - Updates client service tests to validate dual transports, config, and disconnect behavior. > - **Dependencies** > - Bumps `@nktkas/hyperliquid` to `^0.25.9` (lockfile updated). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit b85d046. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Nicholas Smith <nick.smith@consensys.net>
1 parent 7e7938a commit cf57bcc

File tree

6 files changed

+435
-236
lines changed

6 files changed

+435
-236
lines changed

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

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2616,7 +2616,7 @@ describe('HyperLiquidProvider', () => {
26162616
const result = await provider.updatePositionTPSL(updateParams);
26172617

26182618
expect(result.success).toBe(false);
2619-
expect(result.error).toContain('Failed to fetch market metadata');
2619+
expect(result.error).toContain('Invalid meta response');
26202620
});
26212621

26222622
it('should handle meta response without universe property', async () => {
@@ -2634,7 +2634,7 @@ describe('HyperLiquidProvider', () => {
26342634
const result = await provider.updatePositionTPSL(updateParams);
26352635

26362636
expect(result.success).toBe(false);
2637-
expect(result.error).toContain('Failed to fetch market metadata');
2637+
expect(result.error).toContain('Invalid meta response');
26382638
});
26392639
});
26402640

@@ -4266,20 +4266,27 @@ describe('HyperLiquidProvider', () => {
42664266

42674267
expect(result.success).toBe(true);
42684268

4269-
// Verify builder fee approval was called
4269+
// Builder fee approval is set once during ensureReady() initialization
4270+
// With session caching, it should be called once (during first ensureReady)
42704271
expect(
42714272
mockClientService.getExchangeClient().approveBuilderFee,
42724273
).toHaveBeenCalledWith({
42734274
builder: expect.any(String),
42744275
maxFeeRate: expect.stringContaining('%'),
42754276
});
42764277

4277-
// Verify referral code was set
4278-
expect(
4279-
mockClientService.getExchangeClient().setReferrer,
4280-
).toHaveBeenCalledWith({
4281-
code: expect.any(String),
4282-
});
4278+
// Note: Referral setup is fire-and-forget (non-blocking), so we can't reliably
4279+
// test it synchronously. It's tested separately in dedicated referral tests.
4280+
4281+
// Place a second order to verify caching (should NOT call builder fee approval again)
4282+
const mockExchangeClient = mockClientService.getExchangeClient();
4283+
(mockExchangeClient.approveBuilderFee as jest.Mock).mockClear();
4284+
4285+
const result2 = await provider.placeOrder(orderParams);
4286+
4287+
expect(result2.success).toBe(true);
4288+
// Session cache prevents redundant builder fee approval calls
4289+
expect(mockExchangeClient.approveBuilderFee).not.toHaveBeenCalled();
42834290

42844291
// Verify order was placed with builder fee
42854292
expect(mockClientService.getExchangeClient().order).toHaveBeenCalledWith(
@@ -4450,7 +4457,7 @@ describe('HyperLiquidProvider', () => {
44504457
expect(result.error).toContain('Builder fee approval failed');
44514458
});
44524459

4453-
it('should handle referral code setup failure', async () => {
4460+
it('should handle referral code setup failure (non-blocking)', async () => {
44544461
// Mock builder fee already approved
44554462
mockClientService.getInfoClient = jest
44564463
.fn()
@@ -4483,8 +4490,9 @@ describe('HyperLiquidProvider', () => {
44834490

44844491
const result = await provider.placeOrder(orderParams);
44854492

4486-
expect(result.success).toBe(false);
4487-
expect(result.error).toContain('Error ensuring referral code is set');
4493+
// Referral setup is now non-blocking (fire-and-forget), so order should succeed
4494+
expect(result.success).toBe(true);
4495+
expect(result.orderId).toBeDefined();
44884496
});
44894497

44904498
it('should skip referral setup when referral code is not ready', async () => {
@@ -4574,6 +4582,11 @@ describe('HyperLiquidProvider', () => {
45744582

45754583
it('should properly transform getOrders with reduceOnly and isTrigger fields', async () => {
45764584
mockClientService.getInfoClient = jest.fn().mockReturnValue({
4585+
maxBuilderFee: jest.fn().mockResolvedValue(1),
4586+
referral: jest.fn().mockResolvedValue({
4587+
referrerState: { stage: 'ready', data: { code: 'MMCSI' } },
4588+
referredBy: { code: 'MMCSI' },
4589+
}),
45774590
historicalOrders: jest.fn().mockResolvedValue([
45784591
{
45794592
order: {
@@ -4675,6 +4688,11 @@ describe('HyperLiquidProvider', () => {
46754688

46764689
it('should properly transform getOpenOrders with reduceOnly and isTrigger fields', async () => {
46774690
mockClientService.getInfoClient = jest.fn().mockReturnValue({
4691+
maxBuilderFee: jest.fn().mockResolvedValue(1),
4692+
referral: jest.fn().mockResolvedValue({
4693+
referrerState: { stage: 'ready', data: { code: 'MMCSI' } },
4694+
referredBy: { code: 'MMCSI' },
4695+
}),
46784696
clearinghouseState: jest.fn().mockResolvedValue({
46794697
marginSummary: { totalMarginUsed: '500', accountValue: '10500' },
46804698
withdrawable: '9500',
@@ -4993,6 +5011,11 @@ describe('HyperLiquidProvider', () => {
49935011
},
49945012
]);
49955013
mockClientService.getInfoClient = jest.fn().mockReturnValue({
5014+
maxBuilderFee: jest.fn().mockResolvedValue(1),
5015+
referral: jest.fn().mockResolvedValue({
5016+
referrerState: { stage: 'ready', data: { code: 'MMCSI' } },
5017+
referredBy: { code: 'MMCSI' },
5018+
}),
49965019
frontendOpenOrders: mockFrontendOpenOrders,
49975020
clearinghouseState: jest.fn().mockResolvedValue({
49985021
marginSummary: { totalMarginUsed: '0', accountValue: '1000' },
@@ -5073,6 +5096,11 @@ describe('HyperLiquidProvider', () => {
50735096
]);
50745097
});
50755098
mockClientService.getInfoClient = jest.fn().mockReturnValue({
5099+
maxBuilderFee: jest.fn().mockResolvedValue(1),
5100+
referral: jest.fn().mockResolvedValue({
5101+
referrerState: { stage: 'ready', data: { code: 'MMCSI' } },
5102+
referredBy: { code: 'MMCSI' },
5103+
}),
50765104
frontendOpenOrders: mockFrontendOpenOrders,
50775105
clearinghouseState: jest.fn().mockResolvedValue({
50785106
marginSummary: { totalMarginUsed: '0', accountValue: '1000' },

0 commit comments

Comments
 (0)