@pear-protocol/exchanges-sdk 0.3.0 → 0.5.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 +78 -12
- package/dist/client/binance/rest.js +4 -3
- package/dist/client/bybit/rest.js +2 -1
- package/dist/client/hyperliquid/client.test.js +41 -0
- package/dist/client/hyperliquid/exchange-ws.js +3 -2
- package/dist/client/hyperliquid/rest.js +28 -5
- package/dist/client/hyperliquid/ws.js +16 -4
- package/dist/client/index.js +2 -2
- package/dist/client/lighter/constants.js +9 -1
- package/dist/client/lighter/constants.test.js +22 -0
- package/dist/client/lighter/exchange-ws.js +2 -2
- package/dist/client/lighter/rest.js +110 -37
- package/dist/client/lighter/rest.test.js +152 -0
- package/dist/client/lighter/ws.js +7 -2
- package/dist/client/okx/rest.js +20 -10
- package/dist/helper/factory.js +2 -2
- package/dist/helper/hyperliquid/helper.js +28 -13
- package/dist/helper/hyperliquid/helper.test.js +45 -0
- package/dist/helper/lighter/helper.js +77 -16
- package/dist/helper/lighter/helper.test.js +289 -0
- package/dist/helper/lighter/types.js +9 -8
- package/dist/index.d.ts +189 -38
- package/dist/sdk.js +22 -16
- package/dist/sdk.test.js +101 -0
- package/dist/status/hyperliquid/status.js +18 -5
- package/dist/status/hyperliquid/status.test.js +81 -0
- package/dist/status/lighter/status.js +18 -11
- package/dist/status/lighter/status.test.js +75 -0
- package/dist/syncer/base.js +0 -2
- package/dist/syncer/base.test.js +0 -1
- package/dist/syncer/hyperliquid/syncer.js +96 -43
- package/dist/syncer/hyperliquid/syncer.test.js +274 -0
- package/dist/syncer/lighter/funding.js +25 -0
- package/dist/syncer/lighter/funding.test.js +48 -0
- package/dist/syncer/lighter/syncer.js +30 -6
- package/dist/syncer/lighter/syncer.test.js +132 -6
- package/dist/tracker/base.js +23 -6
- package/dist/tracker/base.test.js +132 -0
- package/dist/tracker/binance/tracker.js +48 -18
- package/dist/tracker/binance/tracker.test.js +138 -0
- package/dist/tracker/bybit/tracker.js +78 -23
- package/dist/tracker/bybit/tracker.test.js +145 -0
- package/dist/tracker/hyperliquid/mapper.js +244 -44
- package/dist/tracker/hyperliquid/mapper.test.js +255 -0
- package/dist/tracker/hyperliquid/tracker.js +241 -74
- package/dist/tracker/hyperliquid/tracker.test.js +587 -0
- package/dist/tracker/index.js +15 -37
- package/dist/tracker/lighter/mapper.js +163 -33
- package/dist/tracker/lighter/mapper.test.js +199 -0
- package/dist/tracker/lighter/tracker.js +219 -45
- package/dist/tracker/lighter/tracker.test.js +515 -0
- package/dist/tracker/lighter/types.js +10 -2
- package/dist/tracker/okx/tracker.js +50 -16
- package/dist/tracker/okx/tracker.test.js +126 -0
- package/dist/tracker/types.js +2 -1
- package/dist/tracker/utils.test.js +8 -0
- package/package.json +6 -5
|
@@ -1,67 +1,161 @@
|
|
|
1
1
|
import BigNumber from 'bignumber.js';
|
|
2
|
-
import {
|
|
2
|
+
import { createExchangeWs } from '../../client';
|
|
3
|
+
import { fetchOrderBookDetails, fetchAccount, fetchAccountLimits } from '../../client/lighter/rest';
|
|
3
4
|
import { BaseAccountTracker } from '../base';
|
|
4
|
-
import {
|
|
5
|
+
import { mapPositions, buildAccountBalance, mapMarketDefaultAssetInfo, mapBalance, toLighterAccountTier } from './mapper';
|
|
6
|
+
import { LighterAccountType } from './types';
|
|
5
7
|
|
|
8
|
+
const ACCOUNT_TIER_REFRESH_INTERVAL_MS = 6e4;
|
|
9
|
+
function toPosRecord(positions) {
|
|
10
|
+
const posRecord = {};
|
|
11
|
+
for (const p of positions) {
|
|
12
|
+
posRecord[String(p.market_id)] = p;
|
|
13
|
+
}
|
|
14
|
+
return posRecord;
|
|
15
|
+
}
|
|
6
16
|
class LighterAccountTracker extends BaseAccountTracker {
|
|
7
17
|
accountIdentifier;
|
|
8
|
-
|
|
18
|
+
sdk;
|
|
19
|
+
ws = null;
|
|
9
20
|
messageListenerId = null;
|
|
10
21
|
openListenerId = null;
|
|
11
22
|
pollTimer = null;
|
|
23
|
+
pollInFlight = null;
|
|
24
|
+
startInFlight = null;
|
|
25
|
+
lifecycleVersion = 0;
|
|
26
|
+
stateVersion = 0;
|
|
12
27
|
assetsByAssetId = /* @__PURE__ */ new Map();
|
|
28
|
+
rawPositionsByMarketId = /* @__PURE__ */ new Map();
|
|
29
|
+
marketsByMarketId = /* @__PURE__ */ new Map();
|
|
13
30
|
summary = null;
|
|
31
|
+
accountTier;
|
|
32
|
+
lastAccountTierFetchAt = 0;
|
|
14
33
|
hasAssets = false;
|
|
15
34
|
constructor(config) {
|
|
16
35
|
super(config);
|
|
17
|
-
this.credentials = config.credentials;
|
|
18
36
|
this.accountIdentifier = config.account.exchangeIdentifier;
|
|
19
|
-
this.
|
|
37
|
+
this.sdk = config.sdk;
|
|
38
|
+
}
|
|
39
|
+
/** Stateless one-shot account read; tier enrichment is optional and requires SDK-backed auth. */
|
|
40
|
+
async snapshot() {
|
|
41
|
+
const [account, markets, accountTier] = await Promise.all([
|
|
42
|
+
this.fetchPrimaryAccount(),
|
|
43
|
+
fetchOrderBookDetails(this.demo),
|
|
44
|
+
this.fetchAccountTier()
|
|
45
|
+
]);
|
|
46
|
+
if (!account) return { balance: null, positions: [] };
|
|
47
|
+
const { positions } = mapPositions(toPosRecord(account.positions ?? []));
|
|
48
|
+
const marketsByMarketId = new Map(markets.map((market) => [market.market_id, market]));
|
|
49
|
+
return {
|
|
50
|
+
balance: buildAccountBalance(account, marketsByMarketId, accountTier),
|
|
51
|
+
positions: Array.from(positions.values())
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Stateless one-shot per-asset settings read, keyed by market id (the same identifier the live
|
|
56
|
+
* trackAsset path uses). One public `GET /api/v1/account` covers every market the account holds
|
|
57
|
+
* a position row for (custom margin included, even when its size is zero); `orderBookDetails`
|
|
58
|
+
* supplies the exchange default for markets absent from the account payload.
|
|
59
|
+
*/
|
|
60
|
+
async fetchAssetSnapshots(marketIds) {
|
|
61
|
+
const [account, markets] = await Promise.all([this.fetchPrimaryAccount(), fetchOrderBookDetails(this.demo)]);
|
|
62
|
+
const { trackedAssets } = mapPositions(toPosRecord(account?.positions ?? []), true);
|
|
63
|
+
const defaults = /* @__PURE__ */ new Map();
|
|
64
|
+
for (const m of markets) {
|
|
65
|
+
defaults.set(String(m.market_id), mapMarketDefaultAssetInfo(m));
|
|
66
|
+
}
|
|
67
|
+
const result = {};
|
|
68
|
+
for (const marketId of marketIds) {
|
|
69
|
+
result[marketId] = trackedAssets.get(marketId) ?? defaults.get(marketId) ?? null;
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
start() {
|
|
74
|
+
if (this.startInFlight) {
|
|
75
|
+
return this.startInFlight;
|
|
76
|
+
}
|
|
77
|
+
if (this.ws || this.pollTimer) {
|
|
78
|
+
return Promise.resolve();
|
|
79
|
+
}
|
|
80
|
+
const start = this.performStart().finally(() => {
|
|
81
|
+
if (this.startInFlight === start) {
|
|
82
|
+
this.startInFlight = null;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
this.startInFlight = start;
|
|
86
|
+
return start;
|
|
20
87
|
}
|
|
21
|
-
|
|
22
|
-
|
|
88
|
+
/** Creates one tracker lifecycle while repeated callers share the same pending start. */
|
|
89
|
+
async performStart() {
|
|
90
|
+
const lifecycleVersion = ++this.lifecycleVersion;
|
|
91
|
+
const [markets, accountTier] = await Promise.all([fetchOrderBookDetails(this.demo), this.fetchAccountTier()]);
|
|
92
|
+
if (lifecycleVersion !== this.lifecycleVersion) return;
|
|
93
|
+
this.accountTier = accountTier;
|
|
94
|
+
this.lastAccountTierFetchAt = Date.now();
|
|
23
95
|
const trackedAssets = /* @__PURE__ */ new Map();
|
|
24
96
|
for (const m of markets) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
leverage: leverageFromImf(new BigNumber(m.default_initial_margin_fraction).div(100).toFixed()),
|
|
28
|
-
marginType: "cross"
|
|
29
|
-
});
|
|
97
|
+
this.marketsByMarketId.set(m.market_id, m);
|
|
98
|
+
trackedAssets.set(String(m.market_id), mapMarketDefaultAssetInfo(m));
|
|
30
99
|
}
|
|
31
100
|
this.onStateUpdate({ trackedAssets });
|
|
101
|
+
const ws = (await createExchangeWs("lighter", { demo: this.demo, credentials: this.credentials })).main;
|
|
102
|
+
if (lifecycleVersion !== this.lifecycleVersion) {
|
|
103
|
+
ws.destroy();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
this.ws = ws;
|
|
32
107
|
this.messageListenerId = this.ws.addMessageListener((raw) => this.routeRaw(raw));
|
|
33
|
-
this.openListenerId = this.ws.addOpenListener(() => this.
|
|
34
|
-
this.
|
|
35
|
-
this.pollAccount();
|
|
36
|
-
this.pollTimer = setInterval(() => this.pollAccount(), 2e3);
|
|
108
|
+
this.openListenerId = this.ws.addOpenListener(() => this.handleWsOpen());
|
|
109
|
+
this.handleWsOpen();
|
|
110
|
+
void this.pollAccount();
|
|
111
|
+
this.pollTimer = setInterval(() => void this.pollAccount(), 2e3);
|
|
37
112
|
}
|
|
38
113
|
async cleanupConnections() {
|
|
114
|
+
this.lifecycleVersion += 1;
|
|
115
|
+
this.startInFlight = null;
|
|
39
116
|
if (this.pollTimer) {
|
|
40
117
|
clearInterval(this.pollTimer);
|
|
41
118
|
this.pollTimer = null;
|
|
42
119
|
}
|
|
43
120
|
if (this.messageListenerId) {
|
|
44
|
-
this.ws
|
|
121
|
+
this.ws?.removeMessageListener(this.messageListenerId);
|
|
45
122
|
this.messageListenerId = null;
|
|
46
123
|
}
|
|
47
124
|
if (this.openListenerId) {
|
|
48
|
-
this.ws
|
|
125
|
+
this.ws?.removeOpenListener(this.openListenerId);
|
|
49
126
|
this.openListenerId = null;
|
|
50
127
|
}
|
|
128
|
+
this.ws?.destroy();
|
|
129
|
+
this.ws = null;
|
|
51
130
|
}
|
|
52
131
|
get isConnected() {
|
|
53
|
-
return this.ws
|
|
132
|
+
return this.ws?.connected ?? false;
|
|
54
133
|
}
|
|
55
134
|
ensureAssetTracked(_coin) {
|
|
56
135
|
}
|
|
57
136
|
releaseAsset(_coin) {
|
|
58
137
|
}
|
|
138
|
+
/** Starts a clean account-data epoch before subscribing on each websocket connection. */
|
|
139
|
+
handleWsOpen() {
|
|
140
|
+
this.stateVersion += 1;
|
|
141
|
+
this.assetsByAssetId.clear();
|
|
142
|
+
this.rawPositionsByMarketId.clear();
|
|
143
|
+
this.summary = null;
|
|
144
|
+
this.hasAssets = false;
|
|
145
|
+
this.resetAccountState();
|
|
146
|
+
const trackedAssets = /* @__PURE__ */ new Map();
|
|
147
|
+
for (const market of this.marketsByMarketId.values()) {
|
|
148
|
+
trackedAssets.set(String(market.market_id), mapMarketDefaultAssetInfo(market));
|
|
149
|
+
}
|
|
150
|
+
this.onStateUpdate({ trackedAssets });
|
|
151
|
+
this.sendStaticSubs();
|
|
152
|
+
}
|
|
59
153
|
sendStaticSubs() {
|
|
60
|
-
this.ws
|
|
154
|
+
this.ws?.send({
|
|
61
155
|
type: "subscribe",
|
|
62
156
|
channel: `account_all/${this.accountIdentifier}`
|
|
63
157
|
});
|
|
64
|
-
this.ws
|
|
158
|
+
this.ws?.send({
|
|
65
159
|
type: "subscribe",
|
|
66
160
|
channel: `user_stats/${this.accountIdentifier}`
|
|
67
161
|
});
|
|
@@ -87,16 +181,37 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
87
181
|
}
|
|
88
182
|
}
|
|
89
183
|
onAccountAll(data) {
|
|
184
|
+
this.stateVersion += 1;
|
|
90
185
|
const update = {};
|
|
186
|
+
const isSnapshot = data.type === "subscribed/account_all";
|
|
91
187
|
if (data.assets) {
|
|
188
|
+
if (isSnapshot) {
|
|
189
|
+
this.assetsByAssetId.clear();
|
|
190
|
+
}
|
|
92
191
|
for (const [id, asset] of Object.entries(data.assets)) {
|
|
93
192
|
this.assetsByAssetId.set(id, asset);
|
|
94
193
|
}
|
|
95
194
|
this.hasAssets = true;
|
|
96
195
|
}
|
|
97
|
-
if (data.positions
|
|
98
|
-
const
|
|
196
|
+
if (data.positions) {
|
|
197
|
+
const positionsForMapping = { ...data.positions };
|
|
198
|
+
if (isSnapshot) {
|
|
199
|
+
this.rawPositionsByMarketId.clear();
|
|
200
|
+
}
|
|
201
|
+
for (const [marketId, position] of Object.entries(data.positions)) {
|
|
202
|
+
if (new BigNumber(position.position).isZero()) {
|
|
203
|
+
const previousPosition = this.rawPositionsByMarketId.get(marketId);
|
|
204
|
+
if (previousPosition) {
|
|
205
|
+
positionsForMapping[marketId] = { ...position, sign: previousPosition.sign };
|
|
206
|
+
}
|
|
207
|
+
this.rawPositionsByMarketId.delete(marketId);
|
|
208
|
+
} else {
|
|
209
|
+
this.rawPositionsByMarketId.set(marketId, position);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const { positions, trackedAssets } = mapPositions(positionsForMapping, !isSnapshot);
|
|
99
213
|
update.positions = positions;
|
|
214
|
+
update.positionsMode = isSnapshot ? "replace" : "merge";
|
|
100
215
|
update.trackedAssets = trackedAssets;
|
|
101
216
|
}
|
|
102
217
|
const balance = this.buildBalance();
|
|
@@ -104,46 +219,105 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
104
219
|
if (Object.keys(update).length > 0) this.onStateUpdate(update);
|
|
105
220
|
}
|
|
106
221
|
onUserStats(data) {
|
|
222
|
+
this.stateVersion += 1;
|
|
107
223
|
const s = data.stats;
|
|
108
224
|
this.summary = {
|
|
109
225
|
collateral: s.collateral || "0",
|
|
110
226
|
portfolioValue: s.portfolio_value || "0",
|
|
227
|
+
crossPortfolioValue: s.cross_stats?.portfolio_value,
|
|
111
228
|
availableToTrade: s.available_balance || "0",
|
|
112
|
-
marginUsagePct: s.margin_usage || "0"
|
|
229
|
+
marginUsagePct: s.margin_usage || "0",
|
|
230
|
+
accountType: s.account_trading_mode === 1 ? LighterAccountType.UNIFIED : LighterAccountType.CLASSIC,
|
|
231
|
+
accountTier: this.accountTier
|
|
113
232
|
};
|
|
114
233
|
const balance = this.buildBalance();
|
|
115
234
|
if (balance) this.onStateUpdate({ balance });
|
|
116
235
|
}
|
|
117
236
|
buildBalance() {
|
|
118
237
|
if (!this.summary || !this.hasAssets) return null;
|
|
119
|
-
return mapBalance(
|
|
238
|
+
return mapBalance(
|
|
239
|
+
{ ...this.summary, accountTier: this.accountTier },
|
|
240
|
+
this.assetsByAssetId,
|
|
241
|
+
this.rawPositionsByMarketId.values(),
|
|
242
|
+
this.marketsByMarketId
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
async fetchPrimaryAccount() {
|
|
246
|
+
const data = await fetchAccount(Number(this.accountIdentifier), this.demo);
|
|
247
|
+
return data.accounts[0];
|
|
120
248
|
}
|
|
121
|
-
async
|
|
249
|
+
async fetchAccountTier() {
|
|
250
|
+
if (!this.sdk || this.demo) return void 0;
|
|
122
251
|
try {
|
|
252
|
+
const credentials = this.credentials;
|
|
123
253
|
const accountIndex = Number(this.accountIdentifier);
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
symbol: p.symbol,
|
|
132
|
-
initial_margin_fraction: p.initial_margin_fraction,
|
|
133
|
-
sign: p.sign,
|
|
134
|
-
position: p.position,
|
|
135
|
-
avg_entry_price: p.avg_entry_price,
|
|
136
|
-
unrealized_pnl: p.unrealized_pnl,
|
|
137
|
-
liquidation_price: p.liquidation_price,
|
|
138
|
-
margin_mode: p.margin_mode
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
const { positions, trackedAssets } = mapPositions(posRecord);
|
|
142
|
-
this.onStateUpdate({ positions, trackedAssets });
|
|
254
|
+
const { token } = await this.sdk.core.misc.lighter.createAuthToken({
|
|
255
|
+
accountIndex,
|
|
256
|
+
apiKeyIndex: credentials.api_key_index,
|
|
257
|
+
apiPrivateKey: credentials.api_private_key
|
|
258
|
+
});
|
|
259
|
+
const limits = await fetchAccountLimits(accountIndex, token, this.demo);
|
|
260
|
+
return toLighterAccountTier(limits.user_tier);
|
|
143
261
|
} catch (err) {
|
|
144
262
|
this.onError(err instanceof Error ? err : new Error(String(err)));
|
|
263
|
+
return void 0;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
pollAccount() {
|
|
267
|
+
if (this.pollInFlight) return this.pollInFlight;
|
|
268
|
+
const lifecycleVersion = this.lifecycleVersion;
|
|
269
|
+
const stateVersion = this.stateVersion;
|
|
270
|
+
const poll = this.performAccountPoll(lifecycleVersion, stateVersion).finally(() => {
|
|
271
|
+
if (this.pollInFlight === poll) {
|
|
272
|
+
this.pollInFlight = null;
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
this.pollInFlight = poll;
|
|
276
|
+
return poll;
|
|
277
|
+
}
|
|
278
|
+
async performAccountPoll(lifecycleVersion, stateVersion) {
|
|
279
|
+
try {
|
|
280
|
+
const refreshAccountTier = this.shouldRefreshAccountTier();
|
|
281
|
+
if (refreshAccountTier) {
|
|
282
|
+
this.lastAccountTierFetchAt = Date.now();
|
|
283
|
+
}
|
|
284
|
+
const [account, refreshedAccountTier] = await Promise.all([
|
|
285
|
+
this.fetchPrimaryAccount(),
|
|
286
|
+
refreshAccountTier ? this.fetchAccountTier() : Promise.resolve(void 0)
|
|
287
|
+
]);
|
|
288
|
+
if (lifecycleVersion !== this.lifecycleVersion) return;
|
|
289
|
+
if (refreshAccountTier && refreshedAccountTier !== void 0 && refreshedAccountTier !== this.accountTier) {
|
|
290
|
+
this.accountTier = refreshedAccountTier;
|
|
291
|
+
const balance = this.buildBalance();
|
|
292
|
+
if (balance) {
|
|
293
|
+
this.onStateUpdate({ balance });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (!account || stateVersion !== this.stateVersion) return;
|
|
297
|
+
const positionRecord = toPosRecord(account.positions ?? []);
|
|
298
|
+
const { positions, trackedAssets } = mapPositions(positionRecord, true);
|
|
299
|
+
this.stateVersion += 1;
|
|
300
|
+
this.rawPositionsByMarketId.clear();
|
|
301
|
+
for (const [marketId, position] of Object.entries(positionRecord)) {
|
|
302
|
+
if (!new BigNumber(position.position).isZero()) {
|
|
303
|
+
this.rawPositionsByMarketId.set(marketId, position);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
this.onStateUpdate({
|
|
307
|
+
balance: buildAccountBalance(account, this.marketsByMarketId, this.accountTier),
|
|
308
|
+
positions,
|
|
309
|
+
positionsMode: "replace",
|
|
310
|
+
trackedAssets
|
|
311
|
+
});
|
|
312
|
+
} catch (err) {
|
|
313
|
+
if (lifecycleVersion === this.lifecycleVersion) {
|
|
314
|
+
this.onError(err instanceof Error ? err : new Error(String(err)));
|
|
315
|
+
}
|
|
145
316
|
}
|
|
146
317
|
}
|
|
318
|
+
shouldRefreshAccountTier() {
|
|
319
|
+
return this.sdk !== void 0 && Date.now() - this.lastAccountTierFetchAt >= ACCOUNT_TIER_REFRESH_INTERVAL_MS;
|
|
320
|
+
}
|
|
147
321
|
}
|
|
148
322
|
|
|
149
323
|
export { LighterAccountTracker };
|