@pear-protocol/exchanges-sdk 0.6.7 → 0.7.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.
Files changed (40) hide show
  1. package/README.md +105 -25
  2. package/dist/client/bybit/rest.js +8 -1
  3. package/dist/client/hyperliquid/rest.js +21 -1
  4. package/dist/client/lighter/rest.js +6 -12
  5. package/dist/client/okx/rest.js +5 -1
  6. package/dist/index.d.ts +1110 -74
  7. package/dist/sdk.js +9 -2
  8. package/dist/tracker/balance-core.js +89 -0
  9. package/dist/tracker/balance.js +25 -0
  10. package/dist/tracker/basket-size.js +194 -0
  11. package/dist/tracker/binance/balance.js +11 -0
  12. package/dist/tracker/binance/mapper.js +87 -42
  13. package/dist/tracker/binance/tracker.js +29 -14
  14. package/dist/tracker/binance/types.js +0 -6
  15. package/dist/tracker/bybit/balance.js +14 -0
  16. package/dist/tracker/bybit/mapper.js +109 -52
  17. package/dist/tracker/bybit/tracker.js +242 -33
  18. package/dist/tracker/bybit/types.js +0 -7
  19. package/dist/tracker/hyperliquid/account-mode.js +14 -0
  20. package/dist/tracker/hyperliquid/balance.js +184 -0
  21. package/dist/tracker/hyperliquid/basket-size.js +93 -0
  22. package/dist/tracker/hyperliquid/coin-name.js +18 -0
  23. package/dist/tracker/hyperliquid/collateral.js +48 -0
  24. package/dist/tracker/hyperliquid/decimal.js +28 -0
  25. package/dist/tracker/hyperliquid/mapper.js +399 -176
  26. package/dist/tracker/hyperliquid/spot-pricing.js +106 -0
  27. package/dist/tracker/hyperliquid/tracker.js +237 -78
  28. package/dist/tracker/hyperliquid/types.js +1 -7
  29. package/dist/tracker/index.js +9 -7
  30. package/dist/tracker/lighter/balance.js +87 -0
  31. package/dist/tracker/lighter/basket-size.js +74 -0
  32. package/dist/tracker/lighter/mapper.js +324 -145
  33. package/dist/tracker/lighter/tracker.js +228 -117
  34. package/dist/tracker/lighter/types.js +1 -7
  35. package/dist/tracker/okx/balance.js +17 -0
  36. package/dist/tracker/okx/mapper.js +156 -58
  37. package/dist/tracker/okx/tracker.js +228 -29
  38. package/dist/tracker/okx/types.js +0 -8
  39. package/dist/tracker/types.js +0 -24
  40. package/package.json +5 -7
@@ -0,0 +1,106 @@
1
+ import { toPrice, toDecimal, ZERO, ONE } from './decimal';
2
+
3
+ var __defProp = Object.defineProperty;
4
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
5
+ const USDC_TOKEN_INDEX = 0;
6
+ class SpotPrices {
7
+ static {
8
+ __name(this, "SpotPrices");
9
+ }
10
+ midByToken;
11
+ tokenByCoin;
12
+ constructor(midByToken, tokenByCoin) {
13
+ this.midByToken = midByToken;
14
+ this.tokenByCoin = tokenByCoin;
15
+ }
16
+ /** Builds the price book from the USDC-quoted pairs, then from the pairs those pairs unlock. */
17
+ static from(spotMeta, mids) {
18
+ const tokenByCoin = /* @__PURE__ */ new Map();
19
+ for (const token of spotMeta.tokens ?? []) {
20
+ tokenByCoin.set(token.name, token.index);
21
+ }
22
+ const directMids = directUsdcMids(spotMeta, mids);
23
+ const midByToken = new Map(directMids);
24
+ for (const pair of spotMeta.universe ?? []) {
25
+ const [base, quote] = pair.tokens;
26
+ const quoteMid = directMids.get(quote);
27
+ if (quoteMid === void 0 || midByToken.has(base)) {
28
+ continue;
29
+ }
30
+ const pairMid = toPrice(mids[pair.name]);
31
+ if (pairMid !== null) {
32
+ midByToken.set(base, pairMid.times(quoteMid));
33
+ }
34
+ }
35
+ return new SpotPrices(midByToken, tokenByCoin);
36
+ }
37
+ /** The USDC mid for a named coin, or `null` when the venue publishes no usable price for it. */
38
+ midForCoin(coin) {
39
+ const token = this.tokenByCoin.get(coin);
40
+ if (token === void 0) {
41
+ return null;
42
+ }
43
+ return this.midForToken(token);
44
+ }
45
+ /**
46
+ * The USDC mid for a token index, or `null` when the venue publishes no usable price for it.
47
+ *
48
+ * A spot balance carries the index rather than a name, so this is the direct reach; `midForCoin`
49
+ * is the same lookup for a caller that only holds a name (staked HYPE).
50
+ */
51
+ midForToken(token) {
52
+ return this.midByToken.get(token)?.toFixed() ?? null;
53
+ }
54
+ /**
55
+ * Values every balance in USD, summing over ALL tokens rather than the collateral tokens alone.
56
+ *
57
+ * A token the book holds no usable price for is reported rather than silently valued at zero, so
58
+ * a caller can see that the equity it received is incomplete.
59
+ *
60
+ * KNOWN LIMITATION — an illiquid book can disagree with the venue's own valuation. On
61
+ * `0xb1ec7feb…` the venue valued BRIDGE at $59,378 where `allMids["@219"]` gives $48,393, 18.5%
62
+ * apart; that book quotes `midPx 0.025246`, `markPx 0.018561` and `prevDayPx 0.030264`, and the
63
+ * venue's figure matches none of the three. It is the single cell above 1% across 86 measured on
64
+ * 14 accounts, and it costs 0.26% of that account's Total Equity. No LTV-bearing token and no
65
+ * stablecoin was ever mispriced this way, so no margin figure is affected — only equity, and only
66
+ * through non-collateral dust. Chasing it would need a second price source the venue does not
67
+ * publish.
68
+ */
69
+ valueBalances(balances) {
70
+ let equity = ZERO;
71
+ const unpricedCoins = [];
72
+ for (const balance of balances) {
73
+ const total = toDecimal(balance.total);
74
+ const mid = this.midByToken.get(balance.token);
75
+ if (mid === void 0) {
76
+ if (!total.isZero()) {
77
+ unpricedCoins.push(balance.coin);
78
+ }
79
+ continue;
80
+ }
81
+ equity = equity.plus(total.times(mid));
82
+ }
83
+ return {
84
+ equity: equity.toFixed(),
85
+ unpricedCoins
86
+ };
87
+ }
88
+ }
89
+ function directUsdcMids(spotMeta, mids) {
90
+ const midByToken = /* @__PURE__ */ new Map();
91
+ midByToken.set(USDC_TOKEN_INDEX, ONE);
92
+ for (const pair of spotMeta.universe ?? []) {
93
+ const [base, quote] = pair.tokens;
94
+ if (quote !== USDC_TOKEN_INDEX || base === USDC_TOKEN_INDEX) {
95
+ continue;
96
+ }
97
+ const mid = toPrice(mids[pair.name]);
98
+ if (mid !== null && !midByToken.has(base)) {
99
+ midByToken.set(base, mid);
100
+ }
101
+ }
102
+ return midByToken;
103
+ }
104
+ __name(directUsdcMids, "directUsdcMids");
105
+
106
+ export { SpotPrices };
@@ -1,13 +1,22 @@
1
- import BigNumber from 'bignumber.js';
2
1
  import { createExchangeWs } from '../../client';
3
- import { fetchPerpDexs, fetchInfo, fetchUserAbstraction, fetchSpotClearinghouseState, fetchClearinghouseState, fetchActiveAssetData } from '../../client/hyperliquid/rest';
2
+ import { fetchPerpDexs, fetchInfo, fetchAllMids, fetchUserAbstraction, fetchSpotClearinghouseState, fetchDelegatorSummary, fetchUserVaultEquities, fetchClearinghouseState, fetchActiveAssetData } from '../../client/hyperliquid/rest';
4
3
  import { BaseAccountTracker } from '../base';
4
+ import { computeMaxBasketSize } from '../basket-size';
5
+ import { resolveHyperliquidAccountType } from './account-mode';
6
+ import { minimumAvailableToTrade, findLegMarket, buildPoolBudgets, buildCappedLegs } from './basket-size';
7
+ import { buildAvailableMarginByToken, buildCollateralTokenNames } from './collateral';
5
8
  import { LOCKED_TRACKED_COINS } from './const';
6
- import { aggregateClearinghouseStates, mapBalance, mapPositions } from './mapper';
7
- import { HyperliquidAccountType, COLLATERAL_TOKEN_MAP } from './types';
9
+ import { toDecimal, ZERO } from './decimal';
10
+ import { emptyClearinghouseSummary, aggregateClearinghouseStates, mapBalance, mapPositions } from './mapper';
11
+ import { SpotPrices } from './spot-pricing';
12
+ import { HyperliquidAccountType } from './types';
8
13
 
9
14
  var __defProp = Object.defineProperty;
10
15
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
16
+ const EMPTY_SPOT_META = {
17
+ tokens: [],
18
+ universe: []
19
+ };
11
20
  class HyperliquidAccountTracker extends BaseAccountTracker {
12
21
  static {
13
22
  __name(this, "HyperliquidAccountTracker");
@@ -18,12 +27,20 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
18
27
  openListenerId = null;
19
28
  lifecycleVersion = 0;
20
29
  subscribedCoins = /* @__PURE__ */ new Set();
21
- lastSummary = emptySummary();
30
+ lastSummary = emptyClearinghouseSummary();
22
31
  spotCoinBalances = /* @__PURE__ */ new Map();
32
+ /** Portfolio Margin's own risk scalars, which ride the same `spotState` frame the balances do. */
33
+ portfolioMarginRatio = null;
34
+ borrowRatioByToken = /* @__PURE__ */ new Map();
23
35
  hasSpotState = false;
24
36
  hasClearinghouseState = false;
25
37
  accountType = null;
26
- collateralTokenMap = legacyCollateralTokenMap();
38
+ spotMeta = EMPTY_SPOT_META;
39
+ collateralTokenNames = /* @__PURE__ */ new Map();
40
+ prices = null;
41
+ hasWebData3 = false;
42
+ vaultEquity = "0";
43
+ stakedHype = "0";
27
44
  dexToMeta = /* @__PURE__ */ new Map();
28
45
  trackedAssetData = /* @__PURE__ */ new Map();
29
46
  constructor(config) {
@@ -31,11 +48,12 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
31
48
  this.address = config.account.exchangeIdentifier;
32
49
  }
33
50
  /**
34
- * Reads account mode, collateral balances, per-DEX state, and positions without opening a socket.
51
+ * Reads account mode, spot prices, collateral balances, per-DEX state, staking, vaults and
52
+ * per-asset headroom without opening a socket.
35
53
  */
36
54
  async snapshot() {
37
55
  const user = this.addrLower();
38
- const [perpDexs, allMetas, spotMeta, abstraction, spotState] = await Promise.all([
56
+ const [perpDexs, allMetas, spotMeta, mids, abstraction, spotState, delegator, vaults] = await Promise.all([
39
57
  fetchPerpDexs(this.demo),
40
58
  fetchInfo({
41
59
  type: "allPerpMetas"
@@ -43,8 +61,11 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
43
61
  fetchInfo({
44
62
  type: "spotMeta"
45
63
  }, this.demo),
64
+ fetchAllMids(this.demo),
46
65
  fetchUserAbstraction(user, this.demo),
47
- fetchSpotClearinghouseState(user, this.demo)
66
+ fetchSpotClearinghouseState(user, this.demo),
67
+ fetchDelegatorSummary(user, this.demo),
68
+ fetchUserVaultEquities(user, this.demo)
48
69
  ]);
49
70
  const descriptors = uniqueDexDescriptors(perpDexs, allMetas);
50
71
  const clearinghouseStates = await Promise.all(descriptors.map(async ({ dex, meta }) => ({
@@ -53,17 +74,57 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
53
74
  meta,
54
75
  state: await fetchClearinghouseState(user, dex, this.demo)
55
76
  })));
77
+ const assetData = await this.fetchActiveAssetEvents(this.coinsForAvailableMargin());
56
78
  const { summary, assetPositions } = aggregateClearinghouseStates(clearinghouseStates);
57
- const accountType = resolveHyperliquidAccountType(abstraction);
58
- const collateralTokenMap = buildCollateralTokenMap(allMetas, spotMeta);
59
- const spotCoinBalances = mapSpotBalances(spotState);
60
- const balance = mapBalance(summary, spotCoinBalances, accountType, collateralTokenMap);
79
+ const balance = mapBalance({
80
+ accountType: resolveHyperliquidAccountType(abstraction),
81
+ summary,
82
+ spotBalances: mapSpotBalances(spotState),
83
+ prices: SpotPrices.from(spotMeta, mids),
84
+ vaultEquity: sumRestVaultEquity(vaults),
85
+ stakedHype: stakingAccountHype(delegator),
86
+ availableMargin: minimumAvailableToTrade(assetData.values()),
87
+ collateralTokenNames: buildCollateralTokenNames(spotMeta),
88
+ availableMarginByToken: buildAvailableMarginByToken(summary.perDexState, assetData.values()),
89
+ portfolioMarginRatio: spotState.portfolioMarginRatio ?? null,
90
+ borrowRatioByToken: mapBorrowRatios(spotState),
91
+ timestamp: Date.now()
92
+ });
61
93
  const positions = Array.from(mapPositions(assetPositions).values());
62
94
  return {
63
95
  balance,
64
96
  positions
65
97
  };
66
98
  }
99
+ /** Sizes a basket over REST alone, so the call stays usable without a running socket. */
100
+ async snapshotMaxBasketSize(input) {
101
+ const [dexToMeta, tokenNames] = await Promise.all([
102
+ this.loadPerpMetasOnce(),
103
+ this.loadCollateralTokenNamesOnce()
104
+ ]);
105
+ const assetData = await this.fetchActiveAssetEvents(input.legs.map((leg) => leg.asset));
106
+ return sizeBasket(input, dexToMeta, tokenNames, assetData);
107
+ }
108
+ /**
109
+ * Sizes a basket against live state, fetching per-asset data for any leg the socket has not
110
+ * subscribed to — which is exactly why the call is asynchronous on both paths.
111
+ */
112
+ async maxBasketSize(input) {
113
+ const [dexToMeta, tokenNames] = await Promise.all([
114
+ this.loadPerpMetasOnce(),
115
+ this.loadCollateralTokenNamesOnce()
116
+ ]);
117
+ const missingCoins = input.legs.map((leg) => leg.asset).filter((asset) => !this.trackedAssetData.has(asset));
118
+ const fetched = await this.fetchActiveAssetEvents(missingCoins);
119
+ const assetData = new Map(fetched);
120
+ for (const leg of input.legs) {
121
+ const cached = this.trackedAssetData.get(leg.asset);
122
+ if (cached) {
123
+ assetData.set(leg.asset, cached);
124
+ }
125
+ }
126
+ return sizeBasket(input, dexToMeta, tokenNames, assetData);
127
+ }
67
128
  /**
68
129
  * Stateless one-shot per-asset settings read via the `activeAssetData` info request (the REST
69
130
  * twin of the live WS subscription), one request per coin fired in parallel — the info endpoint
@@ -71,16 +132,15 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
71
132
  * throws if every coin fetch fails.
72
133
  */
73
134
  async fetchAssetSnapshots(coins) {
74
- const user = this.addrLower();
75
- const settled = await Promise.allSettled(coins.map((coin) => fetchActiveAssetData(user, coin, this.demo)));
76
- if (settled.every((r) => r.status === "rejected")) {
135
+ const events = await this.fetchActiveAssetEvents(coins);
136
+ if (coins.length > 0 && events.size === 0) {
77
137
  throw new Error("Hyperliquid snapshotAssets: every activeAssetData read failed");
78
138
  }
79
139
  const result = {};
80
- coins.forEach((coin, i) => {
81
- const r = settled[i];
82
- result[coin] = r?.status === "fulfilled" && r.value.coin ? buildTrackedAssetInfo(r.value) : null;
83
- });
140
+ for (const coin of coins) {
141
+ const event = events.get(coin);
142
+ result[coin] = event ? buildTrackedAssetInfo(event) : null;
143
+ }
84
144
  return result;
85
145
  }
86
146
  async start() {
@@ -90,7 +150,7 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
90
150
  this.subscribedCoins.clear();
91
151
  this.resetLiveAccountState();
92
152
  this.dexToMeta.clear();
93
- const [allMetas, perpDexs, spotMeta, abstraction] = await Promise.all([
153
+ const [allMetas, perpDexs, spotMeta, abstraction, delegator] = await Promise.all([
94
154
  fetchInfo({
95
155
  type: "allPerpMetas"
96
156
  }, this.demo),
@@ -98,12 +158,15 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
98
158
  fetchInfo({
99
159
  type: "spotMeta"
100
160
  }, this.demo),
101
- fetchUserAbstraction(user, this.demo)
161
+ fetchUserAbstraction(user, this.demo),
162
+ fetchDelegatorSummary(user, this.demo)
102
163
  ]);
103
164
  if (lifecycleVersion !== this.lifecycleVersion) return;
104
165
  this.loadPerpMetas(allMetas, perpDexs);
105
- this.collateralTokenMap = buildCollateralTokenMap(allMetas, spotMeta);
166
+ this.spotMeta = spotMeta;
167
+ this.collateralTokenNames = buildCollateralTokenNames(spotMeta);
106
168
  this.accountType = resolveHyperliquidAccountType(abstraction);
169
+ this.stakedHype = stakingAccountHype(delegator);
107
170
  const ws = (await createExchangeWs("hyperliquid", {
108
171
  demo: this.demo,
109
172
  credentials: this.credentials
@@ -197,6 +260,12 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
197
260
  user
198
261
  }
199
262
  });
263
+ this.ws?.send({
264
+ method: "subscribe",
265
+ subscription: {
266
+ type: "allMids"
267
+ }
268
+ });
200
269
  }
201
270
  /** Subscribes to the spot state variant required by the active account mode. */
202
271
  sendSpotStateSubscription(method, accountType) {
@@ -252,6 +321,12 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
252
321
  data: msg.data
253
322
  });
254
323
  break;
324
+ case "allMids":
325
+ this.handle({
326
+ kind: "allMids",
327
+ data: msg.data
328
+ });
329
+ break;
255
330
  case "activeAssetData":
256
331
  this.handle({
257
332
  kind: "activeAssetData",
@@ -274,6 +349,9 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
274
349
  case "webData3":
275
350
  this.onWebData3(event.data);
276
351
  break;
352
+ case "allMids":
353
+ this.onAllMids(event.data);
354
+ break;
277
355
  case "activeAssetData":
278
356
  this.onActiveAssetData(event.data);
279
357
  break;
@@ -285,6 +363,56 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
285
363
  this.dexToMeta.set(dex, dexMeta);
286
364
  });
287
365
  }
366
+ /** Loads the dex metadata a sizing call needs, reusing whatever `start()` already cached. */
367
+ async loadPerpMetasOnce() {
368
+ if (this.dexToMeta.size > 0) {
369
+ return this.dexToMeta;
370
+ }
371
+ const [allMetas, perpDexs] = await Promise.all([
372
+ fetchInfo({
373
+ type: "allPerpMetas"
374
+ }, this.demo),
375
+ fetchPerpDexs(this.demo)
376
+ ]);
377
+ this.loadPerpMetas(allMetas, perpDexs);
378
+ return this.dexToMeta;
379
+ }
380
+ /** Loads the token names a pool id is spelled with, reusing whatever `start()` already cached. */
381
+ async loadCollateralTokenNamesOnce() {
382
+ if (this.collateralTokenNames.size > 0) {
383
+ return this.collateralTokenNames;
384
+ }
385
+ const spotMeta = await fetchInfo({
386
+ type: "spotMeta"
387
+ }, this.demo);
388
+ this.collateralTokenNames = buildCollateralTokenNames(spotMeta);
389
+ return this.collateralTokenNames;
390
+ }
391
+ /** Reads `activeAssetData` for each coin in parallel, dropping the coins the venue refuses. */
392
+ async fetchActiveAssetEvents(coins) {
393
+ const unique = [
394
+ ...new Set(coins)
395
+ ];
396
+ const user = this.addrLower();
397
+ const settled = await Promise.allSettled(unique.map((coin) => fetchActiveAssetData(user, coin, this.demo)));
398
+ const events = /* @__PURE__ */ new Map();
399
+ unique.forEach((coin, index) => {
400
+ const result = settled[index];
401
+ if (result?.status === "fulfilled" && result.value.coin) {
402
+ events.set(coin, result.value);
403
+ }
404
+ });
405
+ return events;
406
+ }
407
+ /** The asset set `availableMargin` is the floor over: the locked coins plus every tracked one. */
408
+ coinsForAvailableMargin() {
409
+ return [
410
+ .../* @__PURE__ */ new Set([
411
+ ...LOCKED_TRACKED_COINS,
412
+ ...this.trackedCoins
413
+ ])
414
+ ];
415
+ }
288
416
  onClearinghouseState(data) {
289
417
  const states = (data.clearinghouseStates || []).flatMap(([dex, state]) => state ? [
290
418
  {
@@ -310,12 +438,33 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
310
438
  onActiveAssetData(data) {
311
439
  if (!data.coin) return;
312
440
  if (!this.subscribedCoins.has(data.coin)) return;
441
+ const previousMargin = this.availableMargin();
313
442
  this.trackedAssetData.set(data.coin, data);
314
443
  const trackedAssets = /* @__PURE__ */ new Map();
315
444
  trackedAssets.set(data.coin, buildTrackedAssetInfo(data));
316
445
  this.onStateUpdate({
317
446
  trackedAssets
318
447
  });
448
+ if (this.availableMargin() === previousMargin) {
449
+ return;
450
+ }
451
+ const balance = this.buildBalance();
452
+ if (balance !== void 0) {
453
+ this.onStateUpdate({
454
+ balance
455
+ });
456
+ }
457
+ }
458
+ onAllMids(data) {
459
+ const mids = data?.mids;
460
+ if (!mids) return;
461
+ this.prices = SpotPrices.from(this.spotMeta, mids);
462
+ const balance = this.buildBalance();
463
+ if (balance !== void 0) {
464
+ this.onStateUpdate({
465
+ balance
466
+ });
467
+ }
319
468
  }
320
469
  onWebData3(data) {
321
470
  const previousAccountType = this.accountType;
@@ -329,6 +478,8 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
329
478
  this.resetLiveAccountState();
330
479
  }
331
480
  this.accountType = nextAccountType;
481
+ this.vaultEquity = sumLiveVaultEquity(data.perpDexStates ?? []);
482
+ this.hasWebData3 = true;
332
483
  if (!nextAccountType) {
333
484
  this.onStateUpdate({
334
485
  balance: null
@@ -346,6 +497,8 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
346
497
  const balances = data?.spotState?.balances;
347
498
  if (!balances) return;
348
499
  this.spotCoinBalances = mapSpotBalances(data.spotState);
500
+ this.portfolioMarginRatio = data.spotState.portfolioMarginRatio ?? null;
501
+ this.borrowRatioByToken = mapBorrowRatios(data.spotState);
349
502
  this.hasSpotState = true;
350
503
  const balance = this.buildBalance();
351
504
  if (balance !== void 0) {
@@ -354,21 +507,47 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
354
507
  });
355
508
  }
356
509
  }
510
+ /**
511
+ * Builds the live balance once every channel that feeds it has reported, or `undefined` until then.
512
+ *
513
+ * All four are required. Every mode now values the spot wallet, so a balance emitted before
514
+ * `spotState` and `allMids` would report equity missing its whole spot half; and one emitted
515
+ * before `webData3` would report vault equity as zero on an account that holds millions in a
516
+ * vault. Partial equity is worse than no equity, so nothing is published early.
517
+ */
357
518
  buildBalance() {
358
- if (!this.hasClearinghouseState || !this.accountType) {
359
- return void 0;
360
- }
361
- if ((this.accountType === HyperliquidAccountType.UNIFIED_ACCOUNT || this.accountType === HyperliquidAccountType.PORTFOLIO_MARGIN) && !this.hasSpotState) {
519
+ if (!this.hasClearinghouseState || !this.hasSpotState || !this.hasWebData3 || !this.prices || !this.accountType) {
362
520
  return void 0;
363
521
  }
364
- return mapBalance(this.lastSummary, this.spotCoinBalances, this.accountType, this.collateralTokenMap);
522
+ return mapBalance({
523
+ accountType: this.accountType,
524
+ summary: this.lastSummary,
525
+ spotBalances: this.spotCoinBalances,
526
+ prices: this.prices,
527
+ vaultEquity: this.vaultEquity,
528
+ stakedHype: this.stakedHype,
529
+ availableMargin: this.availableMargin(),
530
+ collateralTokenNames: this.collateralTokenNames,
531
+ availableMarginByToken: buildAvailableMarginByToken(this.lastSummary.perDexState, this.trackedAssetData.values()),
532
+ portfolioMarginRatio: this.portfolioMarginRatio,
533
+ borrowRatioByToken: this.borrowRatioByToken,
534
+ timestamp: Date.now()
535
+ });
536
+ }
537
+ /** The floor of `availableToTrade` over the tracked set; `null` when nothing is tracked. */
538
+ availableMargin() {
539
+ return minimumAvailableToTrade(this.trackedAssetData.values());
365
540
  }
366
541
  /** Clears both sides of the live balance join and the public account cache without emitting. */
367
542
  resetLiveAccountState() {
368
543
  this.hasSpotState = false;
369
544
  this.hasClearinghouseState = false;
370
- this.lastSummary = emptySummary();
545
+ this.hasWebData3 = false;
546
+ this.vaultEquity = "0";
547
+ this.lastSummary = emptyClearinghouseSummary();
371
548
  this.spotCoinBalances.clear();
549
+ this.portfolioMarginRatio = null;
550
+ this.borrowRatioByToken.clear();
372
551
  this.resetAccountState();
373
552
  }
374
553
  buildTrackedAssetsMap() {
@@ -379,6 +558,18 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
379
558
  return result;
380
559
  }
381
560
  }
561
+ function sizeBasket(input, dexToMeta, tokenNames, assetData) {
562
+ const markets = new Map(input.legs.map((leg) => [
563
+ leg.asset,
564
+ findLegMarket(dexToMeta, tokenNames, leg.asset, assetData.get(leg.asset)?.leverage.value ?? null)
565
+ ]));
566
+ const budgets = buildPoolBudgets(input.legs, markets, assetData);
567
+ return computeMaxBasketSize({
568
+ budgets,
569
+ legs: buildCappedLegs(input.legs, markets)
570
+ });
571
+ }
572
+ __name(sizeBasket, "sizeBasket");
382
573
  function buildTrackedAssetInfo(data) {
383
574
  return {
384
575
  coin: data.coin,
@@ -387,18 +578,6 @@ function buildTrackedAssetInfo(data) {
387
578
  };
388
579
  }
389
580
  __name(buildTrackedAssetInfo, "buildTrackedAssetInfo");
390
- const HYPERLIQUID_ACCOUNT_TYPES = Object.values(HyperliquidAccountType);
391
- function toHyperliquidAccountType(abstraction) {
392
- if (!abstraction) {
393
- return null;
394
- }
395
- return HYPERLIQUID_ACCOUNT_TYPES.includes(abstraction) ? abstraction : null;
396
- }
397
- __name(toHyperliquidAccountType, "toHyperliquidAccountType");
398
- function resolveHyperliquidAccountType(abstraction) {
399
- return toHyperliquidAccountType(abstraction);
400
- }
401
- __name(resolveHyperliquidAccountType, "resolveHyperliquidAccountType");
402
581
  function uniqueDexDescriptors(perpDexs, allMetas) {
403
582
  const seen = /* @__PURE__ */ new Set();
404
583
  return perpDexs.flatMap((descriptor, index) => {
@@ -418,9 +597,8 @@ function uniqueDexDescriptors(perpDexs, allMetas) {
418
597
  __name(uniqueDexDescriptors, "uniqueDexDescriptors");
419
598
  function mapSpotBalances(spotState) {
420
599
  const balances = /* @__PURE__ */ new Map();
421
- const availableAfterMaintenance = new Map(spotState.tokenToAvailableAfterMaintenance ?? []);
422
600
  for (const balance of spotState.balances) {
423
- if (new BigNumber(balance.total || "0").isZero() && new BigNumber(balance.hold || "0").isZero()) {
601
+ if (toDecimal(balance.total).isZero() && toDecimal(balance.hold).isZero()) {
424
602
  continue;
425
603
  }
426
604
  balances.set(balance.coin, {
@@ -428,48 +606,29 @@ function mapSpotBalances(spotState) {
428
606
  token: balance.token,
429
607
  total: balance.total,
430
608
  hold: balance.hold,
431
- availableAfterMaintenance: availableAfterMaintenance.get(balance.token)
609
+ // An absent `ltv` means "not collateral", which weighs zero — never a zero weight, which is
610
+ // a stablecoin at full value. The two states stay apart from the wire onward.
611
+ ltv: balance.ltv ?? null
432
612
  });
433
613
  }
434
614
  return balances;
435
615
  }
436
616
  __name(mapSpotBalances, "mapSpotBalances");
437
- function emptySummary() {
438
- return {
439
- accountValue: "0",
440
- totalMarginUsed: "0",
441
- totalRawUsd: "0",
442
- crossMaintenanceMarginUsed: "0",
443
- isolatedMaintenanceMarginUsed: "0",
444
- maximumStandardMarginRatio: "0.0000",
445
- withdrawable: "0",
446
- totalUnrealizedPnl: "0",
447
- byCollateralToken: /* @__PURE__ */ new Map()
448
- };
617
+ function mapBorrowRatios(spotState) {
618
+ return new Map(spotState.tokenToPortfolioBorrowRatio ?? []);
449
619
  }
450
- __name(emptySummary, "emptySummary");
451
- function buildCollateralTokenMap(allMetas, spotMeta) {
452
- const spotTokenNames = new Map(spotMeta.tokens.map((token) => [
453
- token.index,
454
- token.name
455
- ]));
456
- const result = /* @__PURE__ */ new Map();
457
- for (const meta of allMetas) {
458
- const token = meta.collateralToken;
459
- const coin = spotTokenNames.get(token) ?? COLLATERAL_TOKEN_MAP[token];
460
- if (coin) {
461
- result.set(token, coin);
462
- }
463
- }
464
- return result;
620
+ __name(mapBorrowRatios, "mapBorrowRatios");
621
+ function stakingAccountHype(delegator) {
622
+ return toDecimal(delegator.delegated).plus(toDecimal(delegator.undelegated)).toFixed();
465
623
  }
466
- __name(buildCollateralTokenMap, "buildCollateralTokenMap");
467
- function legacyCollateralTokenMap() {
468
- return new Map(Object.entries(COLLATERAL_TOKEN_MAP).map(([token, coin]) => [
469
- Number(token),
470
- coin
471
- ]));
624
+ __name(stakingAccountHype, "stakingAccountHype");
625
+ function sumRestVaultEquity(vaults) {
626
+ return vaults.reduce((total, vault) => total.plus(toDecimal(vault.equity)), ZERO).toFixed();
627
+ }
628
+ __name(sumRestVaultEquity, "sumRestVaultEquity");
629
+ function sumLiveVaultEquity(perpDexStates) {
630
+ return perpDexStates.reduce((total, state) => total.plus(toDecimal(state?.totalVaultEquity)), ZERO).toFixed();
472
631
  }
473
- __name(legacyCollateralTokenMap, "legacyCollateralTokenMap");
632
+ __name(sumLiveVaultEquity, "sumLiveVaultEquity");
474
633
 
475
634
  export { HyperliquidAccountTracker };
@@ -5,11 +5,5 @@ var HyperliquidAccountType = /* @__PURE__ */ (function(HyperliquidAccountType2)
5
5
  HyperliquidAccountType2["UNIFIED_ACCOUNT"] = "unifiedAccount";
6
6
  return HyperliquidAccountType2;
7
7
  })({});
8
- const COLLATERAL_TOKEN_MAP = {
9
- 0: "USDC",
10
- 235: "USDE",
11
- 268: "USDT0",
12
- 360: "USDH"
13
- };
14
8
 
15
- export { COLLATERAL_TOKEN_MAP, HyperliquidAccountType };
9
+ export { HyperliquidAccountType };
@@ -8,12 +8,15 @@ import { LighterAccountTracker } from './lighter/tracker';
8
8
  export { LighterAccountTracker } from './lighter/tracker';
9
9
  import { OkxAccountTracker } from './okx/tracker';
10
10
  export { OkxAccountTracker } from './okx/tracker';
11
+ export { AccountBalance } from './balance';
12
+ export { ACCOUNT_MODE_LABELS, AccountBalanceCore, AccountMode, CollateralBalance, DecimalString, MarginMode, Side, SignedDecimalString } from './balance-core';
11
13
  export { BaseAccountTracker } from './base';
12
- export { BinanceAccountType } from './binance/types';
13
- export { BybitAccountType } from './bybit/types';
14
- export { HyperliquidAccountType } from './hyperliquid/types';
15
- export { LighterAccountType } from './lighter/types';
16
- export { OkxAccountType } from './okx/types';
14
+ export { BasketLeg, MaxBasketSizeInput, MaxBasketSizeResult, computeMaxBasketSize } from './basket-size';
15
+ export { BinanceMultiAssetBalance, BinanceSingleAssetBalance } from './binance/balance';
16
+ export { BybitIsolatedMarginBalance, BybitPortfolioMarginBalance, BybitRegularMarginBalance } from './bybit/balance';
17
+ export { HyperliquidDexState, HyperliquidPortfolioMarginBalance, HyperliquidPortfolioMarginCollateral, HyperliquidStandardBalance, HyperliquidUnifiedBalance } from './hyperliquid/balance';
18
+ export { LighterClassicBalance, LighterUnifiedBalance } from './lighter/balance';
19
+ export { OkxFuturesBalance, OkxMultiCurrencyMarginBalance, OkxPortfolioMarginBalance, OkxSpotBalance } from './okx/balance';
17
20
  export * from './types';
18
21
 
19
22
  var __defProp = Object.defineProperty;
@@ -50,8 +53,7 @@ function createAccountTracker(exchange, config) {
50
53
  demo: config.demo,
51
54
  onError: config.onError,
52
55
  account: config.account,
53
- credentials: config.credentials,
54
- sdk: config.sdk
56
+ credentials: config.credentials
55
57
  });
56
58
  default: {
57
59
  const _exhaustive = exchange;