@pear-protocol/exchanges-sdk 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/lighter/rest.js +42 -23
- package/dist/client/lighter/ws.js +8 -0
- package/dist/helper/lighter/asset-decimals.helper.js +25 -0
- package/dist/helper/lighter/cctp-deposit.helper.js +30 -105
- package/dist/helper/lighter/deposit-settlement.helper.js +100 -0
- package/dist/helper/lighter/deposit.helper.js +28 -0
- package/dist/helper/lighter/ethereum/usdc.helper.js +17 -7
- package/dist/helper/lighter/ethereum-deposit.helper.js +44 -0
- package/dist/helper/lighter/funxyz-deposit.helper.js +257 -0
- package/dist/helper/lighter/helper.js +38 -55
- package/dist/helper/lighter/index.js +3 -1
- package/dist/helper/lighter/types.js +34 -1
- package/dist/index.d.ts +329 -57
- package/dist/sdk.js +2 -0
- package/dist/status/lighter/status.js +6 -1
- package/dist/syncer/lighter/syncer.js +3 -1
- package/dist/tracker/index.js +1 -0
- package/dist/tracker/lighter/mapper.js +17 -1
- package/dist/tracker/lighter/tracker.js +170 -18
- package/package.json +9 -6
|
@@ -138,6 +138,22 @@ function toLighterAccountType(accountTradingMode) {
|
|
|
138
138
|
return accountTradingMode === 1 ? LighterAccountType.UNIFIED : LighterAccountType.CLASSIC;
|
|
139
139
|
}
|
|
140
140
|
__name(toLighterAccountType, "toLighterAccountType");
|
|
141
|
+
function toLighterAccountTier(userTier) {
|
|
142
|
+
switch (userTier?.trim().toLowerCase()) {
|
|
143
|
+
case "std":
|
|
144
|
+
case "standard": {
|
|
145
|
+
return "standard";
|
|
146
|
+
}
|
|
147
|
+
case "plus": {
|
|
148
|
+
return "plus";
|
|
149
|
+
}
|
|
150
|
+
case "premium": {
|
|
151
|
+
return "premium";
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
__name(toLighterAccountTier, "toLighterAccountTier");
|
|
141
157
|
function toLighterBalance(figures) {
|
|
142
158
|
const spotEquity = figures.equity.spot;
|
|
143
159
|
const perpetualsEquity = figures.equity.perpetuals;
|
|
@@ -393,4 +409,4 @@ function signed(value) {
|
|
|
393
409
|
}
|
|
394
410
|
__name(signed, "signed");
|
|
395
411
|
|
|
396
|
-
export { buildSpotPriceIndex, leverageFromImf, mapLiveBalance, mapMarketDefaultAssetInfo, mapPositions, mapRestBalance, toLighterAccountType };
|
|
412
|
+
export { buildSpotPriceIndex, leverageFromImf, mapLiveBalance, mapMarketDefaultAssetInfo, mapPositions, mapRestBalance, toLighterAccountTier, toLighterAccountType };
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import BigNumber from 'bignumber.js';
|
|
2
2
|
import { createExchangeWs } from '../../client';
|
|
3
|
-
import { fetchOrderBookDetails,
|
|
3
|
+
import { fetchAccount, fetchPublicAccount, fetchOrderBookDetails, fetchPublicOrderBookDetails, fetchAccountLimits } from '../../client/lighter/rest';
|
|
4
|
+
import { retrieveLighterAuthToken } from '../../helper/lighter/auth-token.helper';
|
|
4
5
|
import { BaseAccountTracker } from '../base';
|
|
5
6
|
import { computeMaxBasketSize } from '../basket-size';
|
|
6
7
|
import { toLighterBasketCalculation } from './basket-size';
|
|
7
|
-
import { buildSpotPriceIndex, mapPositions, mapRestBalance, mapMarketDefaultAssetInfo, toLighterAccountType, mapLiveBalance } from './mapper';
|
|
8
|
+
import { buildSpotPriceIndex, mapPositions, mapRestBalance, mapMarketDefaultAssetInfo, toLighterAccountType, mapLiveBalance, toLighterAccountTier } from './mapper';
|
|
8
9
|
|
|
9
10
|
var __defProp = Object.defineProperty;
|
|
10
11
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
11
12
|
const POSITION_POLL_INTERVAL_MS = 2e3;
|
|
13
|
+
const SUBSCRIBE_RETRY_BASE_DELAY_MS = 1e3;
|
|
14
|
+
const SUBSCRIBE_RETRY_MAX_DELAY_MS = 3e4;
|
|
12
15
|
const MARKET_REFRESH_INTERVAL_MS = 6e4;
|
|
13
16
|
const DECIMAL_PATTERN = /^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$/;
|
|
14
17
|
function isZeroSize(size) {
|
|
@@ -30,7 +33,10 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
30
33
|
static {
|
|
31
34
|
__name(this, "LighterAccountTracker");
|
|
32
35
|
}
|
|
36
|
+
sdk;
|
|
33
37
|
accountIdentifier;
|
|
38
|
+
apiKeyIndex;
|
|
39
|
+
apiPrivateKey;
|
|
34
40
|
ws = null;
|
|
35
41
|
messageListenerId = null;
|
|
36
42
|
openListenerId = null;
|
|
@@ -38,8 +44,10 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
38
44
|
positionPollInFlight = null;
|
|
39
45
|
marketRefreshTimer = null;
|
|
40
46
|
marketRefreshInFlight = null;
|
|
47
|
+
subscribeRetryTimer = null;
|
|
41
48
|
startInFlight = null;
|
|
42
49
|
lifecycleVersion = 0;
|
|
50
|
+
wsConnectionVersion = 0;
|
|
43
51
|
stateVersion = 0;
|
|
44
52
|
assetsByAssetId = /* @__PURE__ */ new Map();
|
|
45
53
|
// Written only by `account_all`. The live balance reads its unrealized PnL and maintenance
|
|
@@ -51,16 +59,59 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
51
59
|
// market data arrives, and a balance the index cannot price would otherwise read as unknown.
|
|
52
60
|
spotPriceBySymbol = buildSpotPriceIndex([]);
|
|
53
61
|
stats = null;
|
|
62
|
+
// Null until the authenticated read lands, and null for good where nothing can authenticate it.
|
|
63
|
+
accountTier = null;
|
|
64
|
+
accountTierListeners = /* @__PURE__ */ new Set();
|
|
65
|
+
accountTierInFlight = null;
|
|
54
66
|
hasAssets = false;
|
|
55
67
|
constructor(config) {
|
|
56
68
|
super(config);
|
|
69
|
+
const credentials = config.credentials;
|
|
70
|
+
this.sdk = config.sdk;
|
|
57
71
|
this.accountIdentifier = config.account.exchangeIdentifier;
|
|
72
|
+
this.apiKeyIndex = credentials.api_key_index;
|
|
73
|
+
this.apiPrivateKey = credentials.api_private_key;
|
|
58
74
|
}
|
|
59
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* The tier Lighter has the account on, and every later change to it.
|
|
77
|
+
*
|
|
78
|
+
* A separate channel from the balance on purpose: tier is account configuration, so it neither
|
|
79
|
+
* moves with the account's figures nor belongs to a trading mode. No websocket channel carries
|
|
80
|
+
* it — `/accountLimits` is the only source — so this reports what the last read said, and
|
|
81
|
+
* `refreshAccountTier` is what asks again.
|
|
82
|
+
*/
|
|
83
|
+
trackAccountTier(callback) {
|
|
84
|
+
this.accountTierListeners.add(callback);
|
|
85
|
+
callback(this.accountTier);
|
|
86
|
+
return () => this.accountTierListeners.delete(callback);
|
|
87
|
+
}
|
|
88
|
+
/** The tier as the last read left it, without asking the venue again. */
|
|
89
|
+
getAccountTier() {
|
|
90
|
+
return this.accountTier;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Re-reads the tier and tells every subscriber what changed.
|
|
94
|
+
*
|
|
95
|
+
* Called after something moves the tier — a change this account submitted — because nothing on
|
|
96
|
+
* the socket announces one. Repeated callers share one in-flight read.
|
|
97
|
+
*/
|
|
98
|
+
refreshAccountTier() {
|
|
99
|
+
if (this.accountTierInFlight) {
|
|
100
|
+
return this.accountTierInFlight;
|
|
101
|
+
}
|
|
102
|
+
const refresh = this.performAccountTierRead(this.lifecycleVersion).finally(() => {
|
|
103
|
+
if (this.accountTierInFlight === refresh) {
|
|
104
|
+
this.accountTierInFlight = null;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
this.accountTierInFlight = refresh;
|
|
108
|
+
return refresh;
|
|
109
|
+
}
|
|
110
|
+
/** Stateless one-shot account read — authenticated for connected accounts, and no websocket. */
|
|
60
111
|
async snapshot() {
|
|
61
112
|
const [account, markets] = await Promise.all([
|
|
62
113
|
this.fetchPrimaryAccount(),
|
|
63
|
-
|
|
114
|
+
this.fetchMarkets()
|
|
64
115
|
]);
|
|
65
116
|
if (!account) return {
|
|
66
117
|
balance: null,
|
|
@@ -81,7 +132,7 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
81
132
|
async fetchAssetSnapshots(marketIds) {
|
|
82
133
|
const [account, markets] = await Promise.all([
|
|
83
134
|
this.fetchPrimaryAccount(),
|
|
84
|
-
|
|
135
|
+
this.fetchMarkets()
|
|
85
136
|
]);
|
|
86
137
|
const settings = toAccountSettings(markets.perp, account?.positions ?? []);
|
|
87
138
|
const result = {};
|
|
@@ -105,7 +156,7 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
105
156
|
async snapshotMaxBasketSize(input) {
|
|
106
157
|
const [account, markets] = await Promise.all([
|
|
107
158
|
this.fetchPrimaryAccount(),
|
|
108
|
-
|
|
159
|
+
this.fetchMarkets()
|
|
109
160
|
]);
|
|
110
161
|
if (!account) {
|
|
111
162
|
return computeMaxBasketSize({
|
|
@@ -138,7 +189,7 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
138
189
|
/** Creates one tracker lifecycle while repeated callers share the same pending start. */
|
|
139
190
|
async performStart() {
|
|
140
191
|
const lifecycleVersion = ++this.lifecycleVersion;
|
|
141
|
-
const markets = await
|
|
192
|
+
const markets = await this.fetchMarkets();
|
|
142
193
|
if (lifecycleVersion !== this.lifecycleVersion) return;
|
|
143
194
|
this.spotPriceBySymbol = buildSpotPriceIndex(markets.spot);
|
|
144
195
|
const trackedAssets = /* @__PURE__ */ new Map();
|
|
@@ -165,10 +216,14 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
165
216
|
void this.pollPositions();
|
|
166
217
|
this.positionPollTimer = setInterval(() => void this.pollPositions(), POSITION_POLL_INTERVAL_MS);
|
|
167
218
|
this.marketRefreshTimer = setInterval(() => void this.refreshMarkets(), MARKET_REFRESH_INTERVAL_MS);
|
|
219
|
+
void this.refreshAccountTier();
|
|
168
220
|
}
|
|
169
221
|
async cleanupConnections() {
|
|
170
222
|
this.lifecycleVersion += 1;
|
|
223
|
+
this.wsConnectionVersion += 1;
|
|
171
224
|
this.startInFlight = null;
|
|
225
|
+
this.accountTierInFlight = null;
|
|
226
|
+
this.clearSubscribeRetry();
|
|
172
227
|
if (this.positionPollTimer) {
|
|
173
228
|
clearInterval(this.positionPollTimer);
|
|
174
229
|
this.positionPollTimer = null;
|
|
@@ -197,6 +252,8 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
197
252
|
}
|
|
198
253
|
/** Starts a clean account-data epoch before subscribing on each websocket connection. */
|
|
199
254
|
handleWsOpen() {
|
|
255
|
+
const wsConnectionVersion = ++this.wsConnectionVersion;
|
|
256
|
+
this.clearSubscribeRetry();
|
|
200
257
|
this.stateVersion += 1;
|
|
201
258
|
this.assetsByAssetId.clear();
|
|
202
259
|
this.wsPositionsByMarketId.clear();
|
|
@@ -210,18 +267,47 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
210
267
|
this.onStateUpdate({
|
|
211
268
|
trackedAssets
|
|
212
269
|
});
|
|
213
|
-
this.
|
|
270
|
+
this.subscribeAccountChannels(wsConnectionVersion, 0);
|
|
214
271
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
272
|
+
/**
|
|
273
|
+
* Subscribes this socket epoch's account channels, and keeps trying until one attempt lands.
|
|
274
|
+
*
|
|
275
|
+
* This is the sole subscriber of `account_all` and `user_stats`, and the live balance is built
|
|
276
|
+
* from those two channels alone — see `buildBalance`. A mint that failed once and was never
|
|
277
|
+
* retried therefore left the tracker connected but permanently unsubscribed: the 2s REST poll
|
|
278
|
+
* kept positions moving while the balance silently froze until some incidental reconnect.
|
|
279
|
+
*
|
|
280
|
+
* The wait doubles up to a ceiling, and only a new connection epoch or teardown ends it.
|
|
281
|
+
*/
|
|
282
|
+
subscribeAccountChannels(wsConnectionVersion, attempt) {
|
|
283
|
+
void this.sendAccountSubscriptions(wsConnectionVersion).catch((error) => {
|
|
284
|
+
if (wsConnectionVersion !== this.wsConnectionVersion) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
this.onError(error instanceof Error ? error : new Error(String(error)));
|
|
288
|
+
const delayMs = Math.min(SUBSCRIBE_RETRY_BASE_DELAY_MS * 2 ** attempt, SUBSCRIBE_RETRY_MAX_DELAY_MS);
|
|
289
|
+
this.subscribeRetryTimer = setTimeout(() => {
|
|
290
|
+
this.subscribeRetryTimer = null;
|
|
291
|
+
this.subscribeAccountChannels(wsConnectionVersion, attempt + 1);
|
|
292
|
+
}, delayMs);
|
|
223
293
|
});
|
|
224
294
|
}
|
|
295
|
+
/** Mints a live token for this socket epoch and authenticates every account subscription. */
|
|
296
|
+
async sendAccountSubscriptions(wsConnectionVersion) {
|
|
297
|
+
const authToken = await this.retrieveRequiredAuthToken();
|
|
298
|
+
if (wsConnectionVersion !== this.wsConnectionVersion || !this.ws?.connected) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
this.ws.subscribe(`account_all/${this.accountIdentifier}`, authToken);
|
|
302
|
+
this.ws.subscribe(`user_stats/${this.accountIdentifier}`, authToken);
|
|
303
|
+
}
|
|
304
|
+
/** Drops a pending subscription retry, so nothing outlives the connection that scheduled it. */
|
|
305
|
+
clearSubscribeRetry() {
|
|
306
|
+
if (this.subscribeRetryTimer) {
|
|
307
|
+
clearTimeout(this.subscribeRetryTimer);
|
|
308
|
+
this.subscribeRetryTimer = null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
225
311
|
routeRaw(raw) {
|
|
226
312
|
try {
|
|
227
313
|
const msg = JSON.parse(raw);
|
|
@@ -339,10 +425,40 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
339
425
|
}
|
|
340
426
|
return settings;
|
|
341
427
|
}
|
|
428
|
+
/** Reads the account with credentials when present, or explicitly uses the read-only verifier path. */
|
|
342
429
|
async fetchPrimaryAccount() {
|
|
343
|
-
const
|
|
430
|
+
const authToken = await this.retrieveAuthToken();
|
|
431
|
+
const accountIndex = Number(this.accountIdentifier);
|
|
432
|
+
const data = authToken ? await fetchAccount(accountIndex, authToken, this.demo) : await fetchPublicAccount(accountIndex, this.demo);
|
|
344
433
|
return data.accounts[0];
|
|
345
434
|
}
|
|
435
|
+
/** Reads market context under the same account identity as the snapshot that consumes it. */
|
|
436
|
+
async fetchMarkets() {
|
|
437
|
+
const authToken = await this.retrieveAuthToken();
|
|
438
|
+
return authToken ? fetchOrderBookDetails(authToken, this.demo) : fetchPublicOrderBookDetails(this.demo);
|
|
439
|
+
}
|
|
440
|
+
/** Returns this account's cached auth token, or no token for the explicit empty-key verifier. */
|
|
441
|
+
async retrieveAuthToken() {
|
|
442
|
+
if (!this.apiPrivateKey) {
|
|
443
|
+
return void 0;
|
|
444
|
+
}
|
|
445
|
+
if (!this.sdk) {
|
|
446
|
+
throw new Error("Lighter authenticated account reads require a Pear SDK client");
|
|
447
|
+
}
|
|
448
|
+
return retrieveLighterAuthToken(this.sdk, {
|
|
449
|
+
accountIndex: Number(this.accountIdentifier),
|
|
450
|
+
apiKeyIndex: this.apiKeyIndex,
|
|
451
|
+
apiPrivateKey: this.apiPrivateKey
|
|
452
|
+
}, this.demo);
|
|
453
|
+
}
|
|
454
|
+
/** Requires credentials for private WebSocket channels even when REST is in verifier mode. */
|
|
455
|
+
async retrieveRequiredAuthToken() {
|
|
456
|
+
const authToken = await this.retrieveAuthToken();
|
|
457
|
+
if (!authToken) {
|
|
458
|
+
throw new Error("Lighter WebSocket account channels require API credentials");
|
|
459
|
+
}
|
|
460
|
+
return authToken;
|
|
461
|
+
}
|
|
346
462
|
pollPositions() {
|
|
347
463
|
if (this.positionPollInFlight) return this.positionPollInFlight;
|
|
348
464
|
const lifecycleVersion = this.lifecycleVersion;
|
|
@@ -384,6 +500,42 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
384
500
|
}
|
|
385
501
|
}
|
|
386
502
|
}
|
|
503
|
+
/**
|
|
504
|
+
* Reads the tier Lighter currently has the account on, and publishes it when it moved.
|
|
505
|
+
*
|
|
506
|
+
* `/accountLimits` is authenticated, so a tracker with no SDK to mint a token through — and demo,
|
|
507
|
+
* where no such account exists — reports no tier rather than a wrong one. A failed read does the
|
|
508
|
+
* same and leaves the last known tier standing: the endpoint failing is not the account losing
|
|
509
|
+
* its tier, and blanking it would announce a change nobody made.
|
|
510
|
+
*/
|
|
511
|
+
async performAccountTierRead(lifecycleVersion) {
|
|
512
|
+
if (!this.sdk || this.demo) {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
try {
|
|
516
|
+
const credentials = this.credentials;
|
|
517
|
+
const accountIndex = Number(this.accountIdentifier);
|
|
518
|
+
const authToken = await retrieveLighterAuthToken(this.sdk, {
|
|
519
|
+
accountIndex,
|
|
520
|
+
apiKeyIndex: credentials.api_key_index,
|
|
521
|
+
apiPrivateKey: credentials.api_private_key
|
|
522
|
+
}, this.demo);
|
|
523
|
+
const limits = await fetchAccountLimits(accountIndex, authToken, this.demo);
|
|
524
|
+
const accountTier = toLighterAccountTier(limits.user_tier);
|
|
525
|
+
if (lifecycleVersion !== this.lifecycleVersion) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (accountTier === null || accountTier === this.accountTier) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
this.accountTier = accountTier;
|
|
532
|
+
for (const listener of this.accountTierListeners) {
|
|
533
|
+
listener(accountTier);
|
|
534
|
+
}
|
|
535
|
+
} catch (err) {
|
|
536
|
+
this.onError(err instanceof Error ? err : new Error(String(err)));
|
|
537
|
+
}
|
|
538
|
+
}
|
|
387
539
|
/** Shares one in-flight market read, so a slow response cannot stack requests behind the WAF. */
|
|
388
540
|
refreshMarkets() {
|
|
389
541
|
if (this.marketRefreshInFlight) return this.marketRefreshInFlight;
|
|
@@ -422,7 +574,7 @@ class LighterAccountTracker extends BaseAccountTracker {
|
|
|
422
574
|
*/
|
|
423
575
|
async performMarketRefresh(lifecycleVersion) {
|
|
424
576
|
try {
|
|
425
|
-
const markets = await
|
|
577
|
+
const markets = await this.fetchMarkets();
|
|
426
578
|
if (lifecycleVersion !== this.lifecycleVersion) return;
|
|
427
579
|
for (const market of markets.perp) {
|
|
428
580
|
this.marketsByMarketId.set(market.market_id, market);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pear-protocol/exchanges-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Pear Protocol Exchanges SDK",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -22,12 +22,14 @@
|
|
|
22
22
|
"scripts": {
|
|
23
23
|
"build": "tsup",
|
|
24
24
|
"clean": "rm -rf dist .turbo",
|
|
25
|
-
"typecheck": "tsc --noEmit"
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"test:watch": "vitest"
|
|
26
28
|
},
|
|
27
29
|
"dependencies": {
|
|
28
|
-
"@pear-protocol/core-sdk": "^1.
|
|
29
|
-
"@pear-protocol/types": "^1.
|
|
30
|
-
"@pear-protocol/utils": "^0.5.
|
|
30
|
+
"@pear-protocol/core-sdk": "^1.19.0",
|
|
31
|
+
"@pear-protocol/types": "^1.28.0",
|
|
32
|
+
"@pear-protocol/utils": "^0.5.3",
|
|
31
33
|
"bignumber.js": "^11.1.1",
|
|
32
34
|
"partysocket": "^1.0.3",
|
|
33
35
|
"viem": "^2.48.8",
|
|
@@ -36,6 +38,7 @@
|
|
|
36
38
|
"devDependencies": {
|
|
37
39
|
"@backend/typescript-config": "workspace:*",
|
|
38
40
|
"tsup": "8.5.1",
|
|
39
|
-
"typescript": "5.9.3"
|
|
41
|
+
"typescript": "5.9.3",
|
|
42
|
+
"vitest": "4.1.2"
|
|
40
43
|
}
|
|
41
44
|
}
|