@pear-protocol/exchanges-sdk 0.6.7 → 0.8.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/README.md +105 -25
- package/dist/client/bybit/rest.js +8 -1
- package/dist/client/hyperliquid/rest.js +21 -1
- package/dist/client/lighter/rest.js +6 -12
- package/dist/client/okx/rest.js +5 -1
- package/dist/index.d.ts +1131 -78
- package/dist/sdk.js +9 -2
- package/dist/syncer/base.js +36 -14
- package/dist/tracker/balance-core.js +89 -0
- package/dist/tracker/balance.js +25 -0
- package/dist/tracker/basket-size.js +194 -0
- package/dist/tracker/binance/balance.js +11 -0
- package/dist/tracker/binance/mapper.js +87 -42
- package/dist/tracker/binance/tracker.js +29 -14
- package/dist/tracker/binance/types.js +0 -6
- package/dist/tracker/bybit/balance.js +14 -0
- package/dist/tracker/bybit/mapper.js +109 -52
- package/dist/tracker/bybit/tracker.js +242 -33
- package/dist/tracker/bybit/types.js +0 -7
- package/dist/tracker/hyperliquid/account-mode.js +14 -0
- package/dist/tracker/hyperliquid/balance.js +184 -0
- package/dist/tracker/hyperliquid/basket-size.js +93 -0
- package/dist/tracker/hyperliquid/coin-name.js +18 -0
- package/dist/tracker/hyperliquid/collateral.js +48 -0
- package/dist/tracker/hyperliquid/decimal.js +28 -0
- package/dist/tracker/hyperliquid/mapper.js +399 -176
- package/dist/tracker/hyperliquid/spot-pricing.js +106 -0
- package/dist/tracker/hyperliquid/tracker.js +237 -78
- package/dist/tracker/hyperliquid/types.js +1 -7
- package/dist/tracker/index.js +9 -7
- package/dist/tracker/lighter/balance.js +87 -0
- package/dist/tracker/lighter/basket-size.js +74 -0
- package/dist/tracker/lighter/mapper.js +324 -145
- package/dist/tracker/lighter/tracker.js +228 -117
- package/dist/tracker/lighter/types.js +1 -7
- package/dist/tracker/okx/balance.js +17 -0
- package/dist/tracker/okx/mapper.js +156 -58
- package/dist/tracker/okx/tracker.js +228 -29
- package/dist/tracker/okx/types.js +0 -8
- package/dist/tracker/types.js +0 -24
- package/package.json +7 -9
|
@@ -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 {
|
|
7
|
-
import {
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
75
|
-
|
|
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
|
-
|
|
81
|
-
const
|
|
82
|
-
result[coin] =
|
|
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.
|
|
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(
|
|
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.
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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(
|
|
451
|
-
function
|
|
452
|
-
|
|
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(
|
|
467
|
-
function
|
|
468
|
-
return
|
|
469
|
-
|
|
470
|
-
|
|
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(
|
|
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 {
|
|
9
|
+
export { HyperliquidAccountType };
|
package/dist/tracker/index.js
CHANGED
|
@@ -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 {
|
|
13
|
-
export {
|
|
14
|
-
export {
|
|
15
|
-
export {
|
|
16
|
-
export {
|
|
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;
|