@pear-protocol/exchanges-sdk 0.6.7 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +105 -25
  2. package/dist/client/bybit/rest.js +8 -1
  3. package/dist/client/hyperliquid/rest.js +21 -1
  4. package/dist/client/lighter/rest.js +6 -12
  5. package/dist/client/okx/rest.js +5 -1
  6. package/dist/index.d.ts +1131 -78
  7. package/dist/sdk.js +9 -2
  8. package/dist/syncer/base.js +36 -14
  9. package/dist/tracker/balance-core.js +89 -0
  10. package/dist/tracker/balance.js +25 -0
  11. package/dist/tracker/basket-size.js +194 -0
  12. package/dist/tracker/binance/balance.js +11 -0
  13. package/dist/tracker/binance/mapper.js +87 -42
  14. package/dist/tracker/binance/tracker.js +29 -14
  15. package/dist/tracker/binance/types.js +0 -6
  16. package/dist/tracker/bybit/balance.js +14 -0
  17. package/dist/tracker/bybit/mapper.js +109 -52
  18. package/dist/tracker/bybit/tracker.js +242 -33
  19. package/dist/tracker/bybit/types.js +0 -7
  20. package/dist/tracker/hyperliquid/account-mode.js +14 -0
  21. package/dist/tracker/hyperliquid/balance.js +184 -0
  22. package/dist/tracker/hyperliquid/basket-size.js +93 -0
  23. package/dist/tracker/hyperliquid/coin-name.js +18 -0
  24. package/dist/tracker/hyperliquid/collateral.js +48 -0
  25. package/dist/tracker/hyperliquid/decimal.js +28 -0
  26. package/dist/tracker/hyperliquid/mapper.js +399 -176
  27. package/dist/tracker/hyperliquid/spot-pricing.js +106 -0
  28. package/dist/tracker/hyperliquid/tracker.js +237 -78
  29. package/dist/tracker/hyperliquid/types.js +1 -7
  30. package/dist/tracker/index.js +9 -7
  31. package/dist/tracker/lighter/balance.js +87 -0
  32. package/dist/tracker/lighter/basket-size.js +74 -0
  33. package/dist/tracker/lighter/mapper.js +324 -145
  34. package/dist/tracker/lighter/tracker.js +228 -117
  35. package/dist/tracker/lighter/types.js +1 -7
  36. package/dist/tracker/okx/balance.js +17 -0
  37. package/dist/tracker/okx/mapper.js +156 -58
  38. package/dist/tracker/okx/tracker.js +228 -29
  39. package/dist/tracker/okx/types.js +0 -8
  40. package/dist/tracker/types.js +0 -24
  41. package/package.json +7 -9
@@ -1,9 +1,34 @@
1
1
  import BigNumber from 'bignumber.js';
2
2
  import { positionKey } from '../utils';
3
- import { OkxAccountType } from './types';
4
3
 
5
4
  var __defProp = Object.defineProperty;
6
5
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
6
+ const DECIMAL_PATTERN = /^\d+(\.\d+)?$/;
7
+ const SIGNED_DECIMAL_PATTERN = /^-?\d+(\.\d+)?$/;
8
+ function acctLvToAccountMode(acctLv) {
9
+ switch (acctLv) {
10
+ case "1":
11
+ return "okx:spot";
12
+ case "2":
13
+ return "okx:futures";
14
+ case "3":
15
+ return "okx:multiCurrencyMargin";
16
+ case "4":
17
+ return "okx:portfolioMargin";
18
+ }
19
+ return null;
20
+ }
21
+ __name(acctLvToAccountMode, "acctLvToAccountMode");
22
+ function toWithdrawable(rows, details) {
23
+ return {
24
+ total: withdrawableFromMaxWithdrawals(rows, details),
25
+ byCurrency: new Map(rows.map((row) => [
26
+ row.ccy,
27
+ row.maxWd
28
+ ]))
29
+ };
30
+ }
31
+ __name(toWithdrawable, "toWithdrawable");
7
32
  function mergeRawCoins(existing, incoming) {
8
33
  const merged = new Map(existing);
9
34
  for (const d of incoming) {
@@ -12,50 +37,33 @@ function mergeRawCoins(existing, incoming) {
12
37
  return merged;
13
38
  }
14
39
  __name(mergeRawCoins, "mergeRawCoins");
15
- function buildBalance(raw, coins, accountType) {
16
- if (!accountType) return null;
17
- const isSpot = accountType === OkxAccountType.SPOT;
18
- const free = {};
19
- const used = {};
20
- const total = {};
21
- const assets = [];
22
- let walletBalanceSum = new BigNumber(0);
23
- for (const [, d] of coins) {
24
- const eq = d.eq ?? "0";
25
- const eqBN = new BigNumber(eq);
26
- const cashBN = new BigNumber(d.cashBal ?? "0");
27
- if (eqBN.isZero() && cashBN.isZero()) continue;
28
- const coinFree = isSpot ? d.availBal ?? "0" : d.availEq || d.availBal || "0";
29
- const coinUsed = eqBN.minus(coinFree).toString();
30
- free[d.ccy] = coinFree;
31
- used[d.ccy] = coinUsed;
32
- total[d.ccy] = eq;
33
- walletBalanceSum = walletBalanceSum.plus(cashBN);
34
- assets.push({
35
- asset: d.ccy,
36
- free: coinFree,
37
- used: coinUsed,
38
- total: eq,
39
- walletBalance: d.cashBal ?? "0",
40
- usdValue: d.eqUsd ?? "0"
41
- });
40
+ function buildBalance(raw, accountMode, withdrawable, timestamp) {
41
+ if (accountMode === null) {
42
+ return null;
43
+ }
44
+ const perCollateral = toCollateralRows(raw.details ?? [], withdrawable.byCurrency);
45
+ switch (accountMode) {
46
+ case "okx:spot":
47
+ return {
48
+ accountMode,
49
+ ...splitPoolCore(raw, withdrawable, perCollateral, timestamp)
50
+ };
51
+ case "okx:futures":
52
+ return {
53
+ accountMode,
54
+ ...splitPoolCore(raw, withdrawable, perCollateral, timestamp)
55
+ };
56
+ case "okx:multiCurrencyMargin":
57
+ return {
58
+ accountMode,
59
+ ...sharedPoolCore(raw, withdrawable, perCollateral, timestamp)
60
+ };
61
+ case "okx:portfolioMargin":
62
+ return {
63
+ accountMode,
64
+ ...sharedPoolCore(raw, withdrawable, perCollateral, timestamp)
65
+ };
42
66
  }
43
- const availableToTrade = raw.availEq || raw.adjEq || "0";
44
- return {
45
- accountType,
46
- free,
47
- used,
48
- total,
49
- availableToTrade,
50
- totalEquity: raw.totalEq ?? "0",
51
- walletBalance: walletBalanceSum.toFixed(),
52
- unrealizedPnl: raw.upl || "0",
53
- initialMarginUsed: raw.imr || "0",
54
- maintenanceMargin: raw.mmr || "0",
55
- marginRatio: raw.mgnRatio || "0.0000",
56
- assets,
57
- timestamp: Date.now()
58
- };
59
67
  }
60
68
  __name(buildBalance, "buildBalance");
61
69
  function mapSinglePosition(p) {
@@ -106,20 +114,110 @@ function mapPositionUpdates(raw) {
106
114
  return positions;
107
115
  }
108
116
  __name(mapPositionUpdates, "mapPositionUpdates");
109
- function acctLvToAccountType(acctLv) {
110
- switch (acctLv) {
111
- case "1":
112
- return OkxAccountType.SPOT;
113
- case "2":
114
- return OkxAccountType.SINGLE_CURRENCY_MARGIN;
115
- case "3":
116
- return OkxAccountType.MULTI_CURRENCY_MARGIN;
117
- case "4":
118
- return OkxAccountType.PORTFOLIO_MARGIN;
119
- default:
120
- return null;
117
+ function sharedPoolCore(raw, withdrawable, perCollateral, timestamp) {
118
+ return {
119
+ totalEquity: decimalOrNull(raw.totalEq),
120
+ withdrawableBalance: withdrawable.total,
121
+ availableMargin: availableMarginOf(raw),
122
+ poolsShared: true,
123
+ perCollateral,
124
+ timestamp
125
+ };
126
+ }
127
+ __name(sharedPoolCore, "sharedPoolCore");
128
+ function splitPoolCore(raw, withdrawable, perCollateral, timestamp) {
129
+ const scalarsDescribeTheAccount = atMostOneFundedPool(raw.details ?? []);
130
+ return {
131
+ totalEquity: scalarsDescribeTheAccount ? decimalOrNull(raw.totalEq) : null,
132
+ withdrawableBalance: scalarsDescribeTheAccount ? withdrawable.total : null,
133
+ availableMargin: scalarsDescribeTheAccount ? availableMarginOf(raw) : null,
134
+ poolsShared: false,
135
+ perCollateral,
136
+ timestamp
137
+ };
138
+ }
139
+ __name(splitPoolCore, "splitPoolCore");
140
+ function toCollateralRows(details, withdrawableByCurrency) {
141
+ const rows = [];
142
+ for (const detail of details) {
143
+ if (!isFunded(detail.eq)) {
144
+ continue;
145
+ }
146
+ rows.push({
147
+ asset: detail.ccy,
148
+ equity: detail.eq,
149
+ availableMargin: decimalOrNull(detail.availEq),
150
+ withdrawable: decimalOrNull(withdrawableByCurrency.get(detail.ccy))
151
+ });
152
+ }
153
+ return rows.sort(byCurrencyName);
154
+ }
155
+ __name(toCollateralRows, "toCollateralRows");
156
+ function atMostOneFundedPool(details) {
157
+ let fundedPools = 0;
158
+ for (const detail of details) {
159
+ if (holdsAnyBalance(detail.eq)) {
160
+ fundedPools += 1;
161
+ }
162
+ }
163
+ return fundedPools <= 1;
164
+ }
165
+ __name(atMostOneFundedPool, "atMostOneFundedPool");
166
+ function holdsAnyBalance(value) {
167
+ if (value === void 0 || !SIGNED_DECIMAL_PATTERN.test(value)) {
168
+ return false;
169
+ }
170
+ return !new BigNumber(value).isZero();
171
+ }
172
+ __name(holdsAnyBalance, "holdsAnyBalance");
173
+ function isFunded(value) {
174
+ return isDecimal(value) && !new BigNumber(value).isZero();
175
+ }
176
+ __name(isFunded, "isFunded");
177
+ function byCurrencyName(a, b) {
178
+ if (a.asset === b.asset) {
179
+ return 0;
121
180
  }
181
+ return a.asset < b.asset ? -1 : 1;
182
+ }
183
+ __name(byCurrencyName, "byCurrencyName");
184
+ function decimalOrNull(value) {
185
+ return isDecimal(value) ? value : null;
186
+ }
187
+ __name(decimalOrNull, "decimalOrNull");
188
+ function withdrawableFromMaxWithdrawals(rows, details) {
189
+ if (rows.length === 0) {
190
+ return null;
191
+ }
192
+ const withdrawable = rows.filter((row) => isFunded(row.maxWd));
193
+ if (withdrawable.length === 0) {
194
+ return "0";
195
+ }
196
+ const single = withdrawable.length === 1 ? withdrawable[0] : void 0;
197
+ if (single === void 0) {
198
+ return null;
199
+ }
200
+ return toUsd(single.maxWd, details.get(single.ccy));
201
+ }
202
+ __name(withdrawableFromMaxWithdrawals, "withdrawableFromMaxWithdrawals");
203
+ function availableMarginOf(raw) {
204
+ return decimalOrNull(raw.availEq);
205
+ }
206
+ __name(availableMarginOf, "availableMarginOf");
207
+ function toUsd(amount, detail) {
208
+ if (detail === void 0 || !isDecimal(detail.eq) || !isDecimal(detail.eqUsd)) {
209
+ return null;
210
+ }
211
+ const equity = new BigNumber(detail.eq);
212
+ if (equity.isZero()) {
213
+ return null;
214
+ }
215
+ return new BigNumber(amount).times(detail.eqUsd).div(equity).toFixed();
216
+ }
217
+ __name(toUsd, "toUsd");
218
+ function isDecimal(value) {
219
+ return value !== void 0 && DECIMAL_PATTERN.test(value);
122
220
  }
123
- __name(acctLvToAccountType, "acctLvToAccountType");
221
+ __name(isDecimal, "isDecimal");
124
222
 
125
- export { acctLvToAccountType, buildBalance, mapPositionUpdates, mapPositions, mapSinglePosition, mergeRawCoins };
223
+ export { acctLvToAccountMode, buildBalance, mapPositionUpdates, mapPositions, mapSinglePosition, mergeRawCoins, toWithdrawable };
@@ -1,10 +1,11 @@
1
1
  import { createExchangeWs } from '../../client';
2
- import { fetchLeverageInfos, fetchAccountConfig, fetchBalance, fetchPositions } from '../../client/okx/rest';
2
+ import { fetchLeverageInfos, fetchAccountConfig, fetchBalance, fetchPositions, fetchMaxWithdrawals } from '../../client/okx/rest';
3
3
  import { BaseAccountTracker } from '../base';
4
- import { acctLvToAccountType, mergeRawCoins, mapPositions, buildBalance, mapPositionUpdates } from './mapper';
4
+ import { mapPositions, buildBalance, acctLvToAccountMode, mergeRawCoins, mapPositionUpdates, toWithdrawable } from './mapper';
5
5
 
6
6
  var __defProp = Object.defineProperty;
7
7
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
8
+ const MIN_WITHDRAWABLE_REFRESH_MS = 2e3;
8
9
  class OkxAccountTracker extends BaseAccountTracker {
9
10
  static {
10
11
  __name(this, "OkxAccountTracker");
@@ -15,22 +16,40 @@ class OkxAccountTracker extends BaseAccountTracker {
15
16
  openListenerId = null;
16
17
  rawCoins = /* @__PURE__ */ new Map();
17
18
  lastBalance = null;
18
- accountType = null;
19
+ accountMode = null;
20
+ withdrawable = emptyWithdrawable();
21
+ lastWithdrawableReadAt = 0;
22
+ withdrawableRefreshTimer = null;
23
+ withdrawableReadInFlight = null;
24
+ withdrawableReadPending = false;
25
+ lifecycleVersion = 0;
19
26
  trackedAssetInfos = /* @__PURE__ */ new Map();
20
27
  constructor(config) {
21
28
  super(config);
22
29
  this.creds = config.credentials;
23
30
  }
24
31
  async snapshot() {
25
- const { config, balance, positions } = await this.fetchSnapshotData();
26
- const accountType = acctLvToAccountType(config.acctLv);
27
- const rawCoins = mergeRawCoins(/* @__PURE__ */ new Map(), balance.details ?? []);
32
+ const { config, balance, positions, withdrawable } = await this.fetchSnapshotData();
28
33
  return {
29
- balance: buildBalance(balance, rawCoins, accountType),
34
+ balance: buildBalance(balance, acctLvToAccountMode(config.acctLv), withdrawable, Date.now()),
30
35
  positions: Array.from(mapPositions(positions).values())
31
36
  };
32
37
  }
33
38
  /**
39
+ * OKX sizes no basket in v1: the SDK reads no per-asset max leverage from it, and sizing off
40
+ * `availableMargin` alone would ship a number no venue field backs. The missing input is
41
+ * `GET /api/v5/public/position-tiers` → `maxLever`/`imr` per tier, which the client does not
42
+ * call — and it is NOT `GET /api/v5/account/position-tiers`, a different endpoint carrying no
43
+ * margin or leverage data at all.
44
+ */
45
+ async maxBasketSize(_input) {
46
+ return unsizedBasket();
47
+ }
48
+ /** The REST path answers the same way — there is no size either way, not a slower one. */
49
+ async snapshotMaxBasketSize(_input) {
50
+ return unsizedBasket();
51
+ }
52
+ /**
34
53
  * Stateless one-shot per-asset leverage read via `leverage-info`, which accepts up to 20 instIds
35
54
  * per request (chunked in the client), so N assets cost ceil(N/20) calls. Cross margin only,
36
55
  * matching the account-level assumption used everywhere else in this connector.
@@ -53,8 +72,10 @@ class OkxAccountTracker extends BaseAccountTracker {
53
72
  return result;
54
73
  }
55
74
  async start() {
56
- const { config, balance, positions } = await this.fetchSnapshotData();
57
- this.accountType = acctLvToAccountType(config.acctLv);
75
+ const { config, balance, positions, withdrawable } = await this.fetchSnapshotData();
76
+ this.accountMode = acctLvToAccountMode(config.acctLv);
77
+ this.withdrawable = withdrawable;
78
+ this.lastWithdrawableReadAt = Date.now();
58
79
  this.handleInitialData(balance, positions);
59
80
  this.ws = await createExchangeWs("okx", {
60
81
  demo: this.demo,
@@ -67,6 +88,12 @@ class OkxAccountTracker extends BaseAccountTracker {
67
88
  this.subscribe();
68
89
  }
69
90
  async cleanupConnections() {
91
+ this.lifecycleVersion += 1;
92
+ this.withdrawableReadPending = false;
93
+ if (this.withdrawableRefreshTimer) {
94
+ clearTimeout(this.withdrawableRefreshTimer);
95
+ this.withdrawableRefreshTimer = null;
96
+ }
70
97
  if (this.messageListenerId) {
71
98
  this.ws?.main.removeMessageListener(this.messageListenerId);
72
99
  this.messageListenerId = null;
@@ -82,17 +109,41 @@ class OkxAccountTracker extends BaseAccountTracker {
82
109
  return this.ws?.isConnected ?? false;
83
110
  }
84
111
  async fetchSnapshotData() {
85
- const [config, balance, positions] = await Promise.all([
112
+ const [config, balance, positions, read] = await Promise.all([
86
113
  fetchAccountConfig(this.creds, this.demo),
87
114
  fetchBalance(this.creds, this.demo),
88
- fetchPositions(this.creds, this.demo)
115
+ fetchPositions(this.creds, this.demo),
116
+ this.readMaxWithdrawals()
89
117
  ]);
118
+ const withdrawable = toWithdrawableOrNothing(read, mergeRawCoins(/* @__PURE__ */ new Map(), balance.details ?? []));
90
119
  return {
91
120
  config,
92
121
  balance,
93
- positions
122
+ positions,
123
+ withdrawable
94
124
  };
95
125
  }
126
+ /**
127
+ * Read how much can leave the trading account, per currency, across everything it owns.
128
+ *
129
+ * Best-effort on purpose: this is a fourth signed call that a read-only key may be refused, and
130
+ * a refusal must cost the caller a `null` field rather than the whole balance.
131
+ *
132
+ * A refusal is reported as `failed` rather than as an empty row set, because only the caller
133
+ * knows what to do with it: the snapshot path has no earlier figure and reports nothing known,
134
+ * while the live path keeps the figure its last successful read published.
135
+ */
136
+ async readMaxWithdrawals() {
137
+ return fetchMaxWithdrawals(this.creds, this.demo).then((rows) => ({
138
+ kind: "rows",
139
+ rows
140
+ })).catch((err) => {
141
+ this.onError(err instanceof Error ? err : new Error(String(err)));
142
+ return {
143
+ kind: "failed"
144
+ };
145
+ });
146
+ }
96
147
  ensureAssetTracked(instId) {
97
148
  if (this.trackedAssetInfos.has(instId)) return;
98
149
  fetchLeverageInfos(this.creds, [
@@ -141,10 +192,7 @@ class OkxAccountTracker extends BaseAccountTracker {
141
192
  handleInitialData(balance, positions) {
142
193
  this.lastBalance = balance;
143
194
  this.rawCoins = mergeRawCoins(/* @__PURE__ */ new Map(), balance.details ?? []);
144
- const built = buildBalance(balance, this.rawCoins, this.accountType);
145
- if (built) this.onStateUpdate({
146
- balance: built
147
- });
195
+ this.emitBalance(balance);
148
196
  this.onStateUpdate({
149
197
  positions: mapPositions(positions)
150
198
  });
@@ -196,24 +244,175 @@ class OkxAccountTracker extends BaseAccountTracker {
196
244
  });
197
245
  }
198
246
  }
247
+ /**
248
+ * Fold one account push into the whole account, then publish what that whole account says.
249
+ *
250
+ * OKX pushes only what moved: an update names the currencies whose figures changed and leaves
251
+ * every other currency out. So the rows are taken from the per-currency merge rather than from
252
+ * the push, and everything read off them — the pool count above all — sees every currency the
253
+ * account holds. A push naming one of two funded currencies would otherwise read as a
254
+ * single-pool account and publish scalars that describe neither pool.
255
+ *
256
+ * The account-level figures inherit on `??`, never on `||`, because ABSENT and BLANK say
257
+ * opposite things here. A field this push left out was not restated and its previous value still
258
+ * stands; a field present as `''` is OKX withdrawing the figure, which it does per account mode —
259
+ * `availableMarginOf` records the mode that blanks `availEq`. Inheriting a blank would republish
260
+ * a number the venue no longer states, and it would do it BEHIND `decimalOrNull`, whose whole job
261
+ * is to publish a blank as `null`: overwriting the blank here means that guard never sees it.
262
+ */
199
263
  handleBalanceUpdate(raw) {
200
264
  this.rawCoins = mergeRawCoins(this.rawCoins, raw.details ?? []);
201
- const merged = this.lastBalance ? {
202
- totalEq: raw.totalEq || this.lastBalance.totalEq,
203
- adjEq: raw.adjEq || this.lastBalance.adjEq,
204
- availEq: raw.availEq || this.lastBalance.availEq,
205
- mmr: raw.mmr || this.lastBalance.mmr,
206
- imr: raw.imr || this.lastBalance.imr,
207
- upl: raw.upl || this.lastBalance.upl,
208
- mgnRatio: raw.mgnRatio || this.lastBalance.mgnRatio,
209
- details: raw.details ?? this.lastBalance.details
210
- } : raw;
265
+ const previous = this.lastBalance ?? nothingStated();
266
+ const merged = {
267
+ totalEq: raw.totalEq ?? previous.totalEq,
268
+ adjEq: raw.adjEq ?? previous.adjEq,
269
+ availEq: raw.availEq ?? previous.availEq,
270
+ mmr: raw.mmr ?? previous.mmr,
271
+ imr: raw.imr ?? previous.imr,
272
+ upl: raw.upl ?? previous.upl,
273
+ mgnRatio: raw.mgnRatio ?? previous.mgnRatio,
274
+ // The account's whole row set, which is what the pool count has to be counted over.
275
+ details: Array.from(this.rawCoins.values())
276
+ };
211
277
  this.lastBalance = merged;
212
- const built = buildBalance(merged, this.rawCoins, this.accountType);
213
- if (built) this.onStateUpdate({
214
- balance: built
278
+ this.emitBalance(merged);
279
+ this.refreshWithdrawableSoon();
280
+ }
281
+ /** Publish the balance built from the newest account payload and the cached transferable amounts. */
282
+ emitBalance(balance) {
283
+ const built = buildBalance(balance, this.accountMode, this.withdrawable, Date.now());
284
+ if (built) {
285
+ this.onStateUpdate({
286
+ balance: built
287
+ });
288
+ }
289
+ }
290
+ /**
291
+ * Queue the next transferable-amount read, no sooner than the venue's own rate limit allows.
292
+ *
293
+ * Nothing waits on it: the account update is already published from the cached figure. One
294
+ * queued read covers every account tick that lands before it runs, which is what stops a burst
295
+ * of ticks becoming a burst of signed calls — and because a tick always leaves a read queued or
296
+ * marked pending, a moved figure is still picked up within one interval rather than dropped.
297
+ */
298
+ refreshWithdrawableSoon() {
299
+ if (this.withdrawableReadInFlight !== null) {
300
+ this.withdrawableReadPending = true;
301
+ return;
302
+ }
303
+ if (this.withdrawableRefreshTimer !== null) {
304
+ return;
305
+ }
306
+ const waitMs = Math.max(0, MIN_WITHDRAWABLE_REFRESH_MS - (Date.now() - this.lastWithdrawableReadAt));
307
+ this.withdrawableRefreshTimer = setTimeout(() => {
308
+ this.withdrawableRefreshTimer = null;
309
+ this.lastWithdrawableReadAt = Date.now();
310
+ void this.refreshWithdrawableBalance();
311
+ }, waitMs);
312
+ }
313
+ /**
314
+ * Hold one transferable-amount read open at a time, and queue the ticks that arrive during it.
315
+ *
316
+ * Serialising the reads is what keeps the published figure the newest one: two overlapping reads
317
+ * can come back in either order, and the older answer landing last would publish an amount the
318
+ * venue has already moved past.
319
+ */
320
+ refreshWithdrawableBalance() {
321
+ const lifecycleVersion = this.lifecycleVersion;
322
+ const read = this.performWithdrawableRefresh(lifecycleVersion).finally(() => {
323
+ if (this.withdrawableReadInFlight === read) {
324
+ this.withdrawableReadInFlight = null;
325
+ }
326
+ if (this.withdrawableReadPending && lifecycleVersion === this.lifecycleVersion) {
327
+ this.withdrawableReadPending = false;
328
+ this.refreshWithdrawableSoon();
329
+ }
215
330
  });
331
+ this.withdrawableReadInFlight = read;
332
+ return read;
216
333
  }
334
+ /**
335
+ * Re-read the transferable amount behind the live path, then re-publish only if it moved.
336
+ *
337
+ * Fire-and-forget: the account update is already published with the previous figure, so a slow
338
+ * read delays the correction instead of blocking the balance. A read that FAILS changes nothing
339
+ * at all — the last good figure stays published, because a refusal says OKX did not answer, not
340
+ * that the account has nothing to transfer. The re-publish reads the newest account payload
341
+ * rather than the one this read started from, so an account update that landed meanwhile is not
342
+ * rolled back.
343
+ */
344
+ async performWithdrawableRefresh(lifecycleVersion) {
345
+ try {
346
+ const read = await this.readMaxWithdrawals();
347
+ if (lifecycleVersion !== this.lifecycleVersion) {
348
+ return;
349
+ }
350
+ if (read.kind === "failed") {
351
+ return;
352
+ }
353
+ const withdrawable = toWithdrawable(read.rows, this.rawCoins);
354
+ if (sameWithdrawable(withdrawable, this.withdrawable)) {
355
+ return;
356
+ }
357
+ this.withdrawable = withdrawable;
358
+ if (this.lastBalance) {
359
+ this.emitBalance(this.lastBalance);
360
+ }
361
+ } catch (err) {
362
+ if (lifecycleVersion === this.lifecycleVersion) {
363
+ this.onError(err instanceof Error ? err : new Error(String(err)));
364
+ }
365
+ }
366
+ }
367
+ }
368
+ function toWithdrawableOrNothing(read, details) {
369
+ switch (read.kind) {
370
+ case "rows":
371
+ return toWithdrawable(read.rows, details);
372
+ case "failed":
373
+ return emptyWithdrawable();
374
+ }
375
+ }
376
+ __name(toWithdrawableOrNothing, "toWithdrawableOrNothing");
377
+ function nothingStated() {
378
+ return {
379
+ totalEq: "",
380
+ adjEq: "",
381
+ availEq: "",
382
+ mmr: "",
383
+ imr: "",
384
+ upl: "",
385
+ mgnRatio: "",
386
+ details: []
387
+ };
388
+ }
389
+ __name(nothingStated, "nothingStated");
390
+ function emptyWithdrawable() {
391
+ return {
392
+ total: null,
393
+ byCurrency: /* @__PURE__ */ new Map()
394
+ };
395
+ }
396
+ __name(emptyWithdrawable, "emptyWithdrawable");
397
+ function sameWithdrawable(left, right) {
398
+ if (left.total !== right.total || left.byCurrency.size !== right.byCurrency.size) {
399
+ return false;
400
+ }
401
+ for (const [currency, amount] of left.byCurrency) {
402
+ if (right.byCurrency.get(currency) !== amount) {
403
+ return false;
404
+ }
405
+ }
406
+ return true;
407
+ }
408
+ __name(sameWithdrawable, "sameWithdrawable");
409
+ function unsizedBasket() {
410
+ return {
411
+ maxNotional: null,
412
+ legs: [],
413
+ bindingConstraint: null
414
+ };
217
415
  }
416
+ __name(unsizedBasket, "unsizedBasket");
218
417
 
219
418
  export { OkxAccountTracker };
@@ -1,9 +1 @@
1
- var OkxAccountType = /* @__PURE__ */ (function(OkxAccountType2) {
2
- OkxAccountType2["SPOT"] = "OKX_SPOT";
3
- OkxAccountType2["SINGLE_CURRENCY_MARGIN"] = "OKX_SINGLE";
4
- OkxAccountType2["MULTI_CURRENCY_MARGIN"] = "OKX_MULTI";
5
- OkxAccountType2["PORTFOLIO_MARGIN"] = "OKX_PORTFOLIO";
6
- return OkxAccountType2;
7
- })({});
8
1
 
9
- export { OkxAccountType };
@@ -1,25 +1 @@
1
- import { BinanceAccountType } from './binance/types';
2
- import { BybitAccountType } from './bybit/types';
3
- import { HyperliquidAccountType } from './hyperliquid/types';
4
- import { LighterAccountType } from './lighter/types';
5
- import { OkxAccountType } from './okx/types';
6
1
 
7
- const ACCOUNT_TYPE_LABELS = {
8
- [BinanceAccountType.SINGLE_ASSET]: "Single-Asset Mode",
9
- [BinanceAccountType.MULTI_ASSET]: "Multi-Asset Mode",
10
- [BybitAccountType.ISOLATED_MARGIN]: "Isolated Margin",
11
- [BybitAccountType.REGULAR_MARGIN]: "Cross Margin",
12
- [BybitAccountType.PORTFOLIO_MARGIN]: "Portfolio Margin",
13
- [HyperliquidAccountType.DEFAULT]: "Standard",
14
- [HyperliquidAccountType.DISABLED]: "Standard",
15
- [HyperliquidAccountType.PORTFOLIO_MARGIN]: "Portfolio Margin",
16
- [HyperliquidAccountType.UNIFIED_ACCOUNT]: "Unified Account",
17
- [LighterAccountType.CLASSIC]: "Classic",
18
- [LighterAccountType.UNIFIED]: "Unified",
19
- [OkxAccountType.SPOT]: "Spot Mode",
20
- [OkxAccountType.SINGLE_CURRENCY_MARGIN]: "Single-Currency Margin",
21
- [OkxAccountType.MULTI_CURRENCY_MARGIN]: "Multi-Currency Margin",
22
- [OkxAccountType.PORTFOLIO_MARGIN]: "Portfolio Margin"
23
- };
24
-
25
- export { ACCOUNT_TYPE_LABELS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pear-protocol/exchanges-sdk",
3
- "version": "0.6.7",
3
+ "version": "0.8.0",
4
4
  "description": "Pear Protocol Exchanges SDK",
5
5
  "private": false,
6
6
  "type": "module",
@@ -22,22 +22,20 @@
22
22
  "scripts": {
23
23
  "build": "tsup",
24
24
  "clean": "rm -rf dist .turbo",
25
- "typecheck": "tsc --noEmit",
26
- "test": "vitest run",
27
- "test:watch": "vitest"
25
+ "typecheck": "tsc --noEmit"
28
26
  },
29
27
  "dependencies": {
30
- "@pear-protocol/core-sdk": "^1.17.4",
31
- "@pear-protocol/types": "^1.26.1",
28
+ "@pear-protocol/core-sdk": "^1.18.0",
29
+ "@pear-protocol/types": "^1.27.0",
32
30
  "@pear-protocol/utils": "^0.5.1",
33
31
  "bignumber.js": "^11.1.1",
34
32
  "partysocket": "^1.0.3",
35
- "viem": "^2.48.8"
33
+ "viem": "^2.48.8",
34
+ "zod": "4.1.12"
36
35
  },
37
36
  "devDependencies": {
38
37
  "@backend/typescript-config": "workspace:*",
39
38
  "tsup": "8.5.1",
40
- "typescript": "5.9.3",
41
- "vitest": "4.1.2"
39
+ "typescript": "5.9.3"
42
40
  }
43
41
  }