@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
package/README.md CHANGED
@@ -49,7 +49,7 @@ await tracker.start();
49
49
 
50
50
  const offBalance = tracker.trackBalance((balance) => {
51
51
  if (!balance) return; // unsupported/unrepresentable account mode
52
- console.log(balance.totalEquity, balance.unrealizedPnl);
52
+ console.log(balance.accountMode, balance.totalEquity);
53
53
  });
54
54
 
55
55
  const offPositions = tracker.trackPosition((positions) => {
@@ -68,7 +68,7 @@ Pass `demo: true` to target each exchange's test or simulated-trading environmen
68
68
  const exchanges = new ExchangesSDK({ sdk, demo: true });
69
69
  ```
70
70
 
71
- Hyperliquid and Lighter public REST/WebSocket reads are routed to testnet. Lighter operations that require a backend-generated auth token or signature currently fail closed in demo mode because the existing supporting API signs for mainnet only; public snapshots and tracking remain available, without authenticated account-tier enrichment.
71
+ Hyperliquid and Lighter public REST/WebSocket reads are routed to testnet. Lighter operations that require a backend-generated auth token or signature currently fail closed in demo mode because the existing supporting API signs for mainnet only; public snapshots and tracking remain available.
72
72
 
73
73
  ## Connection
74
74
 
@@ -125,7 +125,7 @@ Possible warning reasons:
125
125
  | `hyperliquid_api_wallet_invalid` | Hyperliquid agent wallet is missing or invalid |
126
126
  | `hyperliquid_builder_fee_not_approved` | Hyperliquid builder fee approval is missing |
127
127
  | `hyperliquid_account_mode_check_failed` | Hyperliquid account mode could not be read |
128
- | `hyperliquid_account_mode_unsupported` | Portfolio Margin or deprecated DEX Abstraction cannot be valued safely |
128
+ | `hyperliquid_account_mode_unsupported` | Hyperliquid reports an account mode this SDK does not model |
129
129
  | `lighter_api_key_invalid` | Lighter API key cannot create an auth token |
130
130
  | `lighter_integrator_not_approved` | Lighter integrator approval is missing |
131
131
 
@@ -136,14 +136,15 @@ When you only need the current balance and positions once — a request/response
136
136
  ```typescript
137
137
  const { balance, positions } = await exchanges.snapshot(tradeAccountId);
138
138
 
139
+ balance?.accountMode;
139
140
  balance?.totalEquity;
140
- balance?.availableToTrade;
141
+ balance?.availableMargin;
141
142
  for (const p of positions) {
142
143
  console.log(p.symbol, p.side, p.leverage, p.liquidationPrice);
143
144
  }
144
145
  ```
145
146
 
146
- `snapshot()` returns the same `AccountBalance` / `AccountPosition` shapes the live tracker produces — `{ balance: AccountBalance | null; positions: AccountPosition[] }` — so either path can feed the same consumer. `balance` is `null` when the account type cannot be represented safely. Hyperliquid currently supports Standard and Unified calculations; Portfolio Margin and deprecated DEX Abstraction fail closed and are surfaced by account status.
147
+ `snapshot()` returns the same `AccountBalance` / `AccountPosition` shapes the live tracker produces — `{ balance: AccountBalance | null; positions: AccountPosition[] }` — so either path can feed the same consumer. `balance` is `null` when the account mode cannot be represented safely. Hyperliquid Standard, Unified and Portfolio Margin each have their own calculation; an abstraction string this SDK does not model fails closed and is surfaced by account status.
147
148
 
148
149
  There is nothing to tear down: no `disconnect()`, no socket. This is the read a stateless caller (such as an MCP tool) wants.
149
150
 
@@ -176,35 +177,110 @@ The read is batch-first: each connector services the whole list with as few requ
176
177
 
177
178
  > For Lighter, a custom margin setting on a market with **no open position** is not exposed by the REST account read, so it reports the market default — the same figure the live path shows before a position exists.
178
179
 
180
+ ## Basket Sizing
181
+
182
+ `maxBasketSize()` answers "how large can this basket be opened on this account". A basket is atomic — every leg executes or none do — so the answer is **one** gross notional for the whole structure, and the per-leg figures are that number spread back over the weights, never independently reachable maxima.
183
+
184
+ ```typescript
185
+ const sized = await exchanges.maxBasketSize(tradeAccountId, {
186
+ legs: [
187
+ { asset: 'BTC', side: 'BUY', weight: 60, leverage: '10', marginMode: 'cross' },
188
+ { asset: 'ETH', side: 'SELL', weight: 40, leverage: '10', marginMode: 'cross' },
189
+ ],
190
+ });
191
+
192
+ sized.maxNotional; // string | null — the whole basket's max GROSS notional in USD
193
+ sized.bindingConstraint; // which pool or leg ran out first
194
+ for (const leg of sized.legs) {
195
+ console.log(leg.asset, leg.notional, leg.marginRequired);
196
+ }
197
+ ```
198
+
199
+ `weight` is unsigned and the set sums to 100; `side` does not enter the maths, because a margin requirement does not depend on direction.
200
+
201
+ The same call is on a running tracker, over either read path:
202
+
203
+ ```typescript
204
+ await tracker.maxBasketSize(input); // live path
205
+ await tracker.snapshotMaxBasketSize(input); // REST only, no socket
206
+ ```
207
+
208
+ Sizing charges each leg's margin to the collateral pool it actually draws on, and the basket stops at whichever pool runs out first. `bindingConstraint` names it: `{ kind: 'cross-pool', pool }` when a pool's budget bound the basket, `{ kind: 'isolated-leg', asset }` when one leg reached its own venue ceiling first.
209
+
210
+ A leg's margin fraction is `1 / min(requestedLeverage, venueMaxLeverage, currentAccountLeverage)`. The account's current leverage is in that minimum on purpose: the open path never sets leverage, so a requested leverage the venue has not been told about would overstate the size. On Hyperliquid each leg is additionally capped at its asset's next margin-tier boundary — a single-leg basket then reproduces the venue's own published `maxTradeSzs` exactly.
211
+
212
+ `maxNotional` is `null` rather than zero in two cases: the venue publishes no per-asset maximum leverage the SDK reads today (Binance, Bybit and OKX all return `null`), or a leg draws on a collateral pool the SDK could not value. Sizing a basket against a pool it cannot value is exactly the guess this call exists to remove.
213
+
179
214
  ## Tracking
180
215
 
181
216
  Each `track*` method returns an unsubscribe function. Callbacks fire with the current snapshot on subscribe (if available) and on every update.
182
217
 
183
218
  ### Balance
184
219
 
220
+ `AccountBalance` is a discriminated union tagged by `accountMode` — venue and mode in one literal, so narrowing needs no separate venue check. Every variant carries the same core; the tag adds the fields that venue and mode actually publish, and no others.
221
+
185
222
  ```typescript
186
223
  tracker.trackBalance((balance) => {
187
224
  if (!balance) return; // a live mode change can explicitly invalidate the cached balance
188
- balance.totalEquity;
189
- balance.walletBalance;
190
- balance.unrealizedPnl;
191
- balance.availableToTrade;
192
- balance.initialMarginUsed;
193
- balance.maintenanceMargin;
194
- balance.marginRatio;
195
- balance.accountType;
196
- balance.accountTier; // Lighter account tier when reported
197
-
198
- for (const a of balance.assets) {
199
- a.asset; // "USDT", "USDC", ...
200
- a.free;
201
- a.used;
202
- a.total;
203
- a.usdValue;
225
+
226
+ balance.accountMode; // "hyperliquid:unified" | "binance:singleAsset" | ...
227
+ balance.totalEquity; // string | null
228
+ balance.withdrawableBalance; // string | null — what can leave the account right now
229
+ balance.availableMargin; // string | null — a floor across the tracked assets, not a per-asset figure
230
+ balance.poolsShared; // boolean
231
+ balance.timestamp;
232
+
233
+ for (const pool of balance.perCollateral) {
234
+ pool.asset; // "USDT", "USDC", "HYPE", ...
235
+ pool.equity; // in THAT asset's own units
236
+ pool.availableMargin; // string | null
237
+ pool.withdrawable; // string | null
204
238
  }
205
239
  });
206
240
  ```
207
241
 
242
+ **`null` means "this venue cannot tell us" — never zero.** Every nullable field follows that one rule: a figure the venue does not publish, or one that cannot be derived without guessing, is `null`. A zero is a zero, and the two are never interchangeable.
243
+
244
+ **`perCollateral` rows are in each asset's own units, never converted.** A row is never turned into USD and two rows are never added together — a USDC row and an ETH row share a column and nothing else. The only USD figures in a variant are the scalars.
245
+
246
+ **`poolsShared` says what those rows mean.** `true`: one pool funds every position, so the rows are holdings inside it and only the scalars are spendable. `false`: the rows are separate budgets that cannot fund each other — on Binance single-asset a USDT-settled contract draws USDT margin and a USDC-settled one draws USDC, and neither can pay for the other.
247
+
248
+ On the three venues whose pools are separate *and* whose account-level figures cover one currency only — Binance single-asset, OKX Spot and OKX Futures — the core scalars carry the venue's own figure when exactly one pool holds a non-zero balance, and `null` once two or more do. They are never summed across pools. Hyperliquid splits by mode: Standard and Unified report `poolsShared: false` with one row per collateral token, while Portfolio Margin reports `true` — it lends against every holding at that coin's LTV, so the rows are holdings in one pool. All three keep their scalars in every case: `availableMargin` is already a per-asset floor.
249
+
250
+ #### Narrowing on `accountMode`
251
+
252
+ Fields beyond the core belong to a variant. Switch on the tag to reach them, and write no `default:` clause — then a mode added in a later release fails the build here instead of silently returning `undefined`.
253
+
254
+ ```typescript
255
+ import type { AccountBalance } from '@pear-protocol/exchanges-sdk';
256
+
257
+ function unrealizedPnlOf(balance: AccountBalance): string | null {
258
+ switch (balance.accountMode) {
259
+ // Hyperliquid and Lighter carry full per-mode detail.
260
+ case 'hyperliquid:standard':
261
+ case 'hyperliquid:unified':
262
+ case 'hyperliquid:portfolioMargin':
263
+ case 'lighter:classic':
264
+ case 'lighter:unified':
265
+ return balance.unrealizedPnl;
266
+
267
+ // The three CEXs carry the core only, so there is no figure to return.
268
+ case 'binance:singleAsset':
269
+ case 'binance:multiAsset':
270
+ case 'bybit:isolatedMargin':
271
+ case 'bybit:regularMargin':
272
+ case 'bybit:portfolioMargin':
273
+ case 'okx:spot':
274
+ case 'okx:futures':
275
+ case 'okx:multiCurrencyMargin':
276
+ case 'okx:portfolioMargin':
277
+ return null;
278
+ }
279
+ }
280
+ ```
281
+
282
+ Each variant is exported as both a zod schema and its inferred type — `HyperliquidStandardBalance`, `HyperliquidUnifiedBalance`, `HyperliquidPortfolioMarginBalance`, `LighterClassicBalance`, `LighterUnifiedBalance`, `BinanceSingleAssetBalance`, `BinanceMultiAssetBalance`, `BybitIsolatedMarginBalance`, `BybitRegularMarginBalance`, `BybitPortfolioMarginBalance`, `OkxSpotBalance`, `OkxFuturesBalance`, `OkxMultiCurrencyMarginBalance`, `OkxPortfolioMarginBalance` — alongside `AccountBalanceCore` and `CollateralBalance`.
283
+
208
284
  ### Positions
209
285
 
210
286
  ```typescript
@@ -256,16 +332,20 @@ tracker.isConnected;
256
332
  tracker.isInitialized; // true after first balance/position state (asset metadata alone does not count)
257
333
  ```
258
334
 
259
- ## Account Type Labels
335
+ ## Account Mode Labels
260
336
 
261
- `balance.accountType` is an exchange-specific enum. Render with:
337
+ `balance.accountMode` names the venue and the mode together. Render it with the label the venue's own account page uses:
262
338
 
263
339
  ```typescript
264
- import { ACCOUNT_TYPE_LABELS } from '@pear-protocol/exchanges-sdk';
340
+ import { ACCOUNT_MODE_LABELS } from '@pear-protocol/exchanges-sdk';
265
341
 
266
- ACCOUNT_TYPE_LABELS[balance.accountType]; // "Cross Margin", "Portfolio Margin", ...
342
+ ACCOUNT_MODE_LABELS['hyperliquid:unified']; // "Unified Account"
343
+ ACCOUNT_MODE_LABELS['binance:singleAsset']; // "Single-Asset Mode"
344
+ ACCOUNT_MODE_LABELS[balance.accountMode];
267
345
  ```
268
346
 
347
+ The map covers every member of `AccountMode`, so a mode added later cannot leave a gap.
348
+
269
349
  ## Cleanup
270
350
 
271
351
  ```typescript
@@ -57,6 +57,13 @@ async function fetchAccountInfo(creds, demo) {
57
57
  return signedGet(creds, "/v5/account/info", {}, demo);
58
58
  }
59
59
  __name(fetchAccountInfo, "fetchAccountInfo");
60
+ const TRANSFERABLE_MAX_COINS = 20;
61
+ async function fetchTransferableAmount(creds, coins, demo) {
62
+ return signedGet(creds, "/v5/account/withdrawal", {
63
+ coinName: coins.slice(0, TRANSFERABLE_MAX_COINS).join(",")
64
+ }, demo);
65
+ }
66
+ __name(fetchTransferableAmount, "fetchTransferableAmount");
60
67
  async function fetchCollateralRatios(creds, demo) {
61
68
  const result = await signedGet(creds, "/v5/spot-margin-uta/tier-collateral-ratio", {}, demo);
62
69
  const ratios = /* @__PURE__ */ new Map();
@@ -101,4 +108,4 @@ async function fetchExecutionsSince(creds, since, demo) {
101
108
  }
102
109
  __name(fetchExecutionsSince, "fetchExecutionsSince");
103
110
 
104
- export { fetchAccountInfo, fetchCollateralRatios, fetchExecutionsSince, fetchPositionLeverage, fetchPositions, fetchWalletBalance };
111
+ export { fetchAccountInfo, fetchCollateralRatios, fetchExecutionsSince, fetchPositionLeverage, fetchPositions, fetchTransferableAmount, fetchWalletBalance };
@@ -56,6 +56,26 @@ async function fetchSpotClearinghouseState(user, demo = false) {
56
56
  }, demo);
57
57
  }
58
58
  __name(fetchSpotClearinghouseState, "fetchSpotClearinghouseState");
59
+ async function fetchAllMids(demo = false) {
60
+ return fetchInfo({
61
+ type: "allMids"
62
+ }, demo);
63
+ }
64
+ __name(fetchAllMids, "fetchAllMids");
65
+ async function fetchDelegatorSummary(user, demo = false) {
66
+ return fetchInfo({
67
+ type: "delegatorSummary",
68
+ user
69
+ }, demo);
70
+ }
71
+ __name(fetchDelegatorSummary, "fetchDelegatorSummary");
72
+ async function fetchUserVaultEquities(user, demo = false) {
73
+ return fetchInfo({
74
+ type: "userVaultEquities",
75
+ user
76
+ }, demo);
77
+ }
78
+ __name(fetchUserVaultEquities, "fetchUserVaultEquities");
59
79
  async function fetchActiveAssetData(user, coin, demo = false) {
60
80
  return fetchInfo({
61
81
  type: "activeAssetData",
@@ -65,4 +85,4 @@ async function fetchActiveAssetData(user, coin, demo = false) {
65
85
  }
66
86
  __name(fetchActiveAssetData, "fetchActiveAssetData");
67
87
 
68
- export { fetchActiveAssetData, fetchClearinghouseState, fetchInfo, fetchPerpDexs, fetchSpotClearinghouseState, fetchUserAbstraction, fetchUserRole };
88
+ export { fetchActiveAssetData, fetchAllMids, fetchClearinghouseState, fetchDelegatorSummary, fetchInfo, fetchPerpDexs, fetchSpotClearinghouseState, fetchUserAbstraction, fetchUserRole, fetchUserVaultEquities };
@@ -144,15 +144,6 @@ async function fetchAccount(accountIndex, demo = false) {
144
144
  return readLighterResponse(response, "/account");
145
145
  }
146
146
  __name(fetchAccount, "fetchAccount");
147
- async function fetchAccountLimits(accountIndex, authToken, demo = false) {
148
- const response = await proxyFetch(`${getLighterBaseUrl(demo)}/api/v1/accountLimits?account_index=${accountIndex}`, {
149
- headers: {
150
- authorization: authToken
151
- }
152
- });
153
- return readLighterResponse(response, "/accountLimits");
154
- }
155
- __name(fetchAccountLimits, "fetchAccountLimits");
156
147
  async function fetchAccountMetadata(accountIndex, authToken, demo = false) {
157
148
  const response = await proxyFetch(`${getLighterBaseUrl(demo)}/api/v1/accountMetadata?by=index&value=${accountIndex}`, {
158
149
  headers: {
@@ -168,9 +159,12 @@ async function fetchAccountMetadata(accountIndex, authToken, demo = false) {
168
159
  }
169
160
  __name(fetchAccountMetadata, "fetchAccountMetadata");
170
161
  async function fetchOrderBookDetails(demo = false) {
171
- const response = await proxyFetch(`${getLighterBaseUrl(demo)}/api/v1/orderBookDetails?filter=perp`);
162
+ const response = await proxyFetch(`${getLighterBaseUrl(demo)}/api/v1/orderBookDetails`);
172
163
  const data = await readLighterResponse(response, "/orderBookDetails");
173
- return data.order_book_details;
164
+ return {
165
+ perp: data.order_book_details ?? [],
166
+ spot: data.spot_order_book_details ?? []
167
+ };
174
168
  }
175
169
  __name(fetchOrderBookDetails, "fetchOrderBookDetails");
176
170
  async function fetchAssetDetails(demo) {
@@ -289,4 +283,4 @@ async function readLighterResponse(response, endpoint, acceptedCodes = [
289
283
  }
290
284
  __name(readLighterResponse, "readLighterResponse");
291
285
 
292
- export { createCctpDepositIntentAddress, fetchAccount, fetchAccountLimits, fetchAccountMetadata, fetchAccountsByL1Address, fetchApiKeys, fetchAssetDetails, fetchDepositHistory, fetchDepositNetworks, fetchOrderBookDetails, fetchPositionFundingSince, fetchTradesSince };
286
+ export { createCctpDepositIntentAddress, fetchAccount, fetchAccountMetadata, fetchAccountsByL1Address, fetchApiKeys, fetchAssetDetails, fetchDepositHistory, fetchDepositNetworks, fetchOrderBookDetails, fetchPositionFundingSince, fetchTradesSince };
@@ -49,6 +49,10 @@ async function fetchBalance(creds, demo) {
49
49
  return data[0];
50
50
  }
51
51
  __name(fetchBalance, "fetchBalance");
52
+ async function fetchMaxWithdrawals(creds, demo) {
53
+ return signedGet(creds, "/api/v5/account/max-withdrawal", {}, demo);
54
+ }
55
+ __name(fetchMaxWithdrawals, "fetchMaxWithdrawals");
52
56
  async function fetchPositions(creds, demo) {
53
57
  return signedGet(creds, "/api/v5/account/positions", {
54
58
  instType: "SWAP"
@@ -94,4 +98,4 @@ async function fetchFillsHistorySince(creds, since, demo) {
94
98
  }
95
99
  __name(fetchFillsHistorySince, "fetchFillsHistorySince");
96
100
 
97
- export { fetchAccountConfig, fetchBalance, fetchFillsHistorySince, fetchLeverageInfos, fetchPositions };
101
+ export { fetchAccountConfig, fetchBalance, fetchFillsHistorySince, fetchLeverageInfos, fetchMaxWithdrawals, fetchPositions };