Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions core/src/exchanges/hyperliquid/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -334,6 +347,13 @@ export class HyperliquidFetcher implements IExchangeFetcher<HyperliquidRawOutcom
});
}

async fetchRawSpotState(walletAddress: string): Promise<HyperliquidRawSpotState> {
return this.postInfo<HyperliquidRawSpotState>({
type: 'spotClearinghouseState',
user: walletAddress,
});
}

async fetchRawUserState(walletAddress: string): Promise<HyperliquidRawUserState> {
return this.postInfo<HyperliquidRawUserState>({
type: 'clearinghouseState',
Expand Down
7 changes: 5 additions & 2 deletions core/src/exchanges/hyperliquid/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,11 @@ export class HyperliquidExchange extends PredictionMarketExchange {

async fetchBalance(): Promise<Balance[]> {
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<Position[]> {
Expand Down
40 changes: 30 additions & 10 deletions core/src/exchanges/hyperliquid/normalizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
HyperliquidRawOpenOrder,
HyperliquidRawPosition,
HyperliquidRawUserState,
HyperliquidRawSpotState,
HyperliquidRawOutcomeMeta,
HyperliquidRawMid,
} from './fetcher';
Expand Down Expand Up @@ -459,17 +460,36 @@ export class HyperliquidNormalizer implements IExchangeNormalizer<HyperliquidRaw
};
}

normalizeBalance(raw: HyperliquidRawUserState): Balance[] {
normalizeBalance(raw: HyperliquidRawUserState, spot?: HyperliquidRawSpotState): Balance[] {
const result: Balance[] = [];

// Spot balances: outcome markets quote against these (USDC, USDH, etc.).
for (const b of spot?.balances ?? []) {
const total = parseFloat(b.total);
const hold = parseFloat(b.hold);
if (!Number.isFinite(total) || total === 0) continue;
result.push({
currency: b.coin,
total,
available: total - (Number.isFinite(hold) ? hold : 0),
locked: Number.isFinite(hold) ? hold : 0,
});
}

// Perp cross-margin account, labelled as such so consumers don't conflate it with spot.
const summary = raw.crossMarginSummary;
const total = parseFloat(summary.accountValue);
const locked = parseFloat(summary.totalMarginUsed);

return [{
currency: 'USDH',
total,
available: total - locked,
locked,
}];
const perpTotal = parseFloat(summary.accountValue);
const perpLocked = parseFloat(summary.totalMarginUsed);
if (Number.isFinite(perpTotal) && perpTotal > 0) {
result.push({
currency: 'USDC_PERP',
total: perpTotal,
available: perpTotal - perpLocked,
locked: perpLocked,
});
}

return result;
}

// -- Private helpers -------------------------------------------------------
Expand Down
Loading