From bfade639d6a01fd486e4c264fee04caefa9f6683 Mon Sep 17 00:00:00 2001 From: "Samuel EF. Tinnerholm" Date: Mon, 22 Jun 2026 14:15:07 +0200 Subject: [PATCH] fix(hyperliquid): fetchBalance reads spot account, drops hardcoded USDH label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fetchBalance previously read crossMarginSummary (perp margin account) and labelled the result currency as 'USDH' regardless of what tokens the user actually held. This silently hid every spot balance — outcome markets quote against USDC on the spot account, so users with deposited funds saw 'USDH: 0' and assumed empty. Now reads spotClearinghouseState alongside the perp account: - Spot balances surface with their real currency (USDC, USDH, etc.), one Balance entry per non-zero coin. - Perp account, when funded, surfaces as 'USDC_PERP' so consumers can tell it apart from spot. Verified locally: wallet 0xcb85… ($14.90 USDC deposited on HL spot) now correctly returns USDC: total=14.9 avail=14.9 instead of USDH: 0. --- core/src/exchanges/hyperliquid/fetcher.ts | 20 ++++++++++ core/src/exchanges/hyperliquid/index.ts | 7 +++- core/src/exchanges/hyperliquid/normalizer.ts | 40 +++++++++++++++----- 3 files changed, 55 insertions(+), 12 deletions(-) diff --git a/core/src/exchanges/hyperliquid/fetcher.ts b/core/src/exchanges/hyperliquid/fetcher.ts index 036952b2..ca9329a6 100644 --- a/core/src/exchanges/hyperliquid/fetcher.ts +++ b/core/src/exchanges/hyperliquid/fetcher.ts @@ -142,6 +142,19 @@ export interface HyperliquidRawUserState { withdrawable: string; } +// Spot balance entry — coin/token + total/hold/entryNtl in token units (already human-readable). +export interface HyperliquidRawSpotBalance { + coin: string; + token: number; + total: string; + hold: string; + entryNtl: string; +} + +export interface HyperliquidRawSpotState { + balances: HyperliquidRawSpotBalance[]; +} + // Per-coin context from spotMetaAndAssetCtxs (only the fields we use) export interface HyperliquidRawSpotAssetCtx { coin: string; // "#NNNN" for outcome legs @@ -334,6 +347,13 @@ export class HyperliquidFetcher implements IExchangeFetcher { + return this.postInfo({ + type: 'spotClearinghouseState', + user: walletAddress, + }); + } + async fetchRawUserState(walletAddress: string): Promise { return this.postInfo({ type: 'clearinghouseState', diff --git a/core/src/exchanges/hyperliquid/index.ts b/core/src/exchanges/hyperliquid/index.ts index b4cd04c3..7e388677 100644 --- a/core/src/exchanges/hyperliquid/index.ts +++ b/core/src/exchanges/hyperliquid/index.ts @@ -171,8 +171,11 @@ export class HyperliquidExchange extends PredictionMarketExchange { async fetchBalance(): Promise { const wallet = this.requireWallet(); - const raw = await this.fetcher.fetchRawUserState(wallet); - return this.normalizer.normalizeBalance(raw); + const [perp, spot] = await Promise.all([ + this.fetcher.fetchRawUserState(wallet), + this.fetcher.fetchRawSpotState(wallet), + ]); + return this.normalizer.normalizeBalance(perp, spot); } async fetchPositions(): Promise { diff --git a/core/src/exchanges/hyperliquid/normalizer.ts b/core/src/exchanges/hyperliquid/normalizer.ts index 906191c6..5ccfe0de 100644 --- a/core/src/exchanges/hyperliquid/normalizer.ts +++ b/core/src/exchanges/hyperliquid/normalizer.ts @@ -26,6 +26,7 @@ import { HyperliquidRawOpenOrder, HyperliquidRawPosition, HyperliquidRawUserState, + HyperliquidRawSpotState, HyperliquidRawOutcomeMeta, HyperliquidRawMid, } from './fetcher'; @@ -459,17 +460,36 @@ export class HyperliquidNormalizer implements IExchangeNormalizer 0) { + result.push({ + currency: 'USDC_PERP', + total: perpTotal, + available: perpTotal - perpLocked, + locked: perpLocked, + }); + } + + return result; } // -- Private helpers -------------------------------------------------------