@lifi/perps-sdk-provider-hyperliquid 4.0.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/HyperliquidProvider.d.ts.map +1 -1
- package/dist/cjs/HyperliquidProvider.js +1 -0
- package/dist/cjs/HyperliquidProvider.js.map +1 -1
- package/dist/cjs/services/getAccount.d.ts.map +1 -1
- package/dist/cjs/services/getAccount.js +1 -11
- package/dist/cjs/services/getAccount.js.map +1 -1
- package/dist/cjs/types/account.d.ts +1 -0
- package/dist/cjs/types/account.d.ts.map +1 -1
- package/dist/cjs/types/account.js +1 -0
- package/dist/cjs/types/account.js.map +1 -1
- package/dist/cjs/utils/index.d.ts +1 -0
- package/dist/cjs/utils/index.d.ts.map +1 -1
- package/dist/cjs/utils/index.js +3 -1
- package/dist/cjs/utils/index.js.map +1 -1
- package/dist/cjs/utils/spotCollateral.d.ts +7 -0
- package/dist/cjs/utils/spotCollateral.d.ts.map +1 -0
- package/dist/cjs/utils/spotCollateral.js +29 -0
- package/dist/cjs/utils/spotCollateral.js.map +1 -0
- package/dist/cjs/websocket/HyperliquidWsProvider.d.ts +15 -1
- package/dist/cjs/websocket/HyperliquidWsProvider.d.ts.map +1 -1
- package/dist/cjs/websocket/HyperliquidWsProvider.js +184 -27
- package/dist/cjs/websocket/HyperliquidWsProvider.js.map +1 -1
- package/dist/esm/HyperliquidProvider.d.ts.map +1 -1
- package/dist/esm/HyperliquidProvider.js +1 -0
- package/dist/esm/HyperliquidProvider.js.map +1 -1
- package/dist/esm/services/getAccount.d.ts.map +1 -1
- package/dist/esm/services/getAccount.js +2 -13
- package/dist/esm/services/getAccount.js.map +1 -1
- package/dist/esm/types/account.d.ts +3 -1
- package/dist/esm/types/account.d.ts.map +1 -1
- package/dist/esm/types/account.js +3 -1
- package/dist/esm/types/account.js.map +1 -1
- package/dist/esm/utils/index.d.ts +1 -0
- package/dist/esm/utils/index.d.ts.map +1 -1
- package/dist/esm/utils/index.js +1 -0
- package/dist/esm/utils/index.js.map +1 -1
- package/dist/esm/utils/spotCollateral.d.ts +14 -0
- package/dist/esm/utils/spotCollateral.d.ts.map +1 -0
- package/dist/esm/utils/spotCollateral.js +39 -0
- package/dist/esm/utils/spotCollateral.js.map +1 -0
- package/dist/esm/websocket/HyperliquidWsProvider.d.ts +23 -1
- package/dist/esm/websocket/HyperliquidWsProvider.d.ts.map +1 -1
- package/dist/esm/websocket/HyperliquidWsProvider.js +223 -17
- package/dist/esm/websocket/HyperliquidWsProvider.js.map +1 -1
- package/dist/types/HyperliquidProvider.d.ts.map +1 -1
- package/dist/types/services/getAccount.d.ts.map +1 -1
- package/dist/types/types/account.d.ts +3 -1
- package/dist/types/types/account.d.ts.map +1 -1
- package/dist/types/utils/index.d.ts +1 -0
- package/dist/types/utils/index.d.ts.map +1 -1
- package/dist/types/utils/spotCollateral.d.ts +14 -0
- package/dist/types/utils/spotCollateral.d.ts.map +1 -0
- package/dist/types/websocket/HyperliquidWsProvider.d.ts +23 -1
- package/dist/types/websocket/HyperliquidWsProvider.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/HyperliquidProvider.ts +2 -0
- package/src/services/getAccount.ts +8 -12
- package/src/types/account.ts +3 -1
- package/src/utils/index.ts +1 -0
- package/src/utils/spotCollateral.ts +50 -0
- package/src/websocket/HyperliquidWsProvider.ts +304 -18
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
type HlUserFees,
|
|
23
23
|
} from '../types/index.js'
|
|
24
24
|
import {
|
|
25
|
+
partitionSpotBalances,
|
|
25
26
|
perpsDexNames,
|
|
26
27
|
spotAssetFromToken,
|
|
27
28
|
spotBalance,
|
|
@@ -96,18 +97,13 @@ const buildBalances = (
|
|
|
96
97
|
priceById: Map<string, number>,
|
|
97
98
|
quoteAssetByCategory: Map<string, Asset>
|
|
98
99
|
): BalancePartition => {
|
|
99
|
-
const balances
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
collateralBalances.push(balance)
|
|
107
|
-
} else {
|
|
108
|
-
balances.push(balance)
|
|
109
|
-
}
|
|
110
|
-
}
|
|
100
|
+
const { balances, collateralBalances } = partitionSpotBalances(
|
|
101
|
+
spotState.balances.map((b) =>
|
|
102
|
+
spotBalance(spotAssetFromToken(b), b.total, priceById)
|
|
103
|
+
),
|
|
104
|
+
quoteAssetIds,
|
|
105
|
+
abstraction === HlAbstractionMode.PORTFOLIO_MARGIN
|
|
106
|
+
)
|
|
111
107
|
|
|
112
108
|
// Unified/portfolio modes hold everything in spot — per-dex equity would
|
|
113
109
|
// double-count. Only disabled/dexAbstraction carry separate venue collateral.
|
package/src/types/account.ts
CHANGED
|
@@ -69,10 +69,12 @@ export type HlPreTransferCheck = {
|
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
71
|
* Possible values returned by the `userAbstraction` info endpoint.
|
|
72
|
-
* `null` means abstraction has never been set
|
|
72
|
+
* `null` means abstraction has never been set; `'default'`/`'disabled'` are
|
|
73
|
+
* the live and legacy spellings of standard (non-abstracted) mode.
|
|
73
74
|
* @public
|
|
74
75
|
*/
|
|
75
76
|
export const HlAbstractionMode = {
|
|
77
|
+
DEFAULT: 'default',
|
|
76
78
|
DISABLED: 'disabled',
|
|
77
79
|
UNIFIED_ACCOUNT: 'unifiedAccount',
|
|
78
80
|
PORTFOLIO_MARGIN: 'portfolioMargin',
|
package/src/utils/index.ts
CHANGED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Balance } from '@lifi/perps-types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Portfolio-margin collateral beyond the category quote assets, keyed by spot
|
|
5
|
+
* display symbol with its loan-to-value weight. Hyperliquid credits these
|
|
6
|
+
* toward buying power at the given fraction (0.5 for both today). Quote assets
|
|
7
|
+
* (USDC/USDT) are already full-value collateral via the category model, so
|
|
8
|
+
* they are not listed here.
|
|
9
|
+
* https://hyperliquid.gitbook.io/hyperliquid-docs/trading/portfolio-margin
|
|
10
|
+
*/
|
|
11
|
+
const PORTFOLIO_MARGIN_LTV: Readonly<Record<string, number>> = {
|
|
12
|
+
HYPE: 0.5,
|
|
13
|
+
UBTC: 0.5,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** @internal */
|
|
17
|
+
export interface SpotPartition {
|
|
18
|
+
collateralBalances: Balance[]
|
|
19
|
+
balances: Balance[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Split spot balances into margin collateral and flat holdings. A balance is
|
|
24
|
+
* collateral when its token is a category quote asset (full value) or, under
|
|
25
|
+
* portfolio margin, an LTV-weighted asset (HYPE/BTC); everything else is a
|
|
26
|
+
* flat holding.
|
|
27
|
+
*/
|
|
28
|
+
export const partitionSpotBalances = (
|
|
29
|
+
spotBalances: readonly Balance[],
|
|
30
|
+
quoteAssetIds: ReadonlySet<string>,
|
|
31
|
+
portfolioMargin: boolean
|
|
32
|
+
): SpotPartition => {
|
|
33
|
+
const collateralBalances: Balance[] = []
|
|
34
|
+
const balances: Balance[] = []
|
|
35
|
+
for (const balance of spotBalances) {
|
|
36
|
+
if (quoteAssetIds.has(balance.asset.id)) {
|
|
37
|
+
collateralBalances.push(balance)
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
const ltv = portfolioMargin
|
|
41
|
+
? PORTFOLIO_MARGIN_LTV[balance.asset.displaySymbol]
|
|
42
|
+
: undefined
|
|
43
|
+
if (ltv !== undefined) {
|
|
44
|
+
collateralBalances.push({ ...balance, collateralWeight: ltv })
|
|
45
|
+
} else {
|
|
46
|
+
balances.push(balance)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { collateralBalances, balances }
|
|
50
|
+
}
|
|
@@ -7,22 +7,32 @@ import {
|
|
|
7
7
|
type QuoteListener,
|
|
8
8
|
ReconnectingWebSocket,
|
|
9
9
|
resolveSubscribeQuote,
|
|
10
|
+
type SubscriptionListener,
|
|
11
|
+
summarizeAccount,
|
|
10
12
|
WsProviderBase,
|
|
11
13
|
type WsProviderFactory,
|
|
14
|
+
type WsStatusListener,
|
|
12
15
|
wsLog,
|
|
13
16
|
} from '@lifi/perps-sdk'
|
|
14
17
|
import {
|
|
18
|
+
type AccountResponse,
|
|
19
|
+
type Balance,
|
|
15
20
|
type MarketContext,
|
|
16
21
|
type OpenOrder,
|
|
17
22
|
type OrderbookLevel,
|
|
18
23
|
type OrderbookResponse,
|
|
19
24
|
OrderSide,
|
|
20
25
|
OrderType,
|
|
26
|
+
type Position,
|
|
21
27
|
type Subscription,
|
|
22
28
|
type TriggerOrder,
|
|
23
29
|
} from '@lifi/perps-types'
|
|
24
30
|
import Big from 'big.js'
|
|
25
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
DEFAULT_HYPERLIQUID_API_URL,
|
|
33
|
+
HYPERLIQUID_FEE_TIER_FALLBACK,
|
|
34
|
+
SPOT_MARKET_ID,
|
|
35
|
+
} from '../constants.js'
|
|
26
36
|
import type {
|
|
27
37
|
HlAssetPosition,
|
|
28
38
|
HlOrderDetail,
|
|
@@ -46,9 +56,12 @@ import type {
|
|
|
46
56
|
HlWsTrade,
|
|
47
57
|
HlWsUserFillsData,
|
|
48
58
|
} from '../types/index.js'
|
|
59
|
+
import { HlAbstractionMode } from '../types/index.js'
|
|
49
60
|
import {
|
|
50
61
|
decodeCompressedJson,
|
|
51
62
|
decodeFastAssetCtxs,
|
|
63
|
+
hlInfoOptions,
|
|
64
|
+
infoRequest,
|
|
52
65
|
isOpenAssetPosition,
|
|
53
66
|
isTriggerOrder,
|
|
54
67
|
mapFill,
|
|
@@ -56,6 +69,7 @@ import {
|
|
|
56
69
|
mapOrderStatus,
|
|
57
70
|
mapOrderType,
|
|
58
71
|
mapPosition,
|
|
72
|
+
partitionSpotBalances,
|
|
59
73
|
priceStepToAggregation,
|
|
60
74
|
spotAssetFromToken,
|
|
61
75
|
spotBalance,
|
|
@@ -99,6 +113,42 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
|
|
|
99
113
|
private readonly clearinghouseRefs = new Map<string, number>()
|
|
100
114
|
private readonly client: PerpsSDKClient | undefined
|
|
101
115
|
private readonly registry: MarketRegistry | undefined
|
|
116
|
+
// The clearinghouse stream covers perps equity only, so its summary is
|
|
117
|
+
// honest solely for modes whose collateral lives per-dex. The abstraction
|
|
118
|
+
// mode is read per subscribed user (null = never set = standard) and
|
|
119
|
+
// selects the summary source: standard/dexAbstraction emit the equity
|
|
120
|
+
// summary straight from clearinghouse frames, unified/portfolio run the
|
|
121
|
+
// spot-fed pipeline below. Frames arriving before the first read are held
|
|
122
|
+
// and released on resolution (delayed, never wrong). The read refreshes on
|
|
123
|
+
// fresh summary subscribes and once older than the TTL, and drops with
|
|
124
|
+
// the user's last clearinghouse subscription.
|
|
125
|
+
private readonly abstractionByUser = new Map<
|
|
126
|
+
string,
|
|
127
|
+
{ mode: HlAbstractionMode | null; readAt: number } | 'pending'
|
|
128
|
+
>()
|
|
129
|
+
private readonly spotRefs = new Map<string, number>()
|
|
130
|
+
// Unified/portfolio summary pipeline: collateral lives in spot, margin and
|
|
131
|
+
// uPnL on the positions — the summary recomputes from the latest of both
|
|
132
|
+
// envelopes with the same gross calculator the REST getAccountSummary uses.
|
|
133
|
+
private readonly unifiedSummaryByUser = new Map<
|
|
134
|
+
string,
|
|
135
|
+
{
|
|
136
|
+
releaseSpot: Promise<() => void>
|
|
137
|
+
spot?: { collateralBalances: Balance[]; balances: Balance[] }
|
|
138
|
+
}
|
|
139
|
+
>()
|
|
140
|
+
// Kept outside the pipeline entry: the first clearinghouse frame lands
|
|
141
|
+
// while the mode read is still pending, before the pipeline exists.
|
|
142
|
+
private readonly latestPositionsByUser = new Map<string, Position[]>()
|
|
143
|
+
private readonly heldSummaryByUser = new Map<
|
|
144
|
+
string,
|
|
145
|
+
{
|
|
146
|
+
portfolioValue: string
|
|
147
|
+
availableMargin: string
|
|
148
|
+
marginUsed: string
|
|
149
|
+
unrealizedPnl: string
|
|
150
|
+
}
|
|
151
|
+
>()
|
|
102
152
|
private perpCtxBySubDex = new Map<string, Record<string, HlWsPerpAssetCtx>>()
|
|
103
153
|
private spotCtxByMarketId: Record<string, HlWsSpotAssetCtx> = {}
|
|
104
154
|
private marketsContextByMarketId: Record<string, MarketContext> = {}
|
|
@@ -142,6 +192,33 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
|
|
|
142
192
|
this.registry = client && getMarketRegistry(client, providerKey)
|
|
143
193
|
}
|
|
144
194
|
|
|
195
|
+
override async subscribe(
|
|
196
|
+
sub: Subscription,
|
|
197
|
+
listener: SubscriptionListener,
|
|
198
|
+
onStatus?: WsStatusListener
|
|
199
|
+
): Promise<() => void> {
|
|
200
|
+
const unsubscribe = await super.subscribe(sub, listener, onStatus)
|
|
201
|
+
// The summary source is decided by the abstraction mode, so every
|
|
202
|
+
// summary subscribe (re-)reads it — consumers resubscribe when they
|
|
203
|
+
// observe an account-config change, and the wire channel is usually
|
|
204
|
+
// reused (multiplexed listeners, teardown linger), so openChannel cannot
|
|
205
|
+
// carry this trigger. Coalesced so one React commit's burst of
|
|
206
|
+
// resubscribing hooks fires a single read.
|
|
207
|
+
if (sub.channel === 'accountSummary') {
|
|
208
|
+
const user = sub.address.toLowerCase()
|
|
209
|
+
const entry = this.abstractionByUser.get(user)
|
|
210
|
+
if (entry === undefined) {
|
|
211
|
+
this.fetchAbstractionMode(sub.address, user)
|
|
212
|
+
} else if (
|
|
213
|
+
entry !== 'pending' &&
|
|
214
|
+
Date.now() - entry.readAt > HyperliquidWsProvider.REFRESH_COALESCE_MS
|
|
215
|
+
) {
|
|
216
|
+
this.fetchAbstractionMode(sub.address, user, entry)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return unsubscribe
|
|
220
|
+
}
|
|
221
|
+
|
|
145
222
|
async subscribeQuote(
|
|
146
223
|
params: ProviderGetQuoteParams,
|
|
147
224
|
onQuote: QuoteListener
|
|
@@ -197,6 +274,14 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
|
|
|
197
274
|
}
|
|
198
275
|
}
|
|
199
276
|
|
|
277
|
+
// The spot wire is shared between the public spotBalances channel and
|
|
278
|
+
// the unified-summary pipeline; refcounted like the clearinghouse sub.
|
|
279
|
+
if (sub.channel === 'spotBalances') {
|
|
280
|
+
const release = await this.acquireSpotWire(sub.address)
|
|
281
|
+
await this.rws.ready()
|
|
282
|
+
return release
|
|
283
|
+
}
|
|
284
|
+
|
|
200
285
|
// `positions` and `accountSummary` are two views over the same wire
|
|
201
286
|
// subscription; refcount it so neither's teardown starves the other.
|
|
202
287
|
if (sub.channel === 'positions' || sub.channel === 'accountSummary') {
|
|
@@ -213,6 +298,12 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
|
|
|
213
298
|
const remaining = (this.clearinghouseRefs.get(user) ?? 1) - 1
|
|
214
299
|
if (remaining <= 0) {
|
|
215
300
|
this.clearinghouseRefs.delete(user)
|
|
301
|
+
// Drop the summary gate's mode read (and its pipeline) with the
|
|
302
|
+
// subscription so a resubscribe reflects an account-mode change.
|
|
303
|
+
this.abstractionByUser.delete(user)
|
|
304
|
+
this.heldSummaryByUser.delete(user)
|
|
305
|
+
this.latestPositionsByUser.delete(user)
|
|
306
|
+
this.syncUnifiedPipeline(user, user, null)
|
|
216
307
|
this.unregisterSub(wireKey)
|
|
217
308
|
this.rws.send(
|
|
218
309
|
JSON.stringify({ method: 'unsubscribe', subscription: payload })
|
|
@@ -955,6 +1046,9 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
|
|
|
955
1046
|
data: positions,
|
|
956
1047
|
})
|
|
957
1048
|
|
|
1049
|
+
this.latestPositionsByUser.set(data.user.toLowerCase(), positions)
|
|
1050
|
+
this.emitUnifiedSummary(data.user.toLowerCase())
|
|
1051
|
+
|
|
958
1052
|
let accountValue = 0
|
|
959
1053
|
let marginUsed = 0
|
|
960
1054
|
for (const [, state] of data.clearinghouseStates) {
|
|
@@ -969,30 +1063,222 @@ export class HyperliquidWsProvider extends WsProviderBase<object> {
|
|
|
969
1063
|
)
|
|
970
1064
|
// Equity semantics, matching the REST summary: `accountValue` already
|
|
971
1065
|
// carries locked margin and unrealized PnL. Spot balances stream apart,
|
|
972
|
-
// so this portfolio value covers perps equity only
|
|
973
|
-
|
|
1066
|
+
// so this portfolio value covers perps equity only — which is why the
|
|
1067
|
+
// frame is gated on the abstraction mode below.
|
|
1068
|
+
this.emitSummaryIfModeAllows(data.user, {
|
|
1069
|
+
portfolioValue: accountValue.toString(),
|
|
1070
|
+
availableMargin: (accountValue - marginUsed).toString(),
|
|
1071
|
+
marginUsed: marginUsed.toString(),
|
|
1072
|
+
unrealizedPnl: unrealizedPnl.toString(),
|
|
1073
|
+
})
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/** Whether the mode's collateral lives per-dex, making the perps-only
|
|
1077
|
+
* summary frame honest. Unified/portfolio hold collateral in spot. */
|
|
1078
|
+
private static summaryComputableFor(mode: HlAbstractionMode | null): boolean {
|
|
1079
|
+
return (
|
|
1080
|
+
mode !== HlAbstractionMode.UNIFIED_ACCOUNT &&
|
|
1081
|
+
mode !== HlAbstractionMode.PORTFOLIO_MARGIN
|
|
1082
|
+
)
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
private async acquireSpotWire(address: string): Promise<() => void> {
|
|
1086
|
+
// Lowercased in the payload too, so releases from either holder (public
|
|
1087
|
+
// channel or pipeline) unsubscribe with an identical payload.
|
|
1088
|
+
const user = address.toLowerCase()
|
|
1089
|
+
const wireKey = `spot:${user}`
|
|
1090
|
+
const payload = { type: 'spotState', user }
|
|
1091
|
+
const count = this.spotRefs.get(user) ?? 0
|
|
1092
|
+
this.spotRefs.set(user, count + 1)
|
|
1093
|
+
if (count === 0) {
|
|
1094
|
+
await this.registerSub(wireKey, payload)
|
|
1095
|
+
}
|
|
1096
|
+
let released = false
|
|
1097
|
+
return () => {
|
|
1098
|
+
if (released) {
|
|
1099
|
+
return
|
|
1100
|
+
}
|
|
1101
|
+
released = true
|
|
1102
|
+
const remaining = (this.spotRefs.get(user) ?? 1) - 1
|
|
1103
|
+
if (remaining <= 0) {
|
|
1104
|
+
this.spotRefs.delete(user)
|
|
1105
|
+
this.unregisterSub(wireKey)
|
|
1106
|
+
this.rws.send(
|
|
1107
|
+
JSON.stringify({ method: 'unsubscribe', subscription: payload })
|
|
1108
|
+
)
|
|
1109
|
+
} else {
|
|
1110
|
+
this.spotRefs.set(user, remaining)
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
/** Start or stop the spot-fed unified pipeline to match the resolved mode. */
|
|
1116
|
+
private syncUnifiedPipeline(
|
|
1117
|
+
key: string,
|
|
1118
|
+
address: string,
|
|
1119
|
+
mode: HlAbstractionMode | null
|
|
1120
|
+
) {
|
|
1121
|
+
const unified = !HyperliquidWsProvider.summaryComputableFor(mode)
|
|
1122
|
+
const existing = this.unifiedSummaryByUser.get(key)
|
|
1123
|
+
if (unified && existing === undefined) {
|
|
1124
|
+
this.unifiedSummaryByUser.set(key, {
|
|
1125
|
+
releaseSpot: this.acquireSpotWire(address).catch((error) => {
|
|
1126
|
+
wsLog.handlerFailure(this.providerKey, error)
|
|
1127
|
+
return () => {}
|
|
1128
|
+
}),
|
|
1129
|
+
})
|
|
1130
|
+
} else if (!unified && existing !== undefined) {
|
|
1131
|
+
this.unifiedSummaryByUser.delete(key)
|
|
1132
|
+
void existing.releaseSpot.then((release) => release())
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
private emitUnifiedSummary(key: string) {
|
|
1137
|
+
const pipeline = this.unifiedSummaryByUser.get(key)
|
|
1138
|
+
const positions = this.latestPositionsByUser.get(key)
|
|
1139
|
+
// Both envelopes must have arrived: a spot-only summary would report
|
|
1140
|
+
// zero margin used against real positions.
|
|
1141
|
+
if (pipeline?.spot === undefined || positions === undefined) {
|
|
1142
|
+
return
|
|
1143
|
+
}
|
|
1144
|
+
const summary = summarizeAccount(
|
|
1145
|
+
// summarizeAccount reads only the two balance lists.
|
|
1146
|
+
{
|
|
1147
|
+
collateralBalances: pipeline.spot.collateralBalances,
|
|
1148
|
+
balances: pipeline.spot.balances,
|
|
1149
|
+
} as AccountResponse,
|
|
1150
|
+
positions,
|
|
1151
|
+
'gross'
|
|
1152
|
+
)
|
|
1153
|
+
this.emit(`accountSummary:${key}`, {
|
|
974
1154
|
channel: 'accountSummary',
|
|
975
|
-
data:
|
|
976
|
-
portfolioValue: accountValue.toString(),
|
|
977
|
-
availableMargin: (accountValue - marginUsed).toString(),
|
|
978
|
-
marginUsed: marginUsed.toString(),
|
|
979
|
-
unrealizedPnl: unrealizedPnl.toString(),
|
|
980
|
-
},
|
|
1155
|
+
data: summary,
|
|
981
1156
|
})
|
|
982
1157
|
}
|
|
983
1158
|
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1159
|
+
/** Re-read the abstraction mode this long after the previous read, so an
|
|
1160
|
+
* in-session account-mode switch flips the gate within one refresh. */
|
|
1161
|
+
private static readonly ABSTRACTION_TTL_MS = 15_000
|
|
1162
|
+
|
|
1163
|
+
/** Subscribe-triggered re-reads younger than this are skipped: a config
|
|
1164
|
+
* change resubscribes every consumer hook in one React commit, and one
|
|
1165
|
+
* read serves them all. */
|
|
1166
|
+
private static readonly REFRESH_COALESCE_MS = 200
|
|
1167
|
+
|
|
1168
|
+
private emitSummaryIfModeAllows(
|
|
1169
|
+
user: string,
|
|
1170
|
+
summary: {
|
|
1171
|
+
portfolioValue: string
|
|
1172
|
+
availableMargin: string
|
|
1173
|
+
marginUsed: string
|
|
1174
|
+
unrealizedPnl: string
|
|
1175
|
+
}
|
|
1176
|
+
) {
|
|
1177
|
+
const key = user.toLowerCase()
|
|
1178
|
+
// Without a client there is no way to read the mode; keep the historic
|
|
1179
|
+
// always-emit behavior rather than silencing every standalone consumer.
|
|
1180
|
+
if (this.client === undefined) {
|
|
1181
|
+
this.emit(`accountSummary:${key}`, {
|
|
1182
|
+
channel: 'accountSummary',
|
|
1183
|
+
data: summary,
|
|
1184
|
+
})
|
|
1185
|
+
return
|
|
1186
|
+
}
|
|
1187
|
+
const entry = this.abstractionByUser.get(key)
|
|
1188
|
+
if (entry === undefined || entry === 'pending') {
|
|
1189
|
+
this.heldSummaryByUser.set(key, summary)
|
|
1190
|
+
if (entry === undefined) {
|
|
1191
|
+
this.fetchAbstractionMode(user, key)
|
|
1192
|
+
}
|
|
1193
|
+
return
|
|
1194
|
+
}
|
|
1195
|
+
if (Date.now() - entry.readAt > HyperliquidWsProvider.ABSTRACTION_TTL_MS) {
|
|
1196
|
+
// Refresh in the background; the current mode keeps gating meanwhile,
|
|
1197
|
+
// so frames neither stall nor mis-emit while the read is in flight.
|
|
1198
|
+
this.fetchAbstractionMode(user, key, entry)
|
|
1199
|
+
}
|
|
1200
|
+
if (!HyperliquidWsProvider.summaryComputableFor(entry.mode)) {
|
|
1201
|
+
return
|
|
1202
|
+
}
|
|
1203
|
+
this.emit(`accountSummary:${key}`, {
|
|
1204
|
+
channel: 'accountSummary',
|
|
1205
|
+
data: summary,
|
|
1206
|
+
})
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
private fetchAbstractionMode(
|
|
1210
|
+
user: string,
|
|
1211
|
+
key: string,
|
|
1212
|
+
refreshing?: { mode: HlAbstractionMode | null; readAt: number }
|
|
1213
|
+
) {
|
|
1214
|
+
const client = this.client
|
|
1215
|
+
if (client === undefined) {
|
|
1216
|
+
return
|
|
1217
|
+
}
|
|
1218
|
+
// A background refresh keeps the current entry live (stamped now so
|
|
1219
|
+
// frames don't re-kick the read); the first read holds frames instead.
|
|
1220
|
+
this.abstractionByUser.set(
|
|
1221
|
+
key,
|
|
1222
|
+
refreshing ? { ...refreshing, readAt: Date.now() } : 'pending'
|
|
988
1223
|
)
|
|
989
|
-
|
|
1224
|
+
// "Never set abstraction" is a successful 200 `null` body — only a fetch
|
|
1225
|
+
// failure clears a first read so the next frame retries it. A failed
|
|
1226
|
+
// refresh keeps the previous mode until the TTL passes again.
|
|
1227
|
+
infoRequest<HlAbstractionMode | null>(
|
|
1228
|
+
DEFAULT_HYPERLIQUID_API_URL,
|
|
1229
|
+
{ type: 'userAbstraction', user },
|
|
1230
|
+
hlInfoOptions(client)
|
|
1231
|
+
).then(
|
|
1232
|
+
(mode) => {
|
|
1233
|
+
this.abstractionByUser.set(key, { mode, readAt: Date.now() })
|
|
1234
|
+
this.syncUnifiedPipeline(key, user, mode)
|
|
1235
|
+
const held = this.heldSummaryByUser.get(key)
|
|
1236
|
+
this.heldSummaryByUser.delete(key)
|
|
1237
|
+
if (held && HyperliquidWsProvider.summaryComputableFor(mode)) {
|
|
1238
|
+
this.emit(`accountSummary:${key}`, {
|
|
1239
|
+
channel: 'accountSummary',
|
|
1240
|
+
data: held,
|
|
1241
|
+
})
|
|
1242
|
+
}
|
|
1243
|
+
},
|
|
1244
|
+
(error) => {
|
|
1245
|
+
if (!refreshing) {
|
|
1246
|
+
this.abstractionByUser.delete(key)
|
|
1247
|
+
}
|
|
1248
|
+
wsLog.handlerFailure(this.providerKey, error)
|
|
1249
|
+
}
|
|
1250
|
+
)
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
private handleSpotState(data: HlWsSpotStateData) {
|
|
1254
|
+
const user = data.user.toLowerCase()
|
|
1255
|
+
const markets = this.registry?.markets ?? []
|
|
1256
|
+
const priceById = spotPriceById(markets, this.mergedMids())
|
|
1257
|
+
const rows = data.spotState.balances.map((b) => ({
|
|
1258
|
+
balance: spotBalance(spotAssetFromToken(b), b.total, priceById),
|
|
1259
|
+
hold: b.hold,
|
|
1260
|
+
}))
|
|
1261
|
+
this.emit(`spotState:${user}`, {
|
|
990
1262
|
channel: 'spotBalances',
|
|
991
|
-
data:
|
|
992
|
-
...spotBalance(spotAssetFromToken(b), b.total, priceById),
|
|
993
|
-
locked: b.hold,
|
|
994
|
-
})),
|
|
1263
|
+
data: rows.map(({ balance, hold }) => ({ ...balance, locked: hold })),
|
|
995
1264
|
})
|
|
1265
|
+
|
|
1266
|
+
const pipeline = this.unifiedSummaryByUser.get(user)
|
|
1267
|
+
if (pipeline !== undefined) {
|
|
1268
|
+
const quoteAssetIds = new Set(markets.map((m) => m.quoteAsset.id))
|
|
1269
|
+
const entry = this.abstractionByUser.get(user)
|
|
1270
|
+
const portfolioMargin =
|
|
1271
|
+
entry !== undefined &&
|
|
1272
|
+
entry !== 'pending' &&
|
|
1273
|
+
entry.mode === HlAbstractionMode.PORTFOLIO_MARGIN
|
|
1274
|
+
// Same partition as getAccount so REST and WS agree on collateral.
|
|
1275
|
+
pipeline.spot = partitionSpotBalances(
|
|
1276
|
+
rows.map(({ balance }) => balance),
|
|
1277
|
+
quoteAssetIds,
|
|
1278
|
+
portfolioMargin
|
|
1279
|
+
)
|
|
1280
|
+
this.emitUnifiedSummary(user)
|
|
1281
|
+
}
|
|
996
1282
|
}
|
|
997
1283
|
}
|
|
998
1284
|
|