@pear-protocol/exchanges-sdk 0.15.0 → 0.17.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 +32 -0
- package/dist/client/base-ws.js +18 -0
- package/dist/client/hyperliquid/rest.js +14 -1
- package/dist/index.d.ts +144 -24
- package/dist/sdk.js +15 -1
- package/dist/syncer/base.js +7 -5
- package/dist/tracker/base.js +0 -2
- package/dist/tracker/binance/tracker.js +1 -1
- package/dist/tracker/bybit/tracker.js +1 -1
- package/dist/tracker/hyperliquid/balance.js +37 -2
- package/dist/tracker/hyperliquid/borrow-lend.js +32 -0
- package/dist/tracker/hyperliquid/mapper.js +53 -1
- package/dist/tracker/hyperliquid/tracker.js +249 -11
- package/dist/tracker/index.js +2 -3
- package/dist/tracker/lighter/tracker.js +5 -4
- package/dist/tracker/okx/tracker.js +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -103,6 +103,38 @@ Hyperliquid status requires the builder address and the minimum approved fee in
|
|
|
103
103
|
|
|
104
104
|
Historical Hyperliquid fill synchronization identifies fills by bare `tid` and requests them aggregated by time, so partial fills of one crossing order arrive as a single fill. Consumers that persisted composite `time:coin:tid` identifiers must account for the transition overlap. Hyperliquid serves only the 10,000 most recent fills; an account with a longer history imports from that boundary forward, and the snapshot-alignment synthetics cover the residual gap. Synchronization fails closed when the exchange reports a maker rebate: the current core external-fill contract accepts only non-negative fees, so the SDK will not silently turn a rebate into a cost or advance the synchronization watermark past it.
|
|
105
105
|
|
|
106
|
+
## Public Tracker (address only)
|
|
107
|
+
|
|
108
|
+
Hyperliquid answers account state for any address with no key, so an account the caller does not own, such as a vault's agent, can be tracked without `connect()`. `createPublicTracker` fetches no credentials and needs no trade account:
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
const tracker = exchanges.createPublicTracker({
|
|
112
|
+
connector: 'hyperliquid',
|
|
113
|
+
address: '0x...',
|
|
114
|
+
onError: (error) => console.warn(error),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await tracker.start(); // rejects if the venue reads or the socket fail
|
|
118
|
+
const offPositions = tracker.trackPosition((positions) => {
|
|
119
|
+
// Main-dex and HIP-3 positions together, e.g. `BTC` and `xyz:TSLA`
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const { balance, positions } = await tracker.snapshot(); // REST, every perp dex, no socket
|
|
123
|
+
|
|
124
|
+
offPositions();
|
|
125
|
+
await tracker.disconnect();
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
It is the same `HyperliquidAccountTracker` the trade-account path builds, so live updates, reconnects, snapshots and cleanup behave identically. It only reads: nothing on it can trade.
|
|
129
|
+
|
|
130
|
+
A read that has not landed never looks like an empty account. `trackPosition` first fires once the first `allDexsClearinghouseState` frame arrives, and `isInitialized` stays `false` until then. When the socket drops, the tracker clears its live state (`isInitialized` turns `false`, `getPositions()` returns `[]`) and reports the outage to `onError` once, however many reconnect attempts fail; listeners are not sent an empty list. Once the socket reopens it resubscribes, and `isInitialized` turns `true` again with the first fresh frame. A subscription the venue refuses, and a frame that cannot be mapped, also reach `onError`.
|
|
131
|
+
|
|
132
|
+
To build one directly, `HyperliquidAccountTracker` now takes the address itself:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
new HyperliquidAccountTracker({ demo: false, address: '0x...', onError });
|
|
136
|
+
```
|
|
137
|
+
|
|
106
138
|
## Account Status
|
|
107
139
|
|
|
108
140
|
Use `status(connection)` to check whether the account is ready for trading through PEAR.
|
package/dist/client/base-ws.js
CHANGED
|
@@ -10,6 +10,7 @@ class BaseWs {
|
|
|
10
10
|
pingTimer = null;
|
|
11
11
|
messageListeners = /* @__PURE__ */ new Map();
|
|
12
12
|
openListeners = /* @__PURE__ */ new Map();
|
|
13
|
+
closeListeners = /* @__PURE__ */ new Map();
|
|
13
14
|
nextListenerId = 0;
|
|
14
15
|
async connect() {
|
|
15
16
|
if (this.ws) return;
|
|
@@ -42,6 +43,13 @@ class BaseWs {
|
|
|
42
43
|
});
|
|
43
44
|
ws.addEventListener("close", () => {
|
|
44
45
|
this.stopPing();
|
|
46
|
+
for (const listener of this.closeListeners.values()) {
|
|
47
|
+
try {
|
|
48
|
+
listener();
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.warn("Error in ws close listener", error);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
45
53
|
});
|
|
46
54
|
ws.addEventListener("error", () => {
|
|
47
55
|
});
|
|
@@ -57,6 +65,7 @@ class BaseWs {
|
|
|
57
65
|
}, "onOpen");
|
|
58
66
|
const onError = /* @__PURE__ */ __name(() => {
|
|
59
67
|
ws.removeEventListener("open", onOpen);
|
|
68
|
+
this.disconnect();
|
|
60
69
|
reject(new Error("WebSocket connection failed"));
|
|
61
70
|
}, "onError");
|
|
62
71
|
ws.addEventListener("open", onOpen, {
|
|
@@ -98,10 +107,19 @@ class BaseWs {
|
|
|
98
107
|
removeOpenListener(id) {
|
|
99
108
|
this.openListeners.delete(id);
|
|
100
109
|
}
|
|
110
|
+
addCloseListener(listener) {
|
|
111
|
+
const id = String(++this.nextListenerId);
|
|
112
|
+
this.closeListeners.set(id, listener);
|
|
113
|
+
return id;
|
|
114
|
+
}
|
|
115
|
+
removeCloseListener(id) {
|
|
116
|
+
this.closeListeners.delete(id);
|
|
117
|
+
}
|
|
101
118
|
destroy() {
|
|
102
119
|
this.disconnect();
|
|
103
120
|
this.messageListeners.clear();
|
|
104
121
|
this.openListeners.clear();
|
|
122
|
+
this.closeListeners.clear();
|
|
105
123
|
}
|
|
106
124
|
startPing() {
|
|
107
125
|
this.stopPing();
|
|
@@ -86,6 +86,19 @@ async function fetchUserVaultEquities(user, demo = false) {
|
|
|
86
86
|
}, demo);
|
|
87
87
|
}
|
|
88
88
|
__name(fetchUserVaultEquities, "fetchUserVaultEquities");
|
|
89
|
+
async function fetchBorrowLendUserState(user, demo = false) {
|
|
90
|
+
return fetchInfo({
|
|
91
|
+
type: "borrowLendUserState",
|
|
92
|
+
user
|
|
93
|
+
}, demo);
|
|
94
|
+
}
|
|
95
|
+
__name(fetchBorrowLendUserState, "fetchBorrowLendUserState");
|
|
96
|
+
async function fetchAllBorrowLendReserveStates(demo = false) {
|
|
97
|
+
return fetchInfo({
|
|
98
|
+
type: "allBorrowLendReserveStates"
|
|
99
|
+
}, demo);
|
|
100
|
+
}
|
|
101
|
+
__name(fetchAllBorrowLendReserveStates, "fetchAllBorrowLendReserveStates");
|
|
89
102
|
async function fetchActiveAssetData(user, coin, demo = false) {
|
|
90
103
|
return fetchInfo({
|
|
91
104
|
type: "activeAssetData",
|
|
@@ -95,4 +108,4 @@ async function fetchActiveAssetData(user, coin, demo = false) {
|
|
|
95
108
|
}
|
|
96
109
|
__name(fetchActiveAssetData, "fetchActiveAssetData");
|
|
97
110
|
|
|
98
|
-
export { HyperliquidInfoRequestError, fetchActiveAssetData, fetchAllMids, fetchClearinghouseState, fetchDelegatorSummary, fetchInfo, fetchPerpDexs, fetchSpotClearinghouseState, fetchUserAbstraction, fetchUserRole, fetchUserVaultEquities };
|
|
111
|
+
export { HyperliquidInfoRequestError, fetchActiveAssetData, fetchAllBorrowLendReserveStates, fetchAllMids, fetchBorrowLendUserState, fetchClearinghouseState, fetchDelegatorSummary, fetchInfo, fetchPerpDexs, fetchSpotClearinghouseState, fetchUserAbstraction, fetchUserRole, fetchUserVaultEquities };
|
package/dist/index.d.ts
CHANGED
|
@@ -54,17 +54,28 @@ type ConnectOptions = {
|
|
|
54
54
|
integratorMinPerpsTakerFee?: number;
|
|
55
55
|
};
|
|
56
56
|
type ExchangeConnection = HttpTypes.ExchangeCredentialsResponse;
|
|
57
|
+
/**
|
|
58
|
+
* A read-only tracker for an address the caller need not own, such as a vault's agent. Only
|
|
59
|
+
* Hyperliquid publishes account state by address, so it is the only connector here.
|
|
60
|
+
*/
|
|
61
|
+
type PublicTrackerConfig = {
|
|
62
|
+
connector: 'hyperliquid';
|
|
63
|
+
address: `0x${string}`;
|
|
64
|
+
onError?: (error: Error) => void;
|
|
65
|
+
};
|
|
57
66
|
|
|
58
67
|
interface WsMessage {
|
|
59
68
|
[key: string]: unknown;
|
|
60
69
|
}
|
|
61
70
|
type MessageListener = (data: string) => void;
|
|
62
71
|
type OpenListener = () => void;
|
|
72
|
+
type CloseListener = () => void;
|
|
63
73
|
declare abstract class BaseWs {
|
|
64
74
|
protected ws: WebSocket | null;
|
|
65
75
|
protected pingTimer: ReturnType<typeof setInterval> | null;
|
|
66
76
|
private messageListeners;
|
|
67
77
|
private openListeners;
|
|
78
|
+
private closeListeners;
|
|
68
79
|
private nextListenerId;
|
|
69
80
|
protected abstract getWsUrl(): string;
|
|
70
81
|
protected abstract getPingIntervalMs(): number;
|
|
@@ -77,6 +88,8 @@ declare abstract class BaseWs {
|
|
|
77
88
|
removeMessageListener(id: string): void;
|
|
78
89
|
addOpenListener(listener: OpenListener): string;
|
|
79
90
|
removeOpenListener(id: string): void;
|
|
91
|
+
addCloseListener(listener: CloseListener): string;
|
|
92
|
+
removeCloseListener(id: string): void;
|
|
80
93
|
destroy(): void;
|
|
81
94
|
private startPing;
|
|
82
95
|
private stopPing;
|
|
@@ -240,11 +253,19 @@ type ExchangeWsByConnector = {
|
|
|
240
253
|
lighter: LighterExchangeWs;
|
|
241
254
|
lighter_rhc: LighterExchangeWs;
|
|
242
255
|
};
|
|
243
|
-
|
|
256
|
+
/**
|
|
257
|
+
* Hyperliquid's account channels are public, so its socket needs no credentials; every other venue
|
|
258
|
+
* still requires them. The tuple keeps the check whole, so a connector not known to be Hyperliquid
|
|
259
|
+
* (a `Connector` union) still requires them too.
|
|
260
|
+
*/
|
|
261
|
+
type CreateExchangeWsConfig<E extends Connector = Connector> = {
|
|
244
262
|
demo: boolean;
|
|
263
|
+
} & ([E] extends ['hyperliquid'] ? {
|
|
264
|
+
credentials?: SupportedCredentials;
|
|
265
|
+
} : {
|
|
245
266
|
credentials: SupportedCredentials;
|
|
246
|
-
};
|
|
247
|
-
declare function createExchangeWs<E extends Connector>(exchange: E, config: CreateExchangeWsConfig): Promise<ExchangeWsByConnector[E]>;
|
|
267
|
+
});
|
|
268
|
+
declare function createExchangeWs<E extends Connector>(exchange: E, config: CreateExchangeWsConfig<E>): Promise<ExchangeWsByConnector[E]>;
|
|
248
269
|
|
|
249
270
|
interface HyperliquidHelper {
|
|
250
271
|
checkAccountActivation(userAddress: `0x${string}`): Promise<boolean>;
|
|
@@ -1447,6 +1468,7 @@ declare const AccountBalance: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
1447
1468
|
withdrawable: z.ZodString;
|
|
1448
1469
|
ltv: z.ZodNullable<z.ZodString>;
|
|
1449
1470
|
borrowCapUsed: z.ZodNullable<z.ZodString>;
|
|
1471
|
+
borrowed: z.ZodString;
|
|
1450
1472
|
}, z.core.$strip>>;
|
|
1451
1473
|
tradingEquity: z.ZodNullable<z.ZodString>;
|
|
1452
1474
|
vaultEquity: z.ZodString;
|
|
@@ -1458,6 +1480,13 @@ declare const AccountBalance: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
1458
1480
|
portfolioMarginRatio: z.ZodNullable<z.ZodString>;
|
|
1459
1481
|
portfolioAccountLeverage: z.ZodNullable<z.ZodString>;
|
|
1460
1482
|
borrowCapUsed: z.ZodNullable<z.ZodString>;
|
|
1483
|
+
borrowedValue: z.ZodNullable<z.ZodString>;
|
|
1484
|
+
healthFactor: z.ZodNullable<z.ZodString>;
|
|
1485
|
+
borrowLendReserves: z.ZodArray<z.ZodObject<{
|
|
1486
|
+
asset: z.ZodString;
|
|
1487
|
+
borrowYearlyRate: z.ZodString;
|
|
1488
|
+
supplyYearlyRate: z.ZodString;
|
|
1489
|
+
}, z.core.$strip>>;
|
|
1461
1490
|
perDexState: z.ZodArray<z.ZodObject<{
|
|
1462
1491
|
dex: z.ZodString;
|
|
1463
1492
|
collateralToken: z.ZodNullable<z.ZodNumber>;
|
|
@@ -1772,9 +1801,12 @@ interface AccountSnapshot {
|
|
|
1772
1801
|
type AssetSnapshotMap = Record<string, TrackedAssetInfo | null>;
|
|
1773
1802
|
|
|
1774
1803
|
type ErrorHandler = (error: Error) => void;
|
|
1804
|
+
/**
|
|
1805
|
+
* What every tracker takes. Credentials are not part of it: a tracker that reads private venue
|
|
1806
|
+
* channels declares them in its own config, while Hyperliquid reads public account state by address.
|
|
1807
|
+
*/
|
|
1775
1808
|
type AccountTrackerConfig = {
|
|
1776
1809
|
demo: boolean;
|
|
1777
|
-
credentials: SupportedCredentials;
|
|
1778
1810
|
onError?: ErrorHandler;
|
|
1779
1811
|
};
|
|
1780
1812
|
interface AccountTracker {
|
|
@@ -1795,7 +1827,6 @@ interface AccountTracker {
|
|
|
1795
1827
|
declare abstract class BaseAccountTracker implements AccountTracker {
|
|
1796
1828
|
protected onError: ErrorHandler;
|
|
1797
1829
|
protected demo: boolean;
|
|
1798
|
-
protected credentials: SupportedCredentials;
|
|
1799
1830
|
private balance;
|
|
1800
1831
|
private balanceInitialized;
|
|
1801
1832
|
private positions;
|
|
@@ -2174,8 +2205,9 @@ type HyperliquidUnifiedBalance = z.infer<typeof HyperliquidUnifiedBalance>;
|
|
|
2174
2205
|
* accounts measured hold one — `0x0871deb3…` reports USDC at −957,802.15. An unsigned row cannot
|
|
2175
2206
|
* state that at all, which is the defect that made a borrowed pool indistinguishable from an
|
|
2176
2207
|
* untouched one.
|
|
2177
|
-
* - **
|
|
2178
|
-
* beside the quantities, and both are PM-only wire fields.
|
|
2208
|
+
* - **three more columns.** `ltv` and `borrowCapUsed` are per-coin risk weights the venue renders
|
|
2209
|
+
* beside the quantities, and both are PM-only wire fields. `borrowed`, the coin's outstanding
|
|
2210
|
+
* debt, is the third, and PM-only as well.
|
|
2179
2211
|
*
|
|
2180
2212
|
* The row is otherwise the shared shape: same keys, same meanings, so a consumer reading
|
|
2181
2213
|
* `perCollateral` without narrowing still reads `asset` / `equity` / `availableMargin` /
|
|
@@ -2188,8 +2220,16 @@ declare const HyperliquidPortfolioMarginCollateral: z.ZodObject<{
|
|
|
2188
2220
|
withdrawable: z.ZodString;
|
|
2189
2221
|
ltv: z.ZodNullable<z.ZodString>;
|
|
2190
2222
|
borrowCapUsed: z.ZodNullable<z.ZodString>;
|
|
2223
|
+
borrowed: z.ZodString;
|
|
2191
2224
|
}, z.core.$strip>;
|
|
2192
2225
|
type HyperliquidPortfolioMarginCollateral = z.infer<typeof HyperliquidPortfolioMarginCollateral>;
|
|
2226
|
+
/** One borrow/lend reserve's yearly rates, named by its coin's wire name. */
|
|
2227
|
+
declare const HyperliquidBorrowLendReserve: z.ZodObject<{
|
|
2228
|
+
asset: z.ZodString;
|
|
2229
|
+
borrowYearlyRate: z.ZodString;
|
|
2230
|
+
supplyYearlyRate: z.ZodString;
|
|
2231
|
+
}, z.core.$strip>;
|
|
2232
|
+
type HyperliquidBorrowLendReserve = z.infer<typeof HyperliquidBorrowLendReserve>;
|
|
2193
2233
|
declare const HyperliquidPortfolioMarginBalance: z.ZodObject<{
|
|
2194
2234
|
totalEquity: z.ZodNullable<z.ZodString>;
|
|
2195
2235
|
withdrawableBalance: z.ZodNullable<z.ZodString>;
|
|
@@ -2204,6 +2244,7 @@ declare const HyperliquidPortfolioMarginBalance: z.ZodObject<{
|
|
|
2204
2244
|
withdrawable: z.ZodString;
|
|
2205
2245
|
ltv: z.ZodNullable<z.ZodString>;
|
|
2206
2246
|
borrowCapUsed: z.ZodNullable<z.ZodString>;
|
|
2247
|
+
borrowed: z.ZodString;
|
|
2207
2248
|
}, z.core.$strip>>;
|
|
2208
2249
|
tradingEquity: z.ZodNullable<z.ZodString>;
|
|
2209
2250
|
vaultEquity: z.ZodString;
|
|
@@ -2215,6 +2256,13 @@ declare const HyperliquidPortfolioMarginBalance: z.ZodObject<{
|
|
|
2215
2256
|
portfolioMarginRatio: z.ZodNullable<z.ZodString>;
|
|
2216
2257
|
portfolioAccountLeverage: z.ZodNullable<z.ZodString>;
|
|
2217
2258
|
borrowCapUsed: z.ZodNullable<z.ZodString>;
|
|
2259
|
+
borrowedValue: z.ZodNullable<z.ZodString>;
|
|
2260
|
+
healthFactor: z.ZodNullable<z.ZodString>;
|
|
2261
|
+
borrowLendReserves: z.ZodArray<z.ZodObject<{
|
|
2262
|
+
asset: z.ZodString;
|
|
2263
|
+
borrowYearlyRate: z.ZodString;
|
|
2264
|
+
supplyYearlyRate: z.ZodString;
|
|
2265
|
+
}, z.core.$strip>>;
|
|
2218
2266
|
perDexState: z.ZodArray<z.ZodObject<{
|
|
2219
2267
|
dex: z.ZodString;
|
|
2220
2268
|
collateralToken: z.ZodNullable<z.ZodNumber>;
|
|
@@ -2226,36 +2274,30 @@ declare const HyperliquidPortfolioMarginBalance: z.ZodObject<{
|
|
|
2226
2274
|
}, z.core.$strip>;
|
|
2227
2275
|
type HyperliquidPortfolioMarginBalance = z.infer<typeof HyperliquidPortfolioMarginBalance>;
|
|
2228
2276
|
|
|
2229
|
-
/**
|
|
2230
|
-
* Hyperliquid's two read paths: the live WebSocket tracker and the one-shot REST snapshot.
|
|
2231
|
-
*
|
|
2232
|
-
* Both fill the same `HyperliquidBalanceContext` and hand it to the same mapper, which is what
|
|
2233
|
-
* makes "same account state, same balance" true across two different transports. The tracker takes
|
|
2234
|
-
* every CHANGING value off a channel — `allDexsClearinghouseState`, `spotState`, `webData3`,
|
|
2235
|
-
* `allMids`, `activeAssetData` — including vault equity, which rides `webData3`. Its only REST
|
|
2236
|
-
* reads happen at `start()`: the static dex metadata, and the staking position, which the venue
|
|
2237
|
-
* publishes on no channel at all.
|
|
2238
|
-
*
|
|
2239
|
-
* The snapshot has no channels, so it reads the same figures over REST — vault equity from
|
|
2240
|
-
* `userVaultEquities`, which was measured equal to the `webData3` figure on ten live accounts.
|
|
2241
|
-
*/
|
|
2242
|
-
|
|
2243
2277
|
type HyperliquidAccountTrackerConfig = AccountTrackerConfig & {
|
|
2244
|
-
account
|
|
2245
|
-
|
|
2278
|
+
/** The account whose state is read. Hyperliquid answers for any address, so no credentials are needed. */
|
|
2279
|
+
address: `0x${string}`;
|
|
2246
2280
|
};
|
|
2247
2281
|
declare class HyperliquidAccountTracker extends BaseAccountTracker {
|
|
2248
2282
|
private readonly address;
|
|
2249
2283
|
private ws;
|
|
2250
2284
|
private messageListenerId;
|
|
2251
2285
|
private openListenerId;
|
|
2286
|
+
private closeListenerId;
|
|
2252
2287
|
private lifecycleVersion;
|
|
2288
|
+
/** Whether the socket is down and the outage already reported, so its retries report nothing more. */
|
|
2289
|
+
private socketDropped;
|
|
2253
2290
|
private subscribedCoins;
|
|
2254
2291
|
private lastSummary;
|
|
2255
2292
|
private spotCoinBalances;
|
|
2256
2293
|
/** Portfolio Margin's own risk scalars, which ride the same `spotState` frame the balances do. */
|
|
2257
2294
|
private portfolioMarginRatio;
|
|
2258
2295
|
private borrowRatioByToken;
|
|
2296
|
+
/** Portfolio Margin's REST-only figures, and the timer that keeps them fresh while in that mode. */
|
|
2297
|
+
private borrowLend;
|
|
2298
|
+
private borrowLendPollTimer;
|
|
2299
|
+
/** Advances on every stop, so a read fired in an earlier poll period is recognised and dropped. */
|
|
2300
|
+
private borrowLendPollGeneration;
|
|
2259
2301
|
private hasSpotState;
|
|
2260
2302
|
private hasClearinghouseState;
|
|
2261
2303
|
private accountType;
|
|
@@ -2265,6 +2307,17 @@ declare class HyperliquidAccountTracker extends BaseAccountTracker {
|
|
|
2265
2307
|
private hasWebData3;
|
|
2266
2308
|
private vaultEquity;
|
|
2267
2309
|
private stakedHype;
|
|
2310
|
+
/** Whether a staking re-read is running, whether another was asked for meanwhile, and its retry. */
|
|
2311
|
+
private stakingReadRunning;
|
|
2312
|
+
private stakingReadRequested;
|
|
2313
|
+
private stakingRetryTimer;
|
|
2314
|
+
private stakingEventReadStarting;
|
|
2315
|
+
private stakingEventReadTimer;
|
|
2316
|
+
private stakingEventReadPending;
|
|
2317
|
+
/** Whether the latest staking summary reported at least one withdrawal still queued. */
|
|
2318
|
+
private hasQueuedWithdrawal;
|
|
2319
|
+
/** Advances on every stop, so a staking read fired before it is recognised and dropped. */
|
|
2320
|
+
private stakingReadGeneration;
|
|
2268
2321
|
private dexToMeta;
|
|
2269
2322
|
private trackedAssetData;
|
|
2270
2323
|
constructor(config: HyperliquidAccountTrackerConfig);
|
|
@@ -2293,6 +2346,15 @@ declare class HyperliquidAccountTracker extends BaseAccountTracker {
|
|
|
2293
2346
|
/** Subscribes to the spot state variant required by the active account mode. */
|
|
2294
2347
|
private sendSpotStateSubscription;
|
|
2295
2348
|
private resubscribeAll;
|
|
2349
|
+
/**
|
|
2350
|
+
* Drops the live account state when the socket goes down, and reports the outage once.
|
|
2351
|
+
*
|
|
2352
|
+
* Without this, the last frame's positions would read as live for the whole outage, or for good
|
|
2353
|
+
* if the socket never returns. Nothing is broadcast, since an empty list would read as a flat
|
|
2354
|
+
* account; `isInitialized` turns false and the reopen resubscribes. partysocket fires `close` for
|
|
2355
|
+
* every failed reconnect attempt too, so only the first close of an outage reaches `onError`.
|
|
2356
|
+
*/
|
|
2357
|
+
private onSocketClosed;
|
|
2296
2358
|
private routeRaw;
|
|
2297
2359
|
private handle;
|
|
2298
2360
|
private loadPerpMetas;
|
|
@@ -2314,6 +2376,58 @@ declare class HyperliquidAccountTracker extends BaseAccountTracker {
|
|
|
2314
2376
|
* vault. Partial equity is worse than no equity, so nothing is published early.
|
|
2315
2377
|
*/
|
|
2316
2378
|
private buildBalance;
|
|
2379
|
+
/**
|
|
2380
|
+
* Runs the borrow/lend poll exactly while the account is in Portfolio Margin.
|
|
2381
|
+
*
|
|
2382
|
+
* Safe to call on every mode read: a poll already in the state the mode asks for is left alone,
|
|
2383
|
+
* so a steady stream of `webData3` frames neither restarts the timer nor refires a read.
|
|
2384
|
+
*/
|
|
2385
|
+
private syncBorrowLendPoll;
|
|
2386
|
+
/**
|
|
2387
|
+
* Stops the borrow/lend poll and drops its figures, so no later mode or session inherits them.
|
|
2388
|
+
*
|
|
2389
|
+
* Every stop opens a new generation — a session ending, a restart, or the account leaving
|
|
2390
|
+
* Portfolio Margin — so a read still in flight from the period just ended can never land.
|
|
2391
|
+
*/
|
|
2392
|
+
private stopBorrowLendPoll;
|
|
2393
|
+
/**
|
|
2394
|
+
* Reads the health factor and reserve rates once, then republishes the balance with them.
|
|
2395
|
+
*
|
|
2396
|
+
* A failed read is reported and left for the next tick: the previous figures stay in place, since
|
|
2397
|
+
* one missed read says nothing about the account that the last good one did not.
|
|
2398
|
+
*/
|
|
2399
|
+
private refreshBorrowLendState;
|
|
2400
|
+
/**
|
|
2401
|
+
* Re-reads the staking account, one read at a time: a request made while a read runs is served by
|
|
2402
|
+
* a single read after it, however many arrive.
|
|
2403
|
+
*
|
|
2404
|
+
* That follow-up matters. A read already in flight may have left before the withdrawal it should
|
|
2405
|
+
* catch completed, so its answer can still hold that HYPE.
|
|
2406
|
+
*/
|
|
2407
|
+
private requestStakingRead;
|
|
2408
|
+
/**
|
|
2409
|
+
* Requests at most one staking read per interval from spot frames, preserving one trailing read
|
|
2410
|
+
* when more frames ask during the interval.
|
|
2411
|
+
*/
|
|
2412
|
+
private requestStakingReadFromSpotState;
|
|
2413
|
+
/** Starts one spot-triggered read, then opens the interval selected by its pending count. */
|
|
2414
|
+
private startStakingReadInterval;
|
|
2415
|
+
/**
|
|
2416
|
+
* Stops the staking re-reads; the cached figure stays, as it was true when last read.
|
|
2417
|
+
*
|
|
2418
|
+
* Every stop opens a new generation (a session ending, or a restart), so a read still in flight
|
|
2419
|
+
* from before it can never land.
|
|
2420
|
+
*/
|
|
2421
|
+
private stopStakingReads;
|
|
2422
|
+
/**
|
|
2423
|
+
* Reads the staking account once, then republishes the balance with it.
|
|
2424
|
+
*
|
|
2425
|
+
* A failed read is reported and retried after `STAKING_READ_RETRY_MS`, keeping the last figure in
|
|
2426
|
+
* place until one succeeds.
|
|
2427
|
+
*/
|
|
2428
|
+
private refreshStakingAccount;
|
|
2429
|
+
/** Retries a failed staking read once `STAKING_READ_RETRY_MS` has passed, unless the session stopped. */
|
|
2430
|
+
private scheduleStakingRetry;
|
|
2317
2431
|
/** The floor of `availableToTrade` over the tracked set; `null` when nothing is tracked. */
|
|
2318
2432
|
private availableMargin;
|
|
2319
2433
|
/** Clears both sides of the live balance join and the public account cache without emitting. */
|
|
@@ -2406,6 +2520,7 @@ declare class LighterAccountTracker extends BaseAccountTracker {
|
|
|
2406
2520
|
private readonly accountIdentifier;
|
|
2407
2521
|
private readonly apiKeyIndex;
|
|
2408
2522
|
private readonly apiPrivateKey;
|
|
2523
|
+
private readonly creds;
|
|
2409
2524
|
private ws;
|
|
2410
2525
|
private messageListenerId;
|
|
2411
2526
|
private openListenerId;
|
|
@@ -2770,6 +2885,11 @@ declare class ExchangesSDK {
|
|
|
2770
2885
|
*/
|
|
2771
2886
|
status(connection: ExchangeConnection, options?: ConnectOptions): Promise<AccountStatus>;
|
|
2772
2887
|
createTracker(connection: ExchangeConnection): AccountTracker;
|
|
2888
|
+
/**
|
|
2889
|
+
* A read-only tracker for any address, built without `connect()`: Hyperliquid answers account
|
|
2890
|
+
* state for an address with no credentials, so none are fetched and no trade account is needed.
|
|
2891
|
+
*/
|
|
2892
|
+
createPublicTracker(config: PublicTrackerConfig): AccountTracker;
|
|
2773
2893
|
snapshot(tradeAccountId: string): Promise<AccountSnapshot>;
|
|
2774
2894
|
snapshotAssets(tradeAccountId: string, coins: string[]): Promise<AssetSnapshotMap>;
|
|
2775
2895
|
createHelper<E extends ExchangeHelperExchange>(exchange: E): ExchangeHelperMap[E];
|
|
@@ -2778,4 +2898,4 @@ declare class ExchangesSDK {
|
|
|
2778
2898
|
private fetchCredentials;
|
|
2779
2899
|
}
|
|
2780
2900
|
|
|
2781
|
-
export { ACCOUNT_MODE_LABELS, ASSET_DECIMALS, AccountBalance, AccountBalanceCore, type AccountHealthStatus, AccountMode, type AccountPosition, type AccountSnapshot, type AccountStatus, type AccountSyncer, type AccountSyncerConfig, type AccountTierCallback, type AccountTracker, type AccountTrackerConfig, type AccountWarningReason, type ApiKey, type ApproveIntegratorParams, type ApproveIntegratorResult, AssetId, type AssetSnapshotMap, type BalanceCallback, BaseAccountSyncer, BaseAccountTracker, BaseWs, BinanceAccountSyncer, BinanceAccountTracker, type BinanceAccountTrackerConfig, BinanceExchangeWs, BinanceMultiAssetBalance, BinanceSingleAssetBalance, BinanceUserDataWs, type BinanceUserDataWsConfig, BinanceWsApi, type BinanceWsApiConfig, BybitAccountSyncer, BybitAccountTracker, type BybitAccountTrackerConfig, BybitExchangeWs, BybitIsolatedMarginBalance, BybitPortfolioMarginBalance, BybitRegularMarginBalance, BybitWs, type BybitWsConfig, type CctpDepositAddressParams, type CctpDepositAddressResult, type CctpDepositHistorySettlement, type CctpDepositParams, type CctpDepositRegistrationSettlement, type CctpDepositResult, type CctpDepositSettlement, type ChangeAccountTierParams, type ChangeAccountTierResult, type CheckApiWalletParams, CollateralBalance, type ConnectOptions, type ConnectWalletParams, type ConnectWalletResult, type CreateAccountTrackerConfig, type CreateExchangeWsConfig, DecimalString, type EthereumDepositParams, type EthereumDepositResult, type ExchangeConnection, type ExchangeHelperExchange, type ExchangeHelperMap, type ExchangeState, ExchangeWs, type ExchangeWsByConnector, ExchangesSDK, type ExchangesSDKConfig, FUNXYZ_DEPOSIT_MINIMUMS_USD, FUNXYZ_ETHEREUM_MAINNET_CHAIN_ID, FUNXYZ_EVM_CHAIN_NAMES, type FastWithdrawParams, type FastWithdrawResult, type FillSyncOutcome, type FundingSyncOutcome, type FunxyzAccountState, type FunxyzAsset, type FunxyzDepositAddressParams, type FunxyzDepositAddressResult, type FunxyzDepositCredit, type FunxyzDepositParams, type FunxyzDepositResult, type FunxyzDepositSettlement, type FunxyzDepositSource, type FunxyzMarket, HyperliquidAccountSyncer, HyperliquidAccountTracker, type HyperliquidAccountTrackerConfig, HyperliquidDexState, HyperliquidExchangeWs, type HyperliquidHelper, HyperliquidPortfolioMarginBalance, HyperliquidPortfolioMarginCollateral, HyperliquidStandardBalance, HyperliquidUnifiedBalance, HyperliquidWs, type IntegratorApprovalRequirements, L2_ASSET_DECIMALS, LIGHTER_NATIVE_USDC_ADDRESSES, LIGHTER_QUOTE_ASSETS, LIGHTER_RHC_CHAIN_ID, LIGHTER_RHC_USDG_ADDRESS, LIGHTER_RHC_USDG_DECIMALS, LIGHTER_USDC_DECIMALS, LighterAccountSyncer, type LighterAccountSyncerConfig, type LighterAccountTier, LighterAccountTracker, type LighterAccountTrackerConfig, type LighterApplyReferralCodeParams, type LighterAuthCredentials, LighterClassicBalance, type LighterConnector, type LighterDeposit, type LighterDepositAddressParams, type LighterDepositAddressResult, type LighterDepositCredit, type LighterDepositHistoryParams, type LighterDepositNetwork, type LighterDepositPage, type LighterDepositParams, type LighterDepositProgress, type LighterDepositResult, type LighterDepositStatus, type LighterDepositWalletAdapter, LighterExchangeWs, type LighterHelper, type LighterLookupResult, type LighterOnboardingStatus, type LighterReferral, type LighterReferralTotals, type LighterReferralTradeStats, LighterRhcAssetId, type LighterRhcDepositAddressParams, type LighterRhcDepositAddressResult, type LighterRhcDepositParams, type LighterRhcDepositResult, type LighterRhcDepositWallet, type LighterRhcHelper, type LighterSharedHelper, LighterUnifiedBalance, type LighterUserReferrals, type LighterUserReferralsParams, type LighterWithdraw, type LighterWithdrawHistoryParams, type LighterWithdrawPage, type LighterWithdrawStatus, type LighterWithdrawType, LighterWs, type ListExternalFillsParams, type ListExternalFundingParams, type ListExternalPositionsParams, MAX_API_KEY_INDEX, MIN_LIGHTER_CCTP_DEPOSIT_USDC, MIN_LIGHTER_FAST_WITHDRAW_COLLATERAL, MIN_LIGHTER_RHC_DEPOSIT_USDG, MIN_USER_API_KEY_INDEX, MarginMode, type MessageListener, OkxAccountSyncer, OkxAccountTracker, type OkxAccountTrackerConfig, OkxExchangeWs, OkxFuturesBalance, OkxMultiCurrencyMarginBalance, OkxPortfolioMarginBalance, OkxSpotBalance, OkxWs, type OkxWsConfig, type OpenListener, type PositionsCallback, RouteType, Side, SignedDecimalString, type StateUpdate, type SupportedCredentials, type SyncerThrottle, type TrackedAssetCallback, type TrackedAssetInfo, type WalletClientWithAccount, type WithdrawParams, type WithdrawResult, type WsMessage, createAccountSyncer, createAccountTracker, createExchangeWs, createHyperliquidHelper, createLighterHelper, createLighterRhcHelper, findAvailableApiKeyIndex, getAssetDecimals, getLighterNativeUsdcAddress, listFunxyzTransferableChains, readFunxyzChainSource, readFunxyzMinimumUsd, retrieveLighterAuthToken, usdcAbi };
|
|
2901
|
+
export { ACCOUNT_MODE_LABELS, ASSET_DECIMALS, AccountBalance, AccountBalanceCore, type AccountHealthStatus, AccountMode, type AccountPosition, type AccountSnapshot, type AccountStatus, type AccountSyncer, type AccountSyncerConfig, type AccountTierCallback, type AccountTracker, type AccountTrackerConfig, type AccountWarningReason, type ApiKey, type ApproveIntegratorParams, type ApproveIntegratorResult, AssetId, type AssetSnapshotMap, type BalanceCallback, BaseAccountSyncer, BaseAccountTracker, BaseWs, BinanceAccountSyncer, BinanceAccountTracker, type BinanceAccountTrackerConfig, BinanceExchangeWs, BinanceMultiAssetBalance, BinanceSingleAssetBalance, BinanceUserDataWs, type BinanceUserDataWsConfig, BinanceWsApi, type BinanceWsApiConfig, BybitAccountSyncer, BybitAccountTracker, type BybitAccountTrackerConfig, BybitExchangeWs, BybitIsolatedMarginBalance, BybitPortfolioMarginBalance, BybitRegularMarginBalance, BybitWs, type BybitWsConfig, type CctpDepositAddressParams, type CctpDepositAddressResult, type CctpDepositHistorySettlement, type CctpDepositParams, type CctpDepositRegistrationSettlement, type CctpDepositResult, type CctpDepositSettlement, type ChangeAccountTierParams, type ChangeAccountTierResult, type CheckApiWalletParams, type CloseListener, CollateralBalance, type ConnectOptions, type ConnectWalletParams, type ConnectWalletResult, type CreateAccountTrackerConfig, type CreateExchangeWsConfig, DecimalString, type EthereumDepositParams, type EthereumDepositResult, type ExchangeConnection, type ExchangeHelperExchange, type ExchangeHelperMap, type ExchangeState, ExchangeWs, type ExchangeWsByConnector, ExchangesSDK, type ExchangesSDKConfig, FUNXYZ_DEPOSIT_MINIMUMS_USD, FUNXYZ_ETHEREUM_MAINNET_CHAIN_ID, FUNXYZ_EVM_CHAIN_NAMES, type FastWithdrawParams, type FastWithdrawResult, type FillSyncOutcome, type FundingSyncOutcome, type FunxyzAccountState, type FunxyzAsset, type FunxyzDepositAddressParams, type FunxyzDepositAddressResult, type FunxyzDepositCredit, type FunxyzDepositParams, type FunxyzDepositResult, type FunxyzDepositSettlement, type FunxyzDepositSource, type FunxyzMarket, HyperliquidAccountSyncer, HyperliquidAccountTracker, type HyperliquidAccountTrackerConfig, HyperliquidBorrowLendReserve, HyperliquidDexState, HyperliquidExchangeWs, type HyperliquidHelper, HyperliquidPortfolioMarginBalance, HyperliquidPortfolioMarginCollateral, HyperliquidStandardBalance, HyperliquidUnifiedBalance, HyperliquidWs, type IntegratorApprovalRequirements, L2_ASSET_DECIMALS, LIGHTER_NATIVE_USDC_ADDRESSES, LIGHTER_QUOTE_ASSETS, LIGHTER_RHC_CHAIN_ID, LIGHTER_RHC_USDG_ADDRESS, LIGHTER_RHC_USDG_DECIMALS, LIGHTER_USDC_DECIMALS, LighterAccountSyncer, type LighterAccountSyncerConfig, type LighterAccountTier, LighterAccountTracker, type LighterAccountTrackerConfig, type LighterApplyReferralCodeParams, type LighterAuthCredentials, LighterClassicBalance, type LighterConnector, type LighterDeposit, type LighterDepositAddressParams, type LighterDepositAddressResult, type LighterDepositCredit, type LighterDepositHistoryParams, type LighterDepositNetwork, type LighterDepositPage, type LighterDepositParams, type LighterDepositProgress, type LighterDepositResult, type LighterDepositStatus, type LighterDepositWalletAdapter, LighterExchangeWs, type LighterHelper, type LighterLookupResult, type LighterOnboardingStatus, type LighterReferral, type LighterReferralTotals, type LighterReferralTradeStats, LighterRhcAssetId, type LighterRhcDepositAddressParams, type LighterRhcDepositAddressResult, type LighterRhcDepositParams, type LighterRhcDepositResult, type LighterRhcDepositWallet, type LighterRhcHelper, type LighterSharedHelper, LighterUnifiedBalance, type LighterUserReferrals, type LighterUserReferralsParams, type LighterWithdraw, type LighterWithdrawHistoryParams, type LighterWithdrawPage, type LighterWithdrawStatus, type LighterWithdrawType, LighterWs, type ListExternalFillsParams, type ListExternalFundingParams, type ListExternalPositionsParams, MAX_API_KEY_INDEX, MIN_LIGHTER_CCTP_DEPOSIT_USDC, MIN_LIGHTER_FAST_WITHDRAW_COLLATERAL, MIN_LIGHTER_RHC_DEPOSIT_USDG, MIN_USER_API_KEY_INDEX, MarginMode, type MessageListener, OkxAccountSyncer, OkxAccountTracker, type OkxAccountTrackerConfig, OkxExchangeWs, OkxFuturesBalance, OkxMultiCurrencyMarginBalance, OkxPortfolioMarginBalance, OkxSpotBalance, OkxWs, type OkxWsConfig, type OpenListener, type PositionsCallback, type PublicTrackerConfig, RouteType, Side, SignedDecimalString, type StateUpdate, type SupportedCredentials, type SyncerThrottle, type TrackedAssetCallback, type TrackedAssetInfo, type WalletClientWithAccount, type WithdrawParams, type WithdrawResult, type WsMessage, createAccountSyncer, createAccountTracker, createExchangeWs, createHyperliquidHelper, createLighterHelper, createLighterRhcHelper, findAvailableApiKeyIndex, getAssetDecimals, getLighterNativeUsdcAddress, listFunxyzTransferableChains, readFunxyzChainSource, readFunxyzMinimumUsd, retrieveLighterAuthToken, usdcAbi };
|
package/dist/sdk.js
CHANGED
|
@@ -2,7 +2,7 @@ import { configureProxy } from '@pear-protocol/utils';
|
|
|
2
2
|
import { createExchangeHelper } from './helper/factory';
|
|
3
3
|
import { checkAccountStatus } from './status';
|
|
4
4
|
import { createAccountSyncer } from './syncer';
|
|
5
|
-
import { createAccountTracker } from './tracker';
|
|
5
|
+
import { HyperliquidAccountTracker, createAccountTracker } from './tracker';
|
|
6
6
|
|
|
7
7
|
var __defProp = Object.defineProperty;
|
|
8
8
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
@@ -42,6 +42,20 @@ class ExchangesSDK {
|
|
|
42
42
|
createTracker(connection) {
|
|
43
43
|
return this.buildTracker(connection);
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* A read-only tracker for any address, built without `connect()`: Hyperliquid answers account
|
|
47
|
+
* state for an address with no credentials, so none are fetched and no trade account is needed.
|
|
48
|
+
*/
|
|
49
|
+
createPublicTracker(config) {
|
|
50
|
+
switch (config.connector) {
|
|
51
|
+
case "hyperliquid":
|
|
52
|
+
return new HyperliquidAccountTracker({
|
|
53
|
+
demo: this.demo,
|
|
54
|
+
onError: config.onError,
|
|
55
|
+
address: config.address
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
45
59
|
async snapshot(tradeAccountId) {
|
|
46
60
|
const connection = await this.fetchCredentials(tradeAccountId);
|
|
47
61
|
return this.buildTracker(connection).snapshot();
|
package/dist/syncer/base.js
CHANGED
|
@@ -23,14 +23,14 @@ class BaseAccountSyncer {
|
|
|
23
23
|
}
|
|
24
24
|
// Phase A: Load current Pear state (positions, fills, instruments, watermark).
|
|
25
25
|
async loadPearState() {
|
|
26
|
-
const
|
|
26
|
+
const watermark = await this.sdk.core.sync.fills.since({
|
|
27
|
+
overlapMs: OVERLAP_MS
|
|
28
|
+
});
|
|
29
|
+
const [{ positions: openPositions }, { fills: provisionalFills }, { instruments }] = await Promise.all([
|
|
27
30
|
this.sdk.core.positions.openLite(),
|
|
28
31
|
this.sdk.core.fills.provisionals(),
|
|
29
32
|
this.sdk.core.instruments.lite({
|
|
30
33
|
connector: this.connector
|
|
31
|
-
}),
|
|
32
|
-
this.sdk.core.sync.fills.since({
|
|
33
|
-
overlapMs: OVERLAP_MS
|
|
34
34
|
})
|
|
35
35
|
]);
|
|
36
36
|
const entryBySymbol = await this.loadEntryBySymbol(openPositions);
|
|
@@ -128,7 +128,9 @@ class BaseAccountSyncer {
|
|
|
128
128
|
},
|
|
129
129
|
...firstSyncStartedAt === void 0 ? {} : {
|
|
130
130
|
firstSyncStartedAt
|
|
131
|
-
}
|
|
131
|
+
},
|
|
132
|
+
// Core defers the create when any execution write postdates this server-minted token.
|
|
133
|
+
executionsUpdatedAtEpochMs: watermark.executionsUpdatedAtEpochMs
|
|
132
134
|
};
|
|
133
135
|
if (firstSyncTrim) {
|
|
134
136
|
console.log(`${this.connector}: first-sync trim \u2014 dropped ${firstSyncTrim.trimmedCount} pre-cutoff fills (cutoffTs=${firstSyncTrim.cutoffTs})`);
|
package/dist/tracker/base.js
CHANGED
|
@@ -6,7 +6,6 @@ class BaseAccountTracker {
|
|
|
6
6
|
}
|
|
7
7
|
onError;
|
|
8
8
|
demo;
|
|
9
|
-
credentials;
|
|
10
9
|
balance = null;
|
|
11
10
|
balanceInitialized = false;
|
|
12
11
|
positions = /* @__PURE__ */ new Map();
|
|
@@ -16,7 +15,6 @@ class BaseAccountTracker {
|
|
|
16
15
|
assetEntries = /* @__PURE__ */ new Map();
|
|
17
16
|
constructor(config) {
|
|
18
17
|
this.demo = config.demo;
|
|
19
|
-
this.credentials = config.credentials;
|
|
20
18
|
this.onError = (error) => {
|
|
21
19
|
console.warn("[AccountTracker] error:", error.message);
|
|
22
20
|
config.onError?.(error);
|
|
@@ -61,7 +61,7 @@ class BinanceAccountTracker extends BaseAccountTracker {
|
|
|
61
61
|
this.handlePositionRisk(positionRisk, false);
|
|
62
62
|
const ws = await createExchangeWs("binance", {
|
|
63
63
|
demo: this.demo,
|
|
64
|
-
credentials: this.
|
|
64
|
+
credentials: this.creds
|
|
65
65
|
});
|
|
66
66
|
this.userDataWs = ws.userData;
|
|
67
67
|
this.wsApiWs = ws.wsApi;
|
|
@@ -82,7 +82,7 @@ class BybitAccountTracker extends BaseAccountTracker {
|
|
|
82
82
|
this.handleInitialData(wallet ?? null, positions, toBybitAccountMode(accountInfo.marginMode));
|
|
83
83
|
this.ws = await createExchangeWs("bybit", {
|
|
84
84
|
demo: this.demo,
|
|
85
|
-
credentials: this.
|
|
85
|
+
credentials: this.creds
|
|
86
86
|
});
|
|
87
87
|
this.messageListenerId = this.ws.main.addMessageListener((raw) => this.routeRaw(raw));
|
|
88
88
|
this.openListenerId = this.ws.main.addOpenListener(() => {
|
|
@@ -146,7 +146,22 @@ const HyperliquidPortfolioMarginCollateral = z.object({
|
|
|
146
146
|
* `borrowed / borrow cap` for this coin, and `null` where the venue renders `--` — no borrow.
|
|
147
147
|
* NOT clipped at one: `0x0871deb3…` reports 1.0081 on USDT0.
|
|
148
148
|
*/
|
|
149
|
-
borrowCapUsed: DecimalString.nullable()
|
|
149
|
+
borrowCapUsed: DecimalString.nullable(),
|
|
150
|
+
/**
|
|
151
|
+
* The coin's outstanding borrow in its own units, and `0` on a coin never borrowed.
|
|
152
|
+
*
|
|
153
|
+
* Read from the wire's `borrowed`, never from a negative `equity`: the USDC row already contains
|
|
154
|
+
* the perps account value, so `equity` is `supplied − borrowed + accountValue` and its negative
|
|
155
|
+
* understates the debt by exactly that wallet.
|
|
156
|
+
*/
|
|
157
|
+
borrowed: DecimalString
|
|
158
|
+
});
|
|
159
|
+
const HyperliquidBorrowLendReserve = z.object({
|
|
160
|
+
asset: z.string(),
|
|
161
|
+
/** What borrowing the coin costs per year, as a fraction — `0.05` is 5% APY. */
|
|
162
|
+
borrowYearlyRate: DecimalString,
|
|
163
|
+
/** What supplying the coin earns per year; Portfolio Margin supplies idle balances automatically. */
|
|
164
|
+
supplyYearlyRate: DecimalString
|
|
150
165
|
});
|
|
151
166
|
const HyperliquidPortfolioMarginBalance = AccountBalanceCore.extend({
|
|
152
167
|
accountMode: z.literal("hyperliquid:portfolioMargin"),
|
|
@@ -194,7 +209,27 @@ const HyperliquidPortfolioMarginBalance = AccountBalanceCore.extend({
|
|
|
194
209
|
* account borrows nothing. Not clipped at one, and never the USDC entry alone.
|
|
195
210
|
*/
|
|
196
211
|
borrowCapUsed: DecimalString.nullable(),
|
|
212
|
+
/**
|
|
213
|
+
* The venue's Your Borrowed: every row's `borrowed` valued at its own mid, in USD, and `0` when
|
|
214
|
+
* the account borrows nothing. Null when a borrowed coin cannot be priced, since a debt counted
|
|
215
|
+
* as zero would hide exactly the part nobody could value.
|
|
216
|
+
*/
|
|
217
|
+
borrowedValue: DecimalString.nullable(),
|
|
218
|
+
/**
|
|
219
|
+
* The venue's Health Factor — LTV-weighted collateral over the borrowed value — at four decimals.
|
|
220
|
+
* Below `1` the account can borrow no more, which is not by itself liquidation: that is
|
|
221
|
+
* `portfolioMarginRatio`. Null when the account borrows nothing, and until the first read lands.
|
|
222
|
+
*
|
|
223
|
+
* Read, never derived, and over REST because no channel carries it: the live tracker refreshes it
|
|
224
|
+
* on a timer, so it can trail the channel-fed figures beside it by up to one poll interval.
|
|
225
|
+
*/
|
|
226
|
+
healthFactor: DecimalString.nullable(),
|
|
227
|
+
/**
|
|
228
|
+
* Every reserve's yearly rates, listed whatever the account holds, so what a borrow will cost
|
|
229
|
+
* shows before the first one happens. Empty until the first read lands.
|
|
230
|
+
*/
|
|
231
|
+
borrowLendReserves: z.array(HyperliquidBorrowLendReserve),
|
|
197
232
|
perDexState: z.array(HyperliquidDexState)
|
|
198
233
|
});
|
|
199
234
|
|
|
200
|
-
export { HyperliquidDexState, HyperliquidPortfolioMarginBalance, HyperliquidPortfolioMarginCollateral, HyperliquidStandardBalance, HyperliquidUnifiedBalance };
|
|
235
|
+
export { HyperliquidBorrowLendReserve, HyperliquidDexState, HyperliquidPortfolioMarginBalance, HyperliquidPortfolioMarginCollateral, HyperliquidStandardBalance, HyperliquidUnifiedBalance };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { fetchBorrowLendUserState, fetchAllBorrowLendReserveStates } from '../../client/hyperliquid/rest';
|
|
2
|
+
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
5
|
+
const EMPTY_BORROW_LEND_STATE = {
|
|
6
|
+
healthFactor: null,
|
|
7
|
+
ratesByToken: /* @__PURE__ */ new Map()
|
|
8
|
+
};
|
|
9
|
+
async function fetchBorrowLendState(user, demo) {
|
|
10
|
+
const [userState, reserves] = await Promise.all([
|
|
11
|
+
fetchBorrowLendUserState(user, demo),
|
|
12
|
+
fetchAllBorrowLendReserveStates(demo)
|
|
13
|
+
]);
|
|
14
|
+
return {
|
|
15
|
+
healthFactor: userState.healthFactor ?? null,
|
|
16
|
+
ratesByToken: ratesByToken(reserves)
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
__name(fetchBorrowLendState, "fetchBorrowLendState");
|
|
20
|
+
function ratesByToken(reserves) {
|
|
21
|
+
const rates = /* @__PURE__ */ new Map();
|
|
22
|
+
for (const [token, reserve] of reserves) {
|
|
23
|
+
rates.set(token, {
|
|
24
|
+
borrowYearlyRate: reserve.borrowYearlyRate,
|
|
25
|
+
supplyYearlyRate: reserve.supplyYearlyRate
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return rates;
|
|
29
|
+
}
|
|
30
|
+
__name(ratesByToken, "ratesByToken");
|
|
31
|
+
|
|
32
|
+
export { EMPTY_BORROW_LEND_STATE, fetchBorrowLendState };
|
|
@@ -232,6 +232,9 @@ function mapPortfolioMarginBalance(context) {
|
|
|
232
232
|
portfolioMarginRatio: wireMarginRatio(context),
|
|
233
233
|
portfolioAccountLeverage: portfolioAccountLeverage(context),
|
|
234
234
|
borrowCapUsed: worstBorrowCapUsed(context),
|
|
235
|
+
borrowedValue: borrowedValueOf(context),
|
|
236
|
+
healthFactor: wireHealthFactor(context),
|
|
237
|
+
borrowLendReserves: borrowLendReserveRows(context),
|
|
235
238
|
perDexState: context.summary.perDexState,
|
|
236
239
|
timestamp: context.timestamp
|
|
237
240
|
};
|
|
@@ -256,7 +259,8 @@ function portfolioMarginCollateralRows(context) {
|
|
|
256
259
|
availableMargin: context.availableMarginByToken.get(balance.token) ?? null,
|
|
257
260
|
withdrawable: signed(total.minus(toDecimal(balance.hold)).toFixed()),
|
|
258
261
|
ltv: balance.ltv === null ? null : toDecimal(balance.ltv).toFixed(),
|
|
259
|
-
borrowCapUsed: borrowCapUsedFor(context, balance.token)
|
|
262
|
+
borrowCapUsed: borrowCapUsedFor(context, balance.token),
|
|
263
|
+
borrowed: toDecimal(balance.borrowed).toFixed()
|
|
260
264
|
});
|
|
261
265
|
}
|
|
262
266
|
rows.sort(byRowAsset);
|
|
@@ -283,6 +287,22 @@ function worstBorrowCapUsed(context) {
|
|
|
283
287
|
return worst.isZero() ? null : worst.toFixed(RATIO_DECIMALS);
|
|
284
288
|
}
|
|
285
289
|
__name(worstBorrowCapUsed, "worstBorrowCapUsed");
|
|
290
|
+
function borrowedValueOf(context) {
|
|
291
|
+
let borrowed = ZERO;
|
|
292
|
+
for (const balance of context.spotBalances.values()) {
|
|
293
|
+
const debt = toDecimal(balance.borrowed);
|
|
294
|
+
if (debt.isZero()) {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
const mid = context.prices.midForToken(balance.token);
|
|
298
|
+
if (mid === null) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
borrowed = borrowed.plus(debt.times(toDecimal(mid)));
|
|
302
|
+
}
|
|
303
|
+
return borrowed.toFixed();
|
|
304
|
+
}
|
|
305
|
+
__name(borrowedValueOf, "borrowedValueOf");
|
|
286
306
|
function wireMarginRatio(context) {
|
|
287
307
|
if (context.portfolioMarginRatio === null) {
|
|
288
308
|
return null;
|
|
@@ -290,6 +310,38 @@ function wireMarginRatio(context) {
|
|
|
290
310
|
return toDecimal(context.portfolioMarginRatio).toFixed(RATIO_DECIMALS);
|
|
291
311
|
}
|
|
292
312
|
__name(wireMarginRatio, "wireMarginRatio");
|
|
313
|
+
function wireHealthFactor(context) {
|
|
314
|
+
const healthFactor = context.borrowLend.healthFactor;
|
|
315
|
+
if (healthFactor === null) {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
return toDecimal(healthFactor).toFixed(RATIO_DECIMALS);
|
|
319
|
+
}
|
|
320
|
+
__name(wireHealthFactor, "wireHealthFactor");
|
|
321
|
+
function borrowLendReserveRows(context) {
|
|
322
|
+
const rows = [];
|
|
323
|
+
for (const [token, rates] of context.borrowLend.ratesByToken) {
|
|
324
|
+
const asset = context.collateralTokenNames.get(token);
|
|
325
|
+
if (asset === void 0) {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
rows.push({
|
|
329
|
+
asset,
|
|
330
|
+
borrowYearlyRate: toDecimal(rates.borrowYearlyRate).toFixed(),
|
|
331
|
+
supplyYearlyRate: toDecimal(rates.supplyYearlyRate).toFixed()
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
rows.sort(byReserveAsset);
|
|
335
|
+
return rows;
|
|
336
|
+
}
|
|
337
|
+
__name(borrowLendReserveRows, "borrowLendReserveRows");
|
|
338
|
+
function byReserveAsset(left, right) {
|
|
339
|
+
if (left.asset === right.asset) {
|
|
340
|
+
return 0;
|
|
341
|
+
}
|
|
342
|
+
return left.asset < right.asset ? -1 : 1;
|
|
343
|
+
}
|
|
344
|
+
__name(byReserveAsset, "byReserveAsset");
|
|
293
345
|
function portfolioAccountLeverage(context) {
|
|
294
346
|
const collateral = haircutCollateral(context);
|
|
295
347
|
if (collateral === null) {
|
|
@@ -2,6 +2,7 @@ import { createExchangeWs } from '../../client';
|
|
|
2
2
|
import { fetchPerpDexs, fetchInfo, fetchAllMids, fetchUserAbstraction, fetchSpotClearinghouseState, fetchDelegatorSummary, fetchUserVaultEquities, fetchClearinghouseState, fetchActiveAssetData } from '../../client/hyperliquid/rest';
|
|
3
3
|
import { BaseAccountTracker } from '../base';
|
|
4
4
|
import { resolveHyperliquidAccountType } from './account-mode';
|
|
5
|
+
import { EMPTY_BORROW_LEND_STATE, fetchBorrowLendState } from './borrow-lend';
|
|
5
6
|
import { buildAvailableMarginByToken, buildCollateralTokenNames, minimumAvailableToTrade } from './collateral';
|
|
6
7
|
import { LOCKED_TRACKED_COINS } from './const';
|
|
7
8
|
import { toDecimal, ZERO } from './decimal';
|
|
@@ -15,6 +16,10 @@ const EMPTY_SPOT_META = {
|
|
|
15
16
|
tokens: [],
|
|
16
17
|
universe: []
|
|
17
18
|
};
|
|
19
|
+
const BORROW_LEND_POLL_INTERVAL_MS = 3e4;
|
|
20
|
+
const STAKING_READ_RETRY_MS = 3e4;
|
|
21
|
+
const STAKING_QUEUED_READ_INTERVAL_MS = 6e4;
|
|
22
|
+
const STAKING_SPECULATIVE_READ_INTERVAL_MS = 3e5;
|
|
18
23
|
class HyperliquidAccountTracker extends BaseAccountTracker {
|
|
19
24
|
static {
|
|
20
25
|
__name(this, "HyperliquidAccountTracker");
|
|
@@ -23,13 +28,21 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
|
|
|
23
28
|
ws = null;
|
|
24
29
|
messageListenerId = null;
|
|
25
30
|
openListenerId = null;
|
|
31
|
+
closeListenerId = null;
|
|
26
32
|
lifecycleVersion = 0;
|
|
33
|
+
/** Whether the socket is down and the outage already reported, so its retries report nothing more. */
|
|
34
|
+
socketDropped = false;
|
|
27
35
|
subscribedCoins = /* @__PURE__ */ new Set();
|
|
28
36
|
lastSummary = emptyClearinghouseSummary();
|
|
29
37
|
spotCoinBalances = /* @__PURE__ */ new Map();
|
|
30
38
|
/** Portfolio Margin's own risk scalars, which ride the same `spotState` frame the balances do. */
|
|
31
39
|
portfolioMarginRatio = null;
|
|
32
40
|
borrowRatioByToken = /* @__PURE__ */ new Map();
|
|
41
|
+
/** Portfolio Margin's REST-only figures, and the timer that keeps them fresh while in that mode. */
|
|
42
|
+
borrowLend = EMPTY_BORROW_LEND_STATE;
|
|
43
|
+
borrowLendPollTimer = null;
|
|
44
|
+
/** Advances on every stop, so a read fired in an earlier poll period is recognised and dropped. */
|
|
45
|
+
borrowLendPollGeneration = 0;
|
|
33
46
|
hasSpotState = false;
|
|
34
47
|
hasClearinghouseState = false;
|
|
35
48
|
accountType = null;
|
|
@@ -39,11 +52,22 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
|
|
|
39
52
|
hasWebData3 = false;
|
|
40
53
|
vaultEquity = "0";
|
|
41
54
|
stakedHype = "0";
|
|
55
|
+
/** Whether a staking re-read is running, whether another was asked for meanwhile, and its retry. */
|
|
56
|
+
stakingReadRunning = false;
|
|
57
|
+
stakingReadRequested = false;
|
|
58
|
+
stakingRetryTimer = null;
|
|
59
|
+
stakingEventReadStarting = false;
|
|
60
|
+
stakingEventReadTimer = null;
|
|
61
|
+
stakingEventReadPending = false;
|
|
62
|
+
/** Whether the latest staking summary reported at least one withdrawal still queued. */
|
|
63
|
+
hasQueuedWithdrawal = false;
|
|
64
|
+
/** Advances on every stop, so a staking read fired before it is recognised and dropped. */
|
|
65
|
+
stakingReadGeneration = 0;
|
|
42
66
|
dexToMeta = /* @__PURE__ */ new Map();
|
|
43
67
|
trackedAssetData = /* @__PURE__ */ new Map();
|
|
44
68
|
constructor(config) {
|
|
45
69
|
super(config);
|
|
46
|
-
this.address = config.
|
|
70
|
+
this.address = config.address;
|
|
47
71
|
}
|
|
48
72
|
/**
|
|
49
73
|
* Reads account mode, spot prices, collateral balances, per-DEX state, staking, vaults and
|
|
@@ -74,18 +98,21 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
|
|
|
74
98
|
})));
|
|
75
99
|
const assetData = await this.fetchActiveAssetEvents(this.coinsForAvailableMargin());
|
|
76
100
|
const { summary, assetPositions } = aggregateClearinghouseStates(clearinghouseStates);
|
|
101
|
+
const accountType = resolveHyperliquidAccountType(abstraction);
|
|
102
|
+
const borrowLend = accountType === HyperliquidAccountType.PORTFOLIO_MARGIN ? await fetchBorrowLendState(user, this.demo) : EMPTY_BORROW_LEND_STATE;
|
|
77
103
|
const balance = mapBalance({
|
|
78
|
-
accountType
|
|
104
|
+
accountType,
|
|
79
105
|
summary,
|
|
80
106
|
spotBalances: mapSpotBalances(spotState),
|
|
81
107
|
prices: SpotPrices.from(spotMeta, mids),
|
|
82
108
|
vaultEquity: sumRestVaultEquity(vaults),
|
|
83
|
-
stakedHype:
|
|
109
|
+
stakedHype: _stakingAccountHype(delegator),
|
|
84
110
|
availableMargin: minimumAvailableToTrade(assetData.values()),
|
|
85
111
|
collateralTokenNames: buildCollateralTokenNames(spotMeta),
|
|
86
112
|
availableMarginByToken: buildAvailableMarginByToken(summary.perDexState, assetData.values()),
|
|
87
113
|
portfolioMarginRatio: spotState.portfolioMarginRatio ?? null,
|
|
88
114
|
borrowRatioByToken: mapBorrowRatios(spotState),
|
|
115
|
+
borrowLend,
|
|
89
116
|
timestamp: Date.now()
|
|
90
117
|
});
|
|
91
118
|
const positions = Array.from(mapPositions(assetPositions).values());
|
|
@@ -116,9 +143,12 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
|
|
|
116
143
|
const lifecycleVersion = ++this.lifecycleVersion;
|
|
117
144
|
const user = this.addrLower();
|
|
118
145
|
this.destroyCurrentSocket();
|
|
146
|
+
this.stopBorrowLendPoll();
|
|
147
|
+
this.stopStakingReads();
|
|
119
148
|
this.subscribedCoins.clear();
|
|
120
149
|
this.resetLiveAccountState();
|
|
121
150
|
this.dexToMeta.clear();
|
|
151
|
+
this.socketDropped = false;
|
|
122
152
|
const [allMetas, perpDexs, spotMeta, abstraction, delegator] = await Promise.all([
|
|
123
153
|
fetchInfo({
|
|
124
154
|
type: "allPerpMetas"
|
|
@@ -135,10 +165,10 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
|
|
|
135
165
|
this.spotMeta = spotMeta;
|
|
136
166
|
this.collateralTokenNames = buildCollateralTokenNames(spotMeta);
|
|
137
167
|
this.accountType = resolveHyperliquidAccountType(abstraction);
|
|
138
|
-
this.stakedHype =
|
|
168
|
+
this.stakedHype = _stakingAccountHype(delegator);
|
|
169
|
+
this.hasQueuedWithdrawal = delegator.nPendingWithdrawals > 0;
|
|
139
170
|
const ws = (await createExchangeWs("hyperliquid", {
|
|
140
|
-
demo: this.demo
|
|
141
|
-
credentials: this.credentials
|
|
171
|
+
demo: this.demo
|
|
142
172
|
})).main;
|
|
143
173
|
if (lifecycleVersion !== this.lifecycleVersion) {
|
|
144
174
|
ws.destroy();
|
|
@@ -147,7 +177,9 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
|
|
|
147
177
|
this.ws = ws;
|
|
148
178
|
this.messageListenerId = this.ws.addMessageListener((raw) => this.routeRaw(raw));
|
|
149
179
|
this.openListenerId = this.ws.addOpenListener(() => this.resubscribeAll());
|
|
180
|
+
this.closeListenerId = this.ws.addCloseListener(() => this.onSocketClosed());
|
|
150
181
|
this.sendStaticSubs();
|
|
182
|
+
this.syncBorrowLendPoll();
|
|
151
183
|
const coinsToTrack = /* @__PURE__ */ new Set([
|
|
152
184
|
...LOCKED_TRACKED_COINS,
|
|
153
185
|
...this.trackedCoins
|
|
@@ -159,6 +191,8 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
|
|
|
159
191
|
async cleanupConnections() {
|
|
160
192
|
this.lifecycleVersion += 1;
|
|
161
193
|
this.destroyCurrentSocket();
|
|
194
|
+
this.stopBorrowLendPoll();
|
|
195
|
+
this.stopStakingReads();
|
|
162
196
|
this.subscribedCoins.clear();
|
|
163
197
|
}
|
|
164
198
|
/** Detaches and destroys the active socket without changing the current lifecycle generation. */
|
|
@@ -171,6 +205,10 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
|
|
|
171
205
|
this.ws?.removeOpenListener(this.openListenerId);
|
|
172
206
|
this.openListenerId = null;
|
|
173
207
|
}
|
|
208
|
+
if (this.closeListenerId) {
|
|
209
|
+
this.ws?.removeCloseListener(this.closeListenerId);
|
|
210
|
+
this.closeListenerId = null;
|
|
211
|
+
}
|
|
174
212
|
this.ws?.destroy();
|
|
175
213
|
this.ws = null;
|
|
176
214
|
}
|
|
@@ -254,6 +292,7 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
|
|
|
254
292
|
}
|
|
255
293
|
resubscribeAll() {
|
|
256
294
|
this.resetLiveAccountState();
|
|
295
|
+
this.socketDropped = false;
|
|
257
296
|
this.sendStaticSubs();
|
|
258
297
|
const user = this.addrLower();
|
|
259
298
|
for (const coin of this.subscribedCoins) {
|
|
@@ -267,6 +306,22 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
|
|
|
267
306
|
});
|
|
268
307
|
}
|
|
269
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* Drops the live account state when the socket goes down, and reports the outage once.
|
|
311
|
+
*
|
|
312
|
+
* Without this, the last frame's positions would read as live for the whole outage, or for good
|
|
313
|
+
* if the socket never returns. Nothing is broadcast, since an empty list would read as a flat
|
|
314
|
+
* account; `isInitialized` turns false and the reopen resubscribes. partysocket fires `close` for
|
|
315
|
+
* every failed reconnect attempt too, so only the first close of an outage reaches `onError`.
|
|
316
|
+
*/
|
|
317
|
+
onSocketClosed() {
|
|
318
|
+
if (this.socketDropped) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
this.socketDropped = true;
|
|
322
|
+
this.resetLiveAccountState();
|
|
323
|
+
this.onError(new Error("Hyperliquid WebSocket closed; account state is unavailable until it reconnects"));
|
|
324
|
+
}
|
|
270
325
|
routeRaw(raw) {
|
|
271
326
|
try {
|
|
272
327
|
const msg = JSON.parse(raw);
|
|
@@ -302,6 +357,9 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
|
|
|
302
357
|
data: msg.data
|
|
303
358
|
});
|
|
304
359
|
break;
|
|
360
|
+
case "error":
|
|
361
|
+
this.onError(new Error(`Hyperliquid WebSocket error: ${String(msg.data)}`));
|
|
362
|
+
break;
|
|
305
363
|
}
|
|
306
364
|
} catch (err) {
|
|
307
365
|
this.onError(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -422,6 +480,7 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
|
|
|
422
480
|
this.resetLiveAccountState();
|
|
423
481
|
}
|
|
424
482
|
this.accountType = nextAccountType;
|
|
483
|
+
this.syncBorrowLendPoll();
|
|
425
484
|
this.vaultEquity = sumLiveVaultEquity(data.perpDexStates ?? []);
|
|
426
485
|
this.hasWebData3 = true;
|
|
427
486
|
if (!nextAccountType) {
|
|
@@ -440,10 +499,15 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
|
|
|
440
499
|
onSpotState(data) {
|
|
441
500
|
const balances = data?.spotState?.balances;
|
|
442
501
|
if (!balances) return;
|
|
502
|
+
const hadSpotState = this.hasSpotState;
|
|
503
|
+
const previousHype = this.spotCoinBalances.get("HYPE")?.total;
|
|
443
504
|
this.spotCoinBalances = mapSpotBalances(data.spotState);
|
|
444
505
|
this.portfolioMarginRatio = data.spotState.portfolioMarginRatio ?? null;
|
|
445
506
|
this.borrowRatioByToken = mapBorrowRatios(data.spotState);
|
|
446
507
|
this.hasSpotState = true;
|
|
508
|
+
if (!toDecimal(this.stakedHype).isZero() && (!hadSpotState || spotHypeRose(previousHype, this.spotCoinBalances))) {
|
|
509
|
+
this.requestStakingReadFromSpotState();
|
|
510
|
+
}
|
|
447
511
|
const balance = this.buildBalance();
|
|
448
512
|
if (balance !== void 0) {
|
|
449
513
|
this.onStateUpdate({
|
|
@@ -475,9 +539,177 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
|
|
|
475
539
|
availableMarginByToken: buildAvailableMarginByToken(this.lastSummary.perDexState, this.trackedAssetData.values()),
|
|
476
540
|
portfolioMarginRatio: this.portfolioMarginRatio,
|
|
477
541
|
borrowRatioByToken: this.borrowRatioByToken,
|
|
542
|
+
borrowLend: this.borrowLend,
|
|
478
543
|
timestamp: Date.now()
|
|
479
544
|
});
|
|
480
545
|
}
|
|
546
|
+
/**
|
|
547
|
+
* Runs the borrow/lend poll exactly while the account is in Portfolio Margin.
|
|
548
|
+
*
|
|
549
|
+
* Safe to call on every mode read: a poll already in the state the mode asks for is left alone,
|
|
550
|
+
* so a steady stream of `webData3` frames neither restarts the timer nor refires a read.
|
|
551
|
+
*/
|
|
552
|
+
syncBorrowLendPoll() {
|
|
553
|
+
const wantsPoll = this.accountType === HyperliquidAccountType.PORTFOLIO_MARGIN;
|
|
554
|
+
if (wantsPoll === (this.borrowLendPollTimer !== null)) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (!wantsPoll) {
|
|
558
|
+
this.stopBorrowLendPoll();
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
void this.refreshBorrowLendState();
|
|
562
|
+
this.borrowLendPollTimer = setInterval(() => void this.refreshBorrowLendState(), BORROW_LEND_POLL_INTERVAL_MS);
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Stops the borrow/lend poll and drops its figures, so no later mode or session inherits them.
|
|
566
|
+
*
|
|
567
|
+
* Every stop opens a new generation — a session ending, a restart, or the account leaving
|
|
568
|
+
* Portfolio Margin — so a read still in flight from the period just ended can never land.
|
|
569
|
+
*/
|
|
570
|
+
stopBorrowLendPoll() {
|
|
571
|
+
if (this.borrowLendPollTimer !== null) {
|
|
572
|
+
clearInterval(this.borrowLendPollTimer);
|
|
573
|
+
this.borrowLendPollTimer = null;
|
|
574
|
+
}
|
|
575
|
+
this.borrowLendPollGeneration += 1;
|
|
576
|
+
this.borrowLend = EMPTY_BORROW_LEND_STATE;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Reads the health factor and reserve rates once, then republishes the balance with them.
|
|
580
|
+
*
|
|
581
|
+
* A failed read is reported and left for the next tick: the previous figures stay in place, since
|
|
582
|
+
* one missed read says nothing about the account that the last good one did not.
|
|
583
|
+
*/
|
|
584
|
+
async refreshBorrowLendState() {
|
|
585
|
+
const generation = this.borrowLendPollGeneration;
|
|
586
|
+
let borrowLend;
|
|
587
|
+
try {
|
|
588
|
+
borrowLend = await fetchBorrowLendState(this.addrLower(), this.demo);
|
|
589
|
+
} catch (err) {
|
|
590
|
+
this.onError(err instanceof Error ? err : new Error(String(err)));
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (generation !== this.borrowLendPollGeneration) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
this.borrowLend = borrowLend;
|
|
597
|
+
const balance = this.buildBalance();
|
|
598
|
+
if (balance !== void 0) {
|
|
599
|
+
this.onStateUpdate({
|
|
600
|
+
balance
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Re-reads the staking account, one read at a time: a request made while a read runs is served by
|
|
606
|
+
* a single read after it, however many arrive.
|
|
607
|
+
*
|
|
608
|
+
* That follow-up matters. A read already in flight may have left before the withdrawal it should
|
|
609
|
+
* catch completed, so its answer can still hold that HYPE.
|
|
610
|
+
*/
|
|
611
|
+
async requestStakingRead() {
|
|
612
|
+
this.stakingReadRequested = true;
|
|
613
|
+
if (this.stakingReadRunning) {
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
const generation = this.stakingReadGeneration;
|
|
617
|
+
this.stakingReadRunning = true;
|
|
618
|
+
while (this.stakingReadRequested && generation === this.stakingReadGeneration) {
|
|
619
|
+
this.stakingReadRequested = false;
|
|
620
|
+
await this.refreshStakingAccount(generation);
|
|
621
|
+
}
|
|
622
|
+
if (generation === this.stakingReadGeneration) {
|
|
623
|
+
this.stakingReadRunning = false;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Requests at most one staking read per interval from spot frames, preserving one trailing read
|
|
628
|
+
* when more frames ask during the interval.
|
|
629
|
+
*/
|
|
630
|
+
requestStakingReadFromSpotState() {
|
|
631
|
+
if (this.stakingEventReadStarting || this.stakingEventReadTimer !== null) {
|
|
632
|
+
this.stakingEventReadPending = true;
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
this.stakingEventReadStarting = true;
|
|
636
|
+
void this.startStakingReadInterval(this.stakingReadGeneration);
|
|
637
|
+
}
|
|
638
|
+
/** Starts one spot-triggered read, then opens the interval selected by its pending count. */
|
|
639
|
+
async startStakingReadInterval(generation) {
|
|
640
|
+
await this.requestStakingRead();
|
|
641
|
+
if (generation !== this.stakingReadGeneration) {
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
this.stakingEventReadStarting = false;
|
|
645
|
+
const interval = this.hasQueuedWithdrawal ? STAKING_QUEUED_READ_INTERVAL_MS : STAKING_SPECULATIVE_READ_INTERVAL_MS;
|
|
646
|
+
this.stakingEventReadTimer = setTimeout(() => {
|
|
647
|
+
this.stakingEventReadTimer = null;
|
|
648
|
+
if (!this.stakingEventReadPending) {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
this.stakingEventReadPending = false;
|
|
652
|
+
this.requestStakingReadFromSpotState();
|
|
653
|
+
}, interval);
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Stops the staking re-reads; the cached figure stays, as it was true when last read.
|
|
657
|
+
*
|
|
658
|
+
* Every stop opens a new generation (a session ending, or a restart), so a read still in flight
|
|
659
|
+
* from before it can never land.
|
|
660
|
+
*/
|
|
661
|
+
stopStakingReads() {
|
|
662
|
+
if (this.stakingRetryTimer !== null) {
|
|
663
|
+
clearTimeout(this.stakingRetryTimer);
|
|
664
|
+
this.stakingRetryTimer = null;
|
|
665
|
+
}
|
|
666
|
+
if (this.stakingEventReadTimer !== null) {
|
|
667
|
+
clearTimeout(this.stakingEventReadTimer);
|
|
668
|
+
this.stakingEventReadTimer = null;
|
|
669
|
+
}
|
|
670
|
+
this.stakingEventReadStarting = false;
|
|
671
|
+
this.stakingEventReadPending = false;
|
|
672
|
+
this.stakingReadGeneration += 1;
|
|
673
|
+
this.stakingReadRunning = false;
|
|
674
|
+
this.stakingReadRequested = false;
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Reads the staking account once, then republishes the balance with it.
|
|
678
|
+
*
|
|
679
|
+
* A failed read is reported and retried after `STAKING_READ_RETRY_MS`, keeping the last figure in
|
|
680
|
+
* place until one succeeds.
|
|
681
|
+
*/
|
|
682
|
+
async refreshStakingAccount(generation) {
|
|
683
|
+
let delegator;
|
|
684
|
+
try {
|
|
685
|
+
delegator = await fetchDelegatorSummary(this.addrLower(), this.demo);
|
|
686
|
+
} catch (err) {
|
|
687
|
+
this.onError(err instanceof Error ? err : new Error(String(err)));
|
|
688
|
+
this.scheduleStakingRetry(generation);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
if (generation !== this.stakingReadGeneration) {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
this.stakedHype = _stakingAccountHype(delegator);
|
|
695
|
+
this.hasQueuedWithdrawal = delegator.nPendingWithdrawals > 0;
|
|
696
|
+
const balance = this.buildBalance();
|
|
697
|
+
if (balance !== void 0) {
|
|
698
|
+
this.onStateUpdate({
|
|
699
|
+
balance
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
/** Retries a failed staking read once `STAKING_READ_RETRY_MS` has passed, unless the session stopped. */
|
|
704
|
+
scheduleStakingRetry(generation) {
|
|
705
|
+
if (generation !== this.stakingReadGeneration || this.stakingRetryTimer !== null) {
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
this.stakingRetryTimer = setTimeout(() => {
|
|
709
|
+
this.stakingRetryTimer = null;
|
|
710
|
+
void this.requestStakingRead();
|
|
711
|
+
}, STAKING_READ_RETRY_MS);
|
|
712
|
+
}
|
|
481
713
|
/** The floor of `availableToTrade` over the tracked set; `null` when nothing is tracked. */
|
|
482
714
|
availableMargin() {
|
|
483
715
|
return minimumAvailableToTrade(this.trackedAssetData.values());
|
|
@@ -502,6 +734,10 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
|
|
|
502
734
|
return result;
|
|
503
735
|
}
|
|
504
736
|
}
|
|
737
|
+
function _stakingAccountHype(delegator) {
|
|
738
|
+
return toDecimal(delegator.delegated).plus(toDecimal(delegator.undelegated)).plus(toDecimal(delegator.totalPendingWithdrawal)).toFixed();
|
|
739
|
+
}
|
|
740
|
+
__name(_stakingAccountHype, "_stakingAccountHype");
|
|
505
741
|
function buildTrackedAssetInfo(data) {
|
|
506
742
|
return {
|
|
507
743
|
coin: data.coin,
|
|
@@ -540,7 +776,9 @@ function mapSpotBalances(spotState) {
|
|
|
540
776
|
hold: balance.hold,
|
|
541
777
|
// An absent `ltv` means "not collateral", which weighs zero — never a zero weight, which is
|
|
542
778
|
// a stablecoin at full value. The two states stay apart from the wire onward.
|
|
543
|
-
ltv: balance.ltv ?? null
|
|
779
|
+
ltv: balance.ltv ?? null,
|
|
780
|
+
// An absent `borrowed` means the coin was never borrowed; the key rides only a borrowed coin.
|
|
781
|
+
borrowed: balance.borrowed ?? null
|
|
544
782
|
});
|
|
545
783
|
}
|
|
546
784
|
return balances;
|
|
@@ -550,10 +788,10 @@ function mapBorrowRatios(spotState) {
|
|
|
550
788
|
return new Map(spotState.tokenToPortfolioBorrowRatio ?? []);
|
|
551
789
|
}
|
|
552
790
|
__name(mapBorrowRatios, "mapBorrowRatios");
|
|
553
|
-
function
|
|
554
|
-
return toDecimal(
|
|
791
|
+
function spotHypeRose(previousHype, balances) {
|
|
792
|
+
return toDecimal(balances.get("HYPE")?.total ?? "0").gt(toDecimal(previousHype ?? "0"));
|
|
555
793
|
}
|
|
556
|
-
__name(
|
|
794
|
+
__name(spotHypeRose, "spotHypeRose");
|
|
557
795
|
function sumRestVaultEquity(vaults) {
|
|
558
796
|
return vaults.reduce((total, vault) => total.plus(toDecimal(vault.equity)), ZERO).toFixed();
|
|
559
797
|
}
|
|
@@ -563,4 +801,4 @@ function sumLiveVaultEquity(perpDexStates) {
|
|
|
563
801
|
}
|
|
564
802
|
__name(sumLiveVaultEquity, "sumLiveVaultEquity");
|
|
565
803
|
|
|
566
|
-
export { HyperliquidAccountTracker };
|
|
804
|
+
export { HyperliquidAccountTracker, _stakingAccountHype };
|
package/dist/tracker/index.js
CHANGED
|
@@ -13,7 +13,7 @@ export { ACCOUNT_MODE_LABELS, AccountBalanceCore, AccountMode, CollateralBalance
|
|
|
13
13
|
export { BaseAccountTracker } from './base';
|
|
14
14
|
export { BinanceMultiAssetBalance, BinanceSingleAssetBalance } from './binance/balance';
|
|
15
15
|
export { BybitIsolatedMarginBalance, BybitPortfolioMarginBalance, BybitRegularMarginBalance } from './bybit/balance';
|
|
16
|
-
export { HyperliquidDexState, HyperliquidPortfolioMarginBalance, HyperliquidPortfolioMarginCollateral, HyperliquidStandardBalance, HyperliquidUnifiedBalance } from './hyperliquid/balance';
|
|
16
|
+
export { HyperliquidBorrowLendReserve, HyperliquidDexState, HyperliquidPortfolioMarginBalance, HyperliquidPortfolioMarginCollateral, HyperliquidStandardBalance, HyperliquidUnifiedBalance } from './hyperliquid/balance';
|
|
17
17
|
export { LighterClassicBalance, LighterUnifiedBalance } from './lighter/balance';
|
|
18
18
|
export { LIGHTER_QUOTE_ASSETS } from './lighter/const';
|
|
19
19
|
export { OkxFuturesBalance, OkxMultiCurrencyMarginBalance, OkxPortfolioMarginBalance, OkxSpotBalance } from './okx/balance';
|
|
@@ -45,8 +45,7 @@ function createAccountTracker(exchange, config) {
|
|
|
45
45
|
return new HyperliquidAccountTracker({
|
|
46
46
|
demo: config.demo,
|
|
47
47
|
onError: config.onError,
|
|
48
|
-
|
|
49
|
-
credentials: config.credentials
|
|
48
|
+
address: config.account.exchangeIdentifier
|
|
50
49
|
});
|
|
51
50
|
case "lighter":
|
|
52
51
|
case "lighter_rhc":
|
|
@@ -38,6 +38,7 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
38
38
|
accountIdentifier;
|
|
39
39
|
apiKeyIndex;
|
|
40
40
|
apiPrivateKey;
|
|
41
|
+
creds;
|
|
41
42
|
ws = null;
|
|
42
43
|
messageListenerId = null;
|
|
43
44
|
openListenerId = null;
|
|
@@ -75,6 +76,7 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
75
76
|
this.accountIdentifier = config.account.exchangeIdentifier;
|
|
76
77
|
this.apiKeyIndex = credentials.api_key_index;
|
|
77
78
|
this.apiPrivateKey = credentials.api_private_key;
|
|
79
|
+
this.creds = credentials;
|
|
78
80
|
}
|
|
79
81
|
/**
|
|
80
82
|
* The tier Lighter has the account on, and every later change to it.
|
|
@@ -177,7 +179,7 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
177
179
|
});
|
|
178
180
|
const ws = (await createExchangeWs(this.connector, {
|
|
179
181
|
demo: this.demo,
|
|
180
|
-
credentials: this.
|
|
182
|
+
credentials: this.creds
|
|
181
183
|
})).main;
|
|
182
184
|
if (lifecycleVersion !== this.lifecycleVersion) {
|
|
183
185
|
ws.destroy();
|
|
@@ -502,12 +504,11 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
502
504
|
return;
|
|
503
505
|
}
|
|
504
506
|
try {
|
|
505
|
-
const credentials = this.credentials;
|
|
506
507
|
const accountIndex = Number(this.accountIdentifier);
|
|
507
508
|
const authToken = await retrieveLighterAuthToken(this.sdk, this.connector, {
|
|
508
509
|
accountIndex,
|
|
509
|
-
apiKeyIndex:
|
|
510
|
-
apiPrivateKey:
|
|
510
|
+
apiKeyIndex: this.creds.api_key_index,
|
|
511
|
+
apiPrivateKey: this.creds.api_private_key
|
|
511
512
|
}, this.demo);
|
|
512
513
|
const limits = await fetchAccountLimits(accountIndex, authToken, this.connector, this.demo);
|
|
513
514
|
const accountTier = toLighterAccountTier(limits.user_tier);
|
|
@@ -65,7 +65,7 @@ class OkxAccountTracker extends BaseAccountTracker {
|
|
|
65
65
|
this.handleInitialData(balance, positions);
|
|
66
66
|
this.ws = await createExchangeWs("okx", {
|
|
67
67
|
demo: this.demo,
|
|
68
|
-
credentials: this.
|
|
68
|
+
credentials: this.creds
|
|
69
69
|
});
|
|
70
70
|
this.messageListenerId = this.ws.main.addMessageListener((raw) => this.routeRaw(raw));
|
|
71
71
|
this.openListenerId = this.ws.main.addOpenListener(() => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pear-protocol/exchanges-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Pear Protocol Exchanges SDK",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"test:watch": "vitest"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@pear-protocol/core-sdk": "^2.
|
|
31
|
-
"@pear-protocol/types": "^2.
|
|
32
|
-
"@pear-protocol/utils": "^1.
|
|
30
|
+
"@pear-protocol/core-sdk": "^2.3.0",
|
|
31
|
+
"@pear-protocol/types": "^2.10.0",
|
|
32
|
+
"@pear-protocol/utils": "^1.2.0",
|
|
33
33
|
"bignumber.js": "^11.1.1",
|
|
34
34
|
"partysocket": "^1.0.3",
|
|
35
35
|
"viem": "^2.48.8",
|