@pear-protocol/exchanges-sdk 0.17.1 → 0.18.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.
@@ -129,7 +129,8 @@ class HyperliquidAccountSyncer extends BaseAccountSyncer {
129
129
  timestamp: rawFill.time,
130
130
  tradeFee: tradeFee.toFixed(),
131
131
  builderFee: new BigNumber(rawFill.builderFee ?? "0").abs().toFixed(),
132
- liquidation: rawFill.liquidation ? {
132
+ // Preserve the existing marker when attribution is absent; reject explicitly named counterparties.
133
+ liquidation: rawFill.liquidation && (!rawFill.liquidation.liquidatedUser || rawFill.liquidation.liquidatedUser.toLowerCase() === this.address.toLowerCase()) ? {
133
134
  mark: rawFill.liquidation.markPx
134
135
  } : null
135
136
  });
@@ -82,7 +82,7 @@ class LighterAccountSyncer extends BaseAccountSyncer {
82
82
  timestamp: rawFill.timestamp,
83
83
  tradeFee: this.getTradeFee(rawFill, accountSide),
84
84
  builderFee: this.getIntegratorFee(rawFill, accountSide),
85
- liquidation: rawFill.type === "liquidation" ? {
85
+ liquidation: rawFill.type === "liquidation" && !this.isMaker(rawFill, accountSide) ? {
86
86
  mark: rawFill.price
87
87
  } : null
88
88
  });
@@ -97,13 +97,13 @@ class LighterAccountSyncer extends BaseAccountSyncer {
97
97
  return toFundingPaymentEntries(fundingEvents, instrumentIds, this.connector);
98
98
  }
99
99
  isReduceOnly(rawFill) {
100
- if (rawFill.type === "liquidation" || rawFill.type === "deleverage" || rawFill.type === "market-settlement") {
101
- return true;
102
- }
103
100
  const accountSide = this.getAccountSide(rawFill);
104
101
  if (!accountSide) {
105
102
  return false;
106
103
  }
104
+ if (rawFill.type === "liquidation" && !this.isMaker(rawFill, accountSide) || rawFill.type === "deleverage" || rawFill.type === "market-settlement") {
105
+ return true;
106
+ }
107
107
  const positionSignChanged = this.isMaker(rawFill, accountSide) ? rawFill.maker_position_sign_changed : rawFill.taker_position_sign_changed;
108
108
  if (positionSignChanged) {
109
109
  return false;
@@ -20,6 +20,10 @@ class BaseAccountTracker {
20
20
  config.onError?.(error);
21
21
  };
22
22
  }
23
+ /** Only public account venues implement credential-free trading equity. */
24
+ async tradingEquity() {
25
+ throw new Error("Public trading equity is unavailable for this exchange");
26
+ }
23
27
  async snapshotAssets(coins) {
24
28
  const unique = [
25
29
  ...new Set(coins)
@@ -0,0 +1,13 @@
1
+ import { Connector } from '@pear-protocol/types';
2
+ import { z } from 'zod';
3
+
4
+ const PublicExchangeAccount = z.object({
5
+ connector: Connector.describe("Exchange connector type"),
6
+ exchangeIdentifier: z.string().describe("User identifier on the exchange")
7
+ });
8
+ const TradingEquity = z.object({
9
+ tradingEquityUsd: z.string().regex(/^-?\d+(\.\d+)?$/),
10
+ timestamp: z.number().int()
11
+ });
12
+
13
+ export { PublicExchangeAccount, TradingEquity };
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { DecimalString, AccountBalanceCore, SignedDecimalString } from '../balance-core';
2
+ import { DecimalString, SignedDecimalString, AccountBalanceCore } from '../balance-core';
3
3
 
4
4
  const HyperliquidDexState = z.object({
5
5
  dex: z.string(),
@@ -9,6 +9,24 @@ const HyperliquidDexState = z.object({
9
9
  crossMaintenanceMargin: DecimalString,
10
10
  withdrawable: DecimalString
11
11
  });
12
+ const HyperliquidSpotHolding = z.object({
13
+ /** The coin's wire name, as every other Hyperliquid row names it — `UBTC`, never the `BTC` alias. */
14
+ asset: z.string(),
15
+ /** The spot token index, the id `perDexState[].collateralToken` joins on. */
16
+ token: z.number().int(),
17
+ /** The venue's `total`, signed: a Portfolio Margin borrow holds a NEGATIVE total. */
18
+ total: SignedDecimalString,
19
+ /**
20
+ * The venue's `hold`, signed and unfloored, exactly as sent. A borrowed coin can report a NEGATIVE
21
+ * hold — borrowing headroom, not a lock — so a reader that wants only the lock floors it itself.
22
+ */
23
+ hold: SignedDecimalString,
24
+ /**
25
+ * The venue's `entryNtl`: what the holding COST in USDC, never what it is worth now. `null` where
26
+ * the wire publishes no key or an unreadable one, which is not a cost basis of zero.
27
+ */
28
+ entryNotional: SignedDecimalString.nullable()
29
+ });
12
30
  const HyperliquidStandardBalance = AccountBalanceCore.extend({
13
31
  accountMode: z.literal("hyperliquid:standard"),
14
32
  /**
@@ -76,7 +94,9 @@ const HyperliquidStandardBalance = AccountBalanceCore.extend({
76
94
  crossMarginRatio: DecimalString,
77
95
  crossAccountLeverage: DecimalString,
78
96
  /** Kept per dex — the aggregate hides HIP-3 positions entirely. */
79
- perDexState: z.array(HyperliquidDexState)
97
+ perDexState: z.array(HyperliquidDexState),
98
+ /** The whole spot wallet, one row per coin — a separate wallet from the perps side in this mode. */
99
+ spotHoldings: z.array(HyperliquidSpotHolding)
80
100
  });
81
101
  const HyperliquidUnifiedBalance = AccountBalanceCore.extend({
82
102
  accountMode: z.literal("hyperliquid:unified"),
@@ -117,7 +137,9 @@ const HyperliquidUnifiedBalance = AccountBalanceCore.extend({
117
137
  * leaves the quotient unknown, where a zero would claim the account carries no leverage at all.
118
138
  */
119
139
  unifiedAccountLeverage: DecimalString.nullable(),
120
- perDexState: z.array(HyperliquidDexState)
140
+ perDexState: z.array(HyperliquidDexState),
141
+ /** The whole spot wallet, one row per coin — `perCollateral` lists only the dex collateral tokens. */
142
+ spotHoldings: z.array(HyperliquidSpotHolding)
121
143
  });
122
144
  const HyperliquidPortfolioMarginCollateral = z.object({
123
145
  asset: z.string(),
@@ -229,7 +251,9 @@ const HyperliquidPortfolioMarginBalance = AccountBalanceCore.extend({
229
251
  * shows before the first one happens. Empty until the first read lands.
230
252
  */
231
253
  borrowLendReserves: z.array(HyperliquidBorrowLendReserve),
232
- perDexState: z.array(HyperliquidDexState)
254
+ perDexState: z.array(HyperliquidDexState),
255
+ /** The whole spot wallet, one row per coin, in the same shape the other two modes publish. */
256
+ spotHoldings: z.array(HyperliquidSpotHolding)
233
257
  });
234
258
 
235
- export { HyperliquidBorrowLendReserve, HyperliquidDexState, HyperliquidPortfolioMarginBalance, HyperliquidPortfolioMarginCollateral, HyperliquidStandardBalance, HyperliquidUnifiedBalance };
259
+ export { HyperliquidBorrowLendReserve, HyperliquidDexState, HyperliquidPortfolioMarginBalance, HyperliquidPortfolioMarginCollateral, HyperliquidSpotHolding, HyperliquidStandardBalance, HyperliquidUnifiedBalance };
@@ -13,6 +13,10 @@ function toPrice(value) {
13
13
  return price;
14
14
  }
15
15
  __name(toPrice, "toPrice");
16
+ function isWireDecimal(value) {
17
+ return value !== null && value !== void 0 && WIRE_DECIMAL.test(value.trim());
18
+ }
19
+ __name(isWireDecimal, "isWireDecimal");
16
20
  function toDecimal(value) {
17
21
  if (value === null || value === void 0) {
18
22
  return ZERO;
@@ -25,4 +29,4 @@ function toDecimal(value) {
25
29
  }
26
30
  __name(toDecimal, "toDecimal");
27
31
 
28
- export { ONE, ZERO, toDecimal, toPrice };
32
+ export { ONE, ZERO, isWireDecimal, toDecimal, toPrice };
@@ -1,4 +1,7 @@
1
+ import { parse, add as add$1, multiply } from '@pear-protocol/utils';
1
2
  import BigNumber from 'bignumber.js';
3
+ import { z } from 'zod';
4
+ import { TradingEquity } from '../equity';
2
5
  import { positionKey } from '../utils';
3
6
  import { toDecimal, ZERO } from './decimal';
4
7
  import { HyperliquidAccountType } from './types';
@@ -100,6 +103,43 @@ function mapBalance(context) {
100
103
  }
101
104
  }
102
105
  __name(mapBalance, "mapBalance");
106
+ function mapTradingEquity(mode, spot, prices, states, timestamp) {
107
+ z.array(z.object({
108
+ total: TradingEquity.shape.tradingEquityUsd
109
+ })).parse(spot.balances);
110
+ const valuation = prices.valueBalances(spot.balances);
111
+ if (valuation.unpricedCoins.length > 0) {
112
+ throw new Error(`Cannot price Hyperliquid holdings: ${valuation.unpricedCoins.join(", ")}`);
113
+ }
114
+ if (mode === null) {
115
+ throw new Error("Unsupported Hyperliquid account mode");
116
+ }
117
+ let equity = parse(valuation.equity);
118
+ switch (mode) {
119
+ case HyperliquidAccountType.UNIFIED_ACCOUNT:
120
+ case HyperliquidAccountType.PORTFOLIO_MARGIN:
121
+ break;
122
+ case HyperliquidAccountType.DEFAULT:
123
+ case HyperliquidAccountType.DISABLED:
124
+ for (const { dex, collateralToken, state } of states) {
125
+ const value = parse(TradingEquity.shape.tradingEquityUsd.parse(state.marginSummary.accountValue));
126
+ if (value.isZero()) {
127
+ continue;
128
+ }
129
+ const price = collateralToken === null ? null : prices.midForToken(collateralToken);
130
+ if (price === null) {
131
+ throw new Error(`Cannot price Hyperliquid collateral for ${dex || "main dex"}`);
132
+ }
133
+ equity = add$1(equity, multiply(value, price));
134
+ }
135
+ break;
136
+ }
137
+ return TradingEquity.parse({
138
+ tradingEquityUsd: equity.toFixed(),
139
+ timestamp
140
+ });
141
+ }
142
+ __name(mapTradingEquity, "mapTradingEquity");
103
143
  function mapPositions(raw) {
104
144
  const positions = /* @__PURE__ */ new Map();
105
145
  for (const assetPosition of [
@@ -168,6 +208,8 @@ function mapStandardBalance(context) {
168
208
  crossMarginRatio: ratio(context.summary.crossMaintenanceMarginUsed, context.summary.crossAccountValue),
169
209
  crossAccountLeverage: accountLeverage(context.summary.crossNotional, context.summary.crossAccountValue),
170
210
  perDexState: context.summary.perDexState,
211
+ // The whole spot wallet in every mode, so a reader never has to know which mode lists which coins.
212
+ spotHoldings: spotHoldingRows(context),
171
213
  timestamp: context.timestamp
172
214
  };
173
215
  }
@@ -201,6 +243,8 @@ function mapUnifiedBalance(context) {
201
243
  unifiedAccountRatio: crossRatioAgainstFreeSpot(context),
202
244
  unifiedAccountLeverage: unifiedAccountLeverage(context),
203
245
  perDexState: context.summary.perDexState,
246
+ // The whole spot wallet in every mode, so a reader never has to know which mode lists which coins.
247
+ spotHoldings: spotHoldingRows(context),
204
248
  timestamp: context.timestamp
205
249
  };
206
250
  }
@@ -236,6 +280,8 @@ function mapPortfolioMarginBalance(context) {
236
280
  healthFactor: wireHealthFactor(context),
237
281
  borrowLendReserves: borrowLendReserveRows(context),
238
282
  perDexState: context.summary.perDexState,
283
+ // The whole spot wallet in every mode, so a reader never has to know which mode lists which coins.
284
+ spotHoldings: spotHoldingRows(context),
239
285
  timestamp: context.timestamp
240
286
  };
241
287
  }
@@ -267,6 +313,28 @@ function portfolioMarginCollateralRows(context) {
267
313
  return rows;
268
314
  }
269
315
  __name(portfolioMarginCollateralRows, "portfolioMarginCollateralRows");
316
+ function spotHoldingRows(context) {
317
+ const rows = [];
318
+ for (const balance of context.spotBalances.values()) {
319
+ rows.push({
320
+ asset: balance.coin,
321
+ token: balance.token,
322
+ total: signed(balance.total),
323
+ hold: signed(balance.hold),
324
+ entryNotional: balance.entryNtl === null ? null : signed(balance.entryNtl)
325
+ });
326
+ }
327
+ rows.sort(byHoldingAsset);
328
+ return rows;
329
+ }
330
+ __name(spotHoldingRows, "spotHoldingRows");
331
+ function byHoldingAsset(left, right) {
332
+ if (left.asset === right.asset) {
333
+ return 0;
334
+ }
335
+ return left.asset < right.asset ? -1 : 1;
336
+ }
337
+ __name(byHoldingAsset, "byHoldingAsset");
270
338
  function byRowAsset(left, right) {
271
339
  if (left.asset === right.asset) {
272
340
  return 0;
@@ -568,4 +636,4 @@ function ratio(numerator, denominator) {
568
636
  }
569
637
  __name(ratio, "ratio");
570
638
 
571
- export { aggregateClearinghouseStates, emptyClearinghouseSummary, mapBalance, mapPositions };
639
+ export { aggregateClearinghouseStates, emptyClearinghouseSummary, mapBalance, mapPositions, mapTradingEquity };
@@ -1,12 +1,13 @@
1
+ import { EthereumAddress } from '@pear-protocol/types';
1
2
  import { createExchangeWs } from '../../client';
2
- import { fetchPerpDexs, fetchInfo, fetchAllMids, fetchUserAbstraction, fetchSpotClearinghouseState, fetchDelegatorSummary, fetchUserVaultEquities, fetchClearinghouseState, fetchActiveAssetData } from '../../client/hyperliquid/rest';
3
+ import { fetchDelegatorSummary, fetchUserVaultEquities, fetchInfo, fetchPerpDexs, fetchUserAbstraction, fetchSpotClearinghouseState, fetchAllMids, fetchClearinghouseState, fetchActiveAssetData } from '../../client/hyperliquid/rest';
3
4
  import { BaseAccountTracker } from '../base';
4
5
  import { resolveHyperliquidAccountType } from './account-mode';
5
6
  import { EMPTY_BORROW_LEND_STATE, fetchBorrowLendState } from './borrow-lend';
6
7
  import { buildAvailableMarginByToken, buildCollateralTokenNames, minimumAvailableToTrade } from './collateral';
7
8
  import { LOCKED_TRACKED_COINS } from './const';
8
- import { toDecimal, ZERO } from './decimal';
9
- import { emptyClearinghouseSummary, aggregateClearinghouseStates, mapBalance, mapPositions } from './mapper';
9
+ import { toDecimal, isWireDecimal, ZERO } from './decimal';
10
+ import { emptyClearinghouseSummary, aggregateClearinghouseStates, mapBalance, mapPositions, mapTradingEquity } from './mapper';
10
11
  import { SpotPrices } from './spot-pricing';
11
12
  import { HyperliquidAccountType } from './types';
12
13
 
@@ -67,7 +68,7 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
67
68
  trackedAssetData = /* @__PURE__ */ new Map();
68
69
  constructor(config) {
69
70
  super(config);
70
- this.address = config.address;
71
+ this.address = "address" in config ? config.address : EthereumAddress.parse(config.account.exchangeIdentifier);
71
72
  }
72
73
  /**
73
74
  * Reads account mode, spot prices, collateral balances, per-DEX state, staking, vaults and
@@ -75,36 +76,21 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
75
76
  */
76
77
  async snapshot() {
77
78
  const user = this.addrLower();
78
- const [perpDexs, allMetas, spotMeta, mids, abstraction, spotState, delegator, vaults] = await Promise.all([
79
- fetchPerpDexs(this.demo),
80
- fetchInfo({
81
- type: "allPerpMetas"
82
- }, this.demo),
83
- fetchInfo({
84
- type: "spotMeta"
85
- }, this.demo),
86
- fetchAllMids(this.demo),
87
- fetchUserAbstraction(user, this.demo),
88
- fetchSpotClearinghouseState(user, this.demo),
79
+ const [state, clearinghouseStates, delegator, vaults] = await Promise.all([
80
+ this.fetchAccountState(),
81
+ this.fetchPerpStates(),
89
82
  fetchDelegatorSummary(user, this.demo),
90
83
  fetchUserVaultEquities(user, this.demo)
91
84
  ]);
92
- const descriptors = uniqueDexDescriptors(perpDexs, allMetas);
93
- const clearinghouseStates = await Promise.all(descriptors.map(async ({ dex, meta }) => ({
94
- dex,
95
- collateralToken: meta?.collateralToken ?? null,
96
- meta,
97
- state: await fetchClearinghouseState(user, dex, this.demo)
98
- })));
85
+ const { spotState, spotMeta, prices, accountType } = state;
99
86
  const assetData = await this.fetchActiveAssetEvents(this.coinsForAvailableMargin());
100
87
  const { summary, assetPositions } = aggregateClearinghouseStates(clearinghouseStates);
101
- const accountType = resolveHyperliquidAccountType(abstraction);
102
88
  const borrowLend = accountType === HyperliquidAccountType.PORTFOLIO_MARGIN ? await fetchBorrowLendState(user, this.demo) : EMPTY_BORROW_LEND_STATE;
103
89
  const balance = mapBalance({
104
90
  accountType,
105
91
  summary,
106
92
  spotBalances: mapSpotBalances(spotState),
107
- prices: SpotPrices.from(spotMeta, mids),
93
+ prices,
108
94
  vaultEquity: sumRestVaultEquity(vaults),
109
95
  stakedHype: _stakingAccountHype(delegator),
110
96
  availableMargin: minimumAvailableToTrade(assetData.values()),
@@ -121,6 +107,13 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
121
107
  positions
122
108
  };
123
109
  }
110
+ /** Value trading holdings with the same account reads and pricing used by snapshots. */
111
+ async tradingEquity() {
112
+ const state = await this.fetchAccountState();
113
+ const standard = state.accountType === HyperliquidAccountType.DEFAULT || state.accountType === HyperliquidAccountType.DISABLED;
114
+ const clearinghouseStates = standard ? await this.fetchPerpStates() : [];
115
+ return mapTradingEquity(state.accountType, state.spotState, state.prices, clearinghouseStates, Date.now());
116
+ }
124
117
  /**
125
118
  * Stateless one-shot per-asset settings read via the `activeAssetData` info request (the REST
126
119
  * twin of the live WS subscription), one request per coin fired in parallel — the info endpoint
@@ -384,6 +377,39 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
384
377
  break;
385
378
  }
386
379
  }
380
+ /** Share the public account-mode and spot valuation inputs across one-shot reads. */
381
+ async fetchAccountState() {
382
+ const user = this.addrLower();
383
+ const [abstraction, spotState, spotMeta, mids] = await Promise.all([
384
+ fetchUserAbstraction(user, this.demo),
385
+ fetchSpotClearinghouseState(user, this.demo),
386
+ fetchInfo({
387
+ type: "spotMeta"
388
+ }, this.demo),
389
+ fetchAllMids(this.demo)
390
+ ]);
391
+ return {
392
+ accountType: resolveHyperliquidAccountType(abstraction),
393
+ spotState,
394
+ spotMeta,
395
+ prices: SpotPrices.from(spotMeta, mids)
396
+ };
397
+ }
398
+ /** Retain each DEX's collateral identity when reading separate perpetual wallets. */
399
+ async fetchPerpStates() {
400
+ const [dexes, metas] = await Promise.all([
401
+ fetchPerpDexs(this.demo),
402
+ fetchInfo({
403
+ type: "allPerpMetas"
404
+ }, this.demo)
405
+ ]);
406
+ return await Promise.all(uniqueDexDescriptors(dexes, metas).map(async ({ dex, meta }) => ({
407
+ dex,
408
+ collateralToken: meta?.collateralToken ?? null,
409
+ meta,
410
+ state: await fetchClearinghouseState(this.addrLower(), dex, this.demo)
411
+ })));
412
+ }
387
413
  loadPerpMetas(allMetas, perpDexs) {
388
414
  allMetas.forEach((dexMeta, index) => {
389
415
  const dex = perpDexs[index]?.name ?? "";
@@ -774,6 +800,8 @@ function mapSpotBalances(spotState) {
774
800
  token: balance.token,
775
801
  total: balance.total,
776
802
  hold: balance.hold,
803
+ // An absent or unreadable `entryNtl` states no cost basis, which is not a cost basis of zero.
804
+ entryNtl: isWireDecimal(balance.entryNtl) ? balance.entryNtl : null,
777
805
  // An absent `ltv` means "not collateral", which weighs zero — never a zero weight, which is
778
806
  // a stablecoin at full value. The two states stay apart from the wire onward.
779
807
  ltv: balance.ltv ?? null,
@@ -2,6 +2,8 @@ import { BinanceAccountTracker } from './binance/tracker';
2
2
  export { BinanceAccountTracker } from './binance/tracker';
3
3
  import { BybitAccountTracker } from './bybit/tracker';
4
4
  export { BybitAccountTracker } from './bybit/tracker';
5
+ import { PublicExchangeAccount } from './equity';
6
+ export { PublicExchangeAccount, TradingEquity } from './equity';
5
7
  import { HyperliquidAccountTracker } from './hyperliquid/tracker';
6
8
  export { HyperliquidAccountTracker } from './hyperliquid/tracker';
7
9
  import { LighterAccountTracker } from './lighter/tracker';
@@ -13,7 +15,7 @@ export { ACCOUNT_MODE_LABELS, AccountBalanceCore, AccountMode, CollateralBalance
13
15
  export { BaseAccountTracker } from './base';
14
16
  export { BinanceMultiAssetBalance, BinanceSingleAssetBalance } from './binance/balance';
15
17
  export { BybitIsolatedMarginBalance, BybitPortfolioMarginBalance, BybitRegularMarginBalance } from './bybit/balance';
16
- export { HyperliquidBorrowLendReserve, HyperliquidDexState, HyperliquidPortfolioMarginBalance, HyperliquidPortfolioMarginCollateral, HyperliquidStandardBalance, HyperliquidUnifiedBalance } from './hyperliquid/balance';
18
+ export { HyperliquidBorrowLendReserve, HyperliquidDexState, HyperliquidPortfolioMarginBalance, HyperliquidPortfolioMarginCollateral, HyperliquidSpotHolding, HyperliquidStandardBalance, HyperliquidUnifiedBalance } from './hyperliquid/balance';
17
19
  export { LighterClassicBalance, LighterUnifiedBalance } from './lighter/balance';
18
20
  export { LIGHTER_QUOTE_ASSETS } from './lighter/const';
19
21
  export { OkxFuturesBalance, OkxMultiCurrencyMarginBalance, OkxPortfolioMarginBalance, OkxSpotBalance } from './okx/balance';
@@ -22,6 +24,27 @@ export * from './types';
22
24
  var __defProp = Object.defineProperty;
23
25
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
24
26
  function createAccountTracker(exchange, config) {
27
+ if (config.access === "public") {
28
+ const account = PublicExchangeAccount.parse(config.account);
29
+ switch (exchange) {
30
+ case "hyperliquid":
31
+ return new HyperliquidAccountTracker({
32
+ ...config,
33
+ account
34
+ });
35
+ case "lighter":
36
+ case "lighter_rhc":
37
+ return new LighterAccountTracker({
38
+ ...config,
39
+ connector: exchange,
40
+ account
41
+ });
42
+ case "binance":
43
+ case "bybit":
44
+ case "okx":
45
+ throw new Error(`Public account reads are unavailable for ${exchange}`);
46
+ }
47
+ }
25
48
  switch (exchange) {
26
49
  case "binance":
27
50
  return new BinanceAccountTracker({
@@ -1,4 +1,7 @@
1
+ import { add } from '@pear-protocol/utils';
1
2
  import BigNumber from 'bignumber.js';
3
+ import { z } from 'zod';
4
+ import { TradingEquity } from '../equity';
2
5
  import { positionKey } from '../utils';
3
6
  import { LighterAccountType } from './types';
4
7
 
@@ -70,6 +73,22 @@ function mapRestBalance(account, markets, timestamp) {
70
73
  });
71
74
  }
72
75
  __name(mapRestBalance, "mapRestBalance");
76
+ function mapTradingEquity(account, markets, timestamp) {
77
+ const accountValue = TradingEquity.shape.tradingEquityUsd.parse(account.total_asset_value);
78
+ z.array(z.object({
79
+ symbol: z.string(),
80
+ balance: TradingEquity.shape.tradingEquityUsd
81
+ })).parse(account.assets);
82
+ const spotEquity = toEquitySplit(account.assets ?? [], markets.spotPriceBySymbol, markets.quoteAsset).spot;
83
+ if (spotEquity === null) {
84
+ throw new Error("Cannot price Lighter spot holdings");
85
+ }
86
+ return TradingEquity.parse({
87
+ tradingEquityUsd: add(accountValue, spotEquity.toFixed()).toFixed(),
88
+ timestamp
89
+ });
90
+ }
91
+ __name(mapTradingEquity, "mapTradingEquity");
73
92
  function mapPositions(raw, includeZeroPositions = false) {
74
93
  const positions = /* @__PURE__ */ new Map();
75
94
  const trackedAssets = /* @__PURE__ */ new Map();
@@ -429,4 +448,4 @@ function signed(value) {
429
448
  }
430
449
  __name(signed, "signed");
431
450
 
432
- export { buildSpotPriceIndex, leverageFromImf, mapLiveBalance, mapMarketDefaultAssetInfo, mapPositions, mapRestBalance, toLighterAccountTier, toLighterAccountType };
451
+ export { buildSpotPriceIndex, leverageFromImf, mapLiveBalance, mapMarketDefaultAssetInfo, mapPositions, mapRestBalance, mapTradingEquity, toLighterAccountTier, toLighterAccountType };
@@ -1,10 +1,11 @@
1
1
  import BigNumber from 'bignumber.js';
2
+ import { z } from 'zod';
2
3
  import { createExchangeWs } from '../../client';
3
4
  import { fetchAccount, fetchPublicAccount, fetchOrderBookDetails, fetchPublicOrderBookDetails, fetchAccountLimits } from '../../client/lighter/rest';
4
5
  import { retrieveLighterAuthToken } from '../../helper/lighter/auth-token.helper';
5
6
  import { BaseAccountTracker } from '../base';
6
7
  import { LIGHTER_QUOTE_ASSETS } from './const';
7
- import { buildSpotPriceIndex, mapPositions, mapRestBalance, mapMarketDefaultAssetInfo, toLighterAccountType, mapLiveBalance, toLighterAccountTier } from './mapper';
8
+ import { buildSpotPriceIndex, mapPositions, mapRestBalance, mapTradingEquity, 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 });
@@ -36,8 +37,6 @@ class LighterAccountTracker extends BaseAccountTracker {
36
37
  connector;
37
38
  quoteAsset;
38
39
  accountIdentifier;
39
- apiKeyIndex;
40
- apiPrivateKey;
41
40
  creds;
42
41
  ws = null;
43
42
  messageListenerId = null;
@@ -68,15 +67,12 @@ class LighterAccountTracker extends BaseAccountTracker {
68
67
  hasAssets = false;
69
68
  constructor(config) {
70
69
  super(config);
71
- const credentials = config.credentials;
72
- this.sdk = config.sdk;
70
+ this.creds = config.access === "public" ? null : config.credentials;
71
+ this.sdk = config.access === "public" ? null : config.sdk;
73
72
  this.connector = config.connector;
74
73
  this.quoteAsset = LIGHTER_QUOTE_ASSETS[config.connector];
75
74
  this.spotPriceBySymbol = buildSpotPriceIndex([], this.quoteAsset);
76
- this.accountIdentifier = config.account.exchangeIdentifier;
77
- this.apiKeyIndex = credentials.api_key_index;
78
- this.apiPrivateKey = credentials.api_private_key;
79
- this.creds = credentials;
75
+ this.accountIdentifier = z.string().regex(/^\d+$/).transform(Number).pipe(z.number().int().nonnegative().safe()).parse(config.account.exchangeIdentifier).toString();
80
76
  }
81
77
  /**
82
78
  * The tier Lighter has the account on, and every later change to it.
@@ -129,6 +125,17 @@ class LighterAccountTracker extends BaseAccountTracker {
129
125
  positions: Array.from(positions.values())
130
126
  };
131
127
  }
128
+ /** Reuse the snapshot read path and let the mapper include open PnL exactly once. */
129
+ async tradingEquity() {
130
+ const [account, markets] = await Promise.all([
131
+ this.fetchPrimaryAccount(),
132
+ this.fetchMarkets()
133
+ ]);
134
+ if (!account) {
135
+ throw new Error("Lighter account not found");
136
+ }
137
+ return mapTradingEquity(account, toMarketContext(markets.perp, markets.spot, this.quoteAsset), Date.now());
138
+ }
132
139
  /**
133
140
  * Stateless one-shot per-asset settings read, keyed by market id (the same identifier the live
134
141
  * trackAsset path uses). One public `GET /api/v1/account` covers every market the account holds
@@ -164,6 +171,9 @@ class LighterAccountTracker extends BaseAccountTracker {
164
171
  }
165
172
  /** Creates one tracker lifecycle while repeated callers share the same pending start. */
166
173
  async performStart() {
174
+ if (!this.creds) {
175
+ throw new Error("Public Lighter trackers support REST snapshots only");
176
+ }
167
177
  const lifecycleVersion = ++this.lifecycleVersion;
168
178
  const markets = await this.fetchMarkets();
169
179
  if (lifecycleVersion !== this.lifecycleVersion) return;
@@ -397,6 +407,11 @@ class LighterAccountTracker extends BaseAccountTracker {
397
407
  const authToken = await this.retrieveAuthToken();
398
408
  const accountIndex = Number(this.accountIdentifier);
399
409
  const data = authToken ? await fetchAccount(accountIndex, authToken, this.connector, this.demo) : await fetchPublicAccount(accountIndex, this.connector, this.demo);
410
+ if (!this.creds) {
411
+ z.array(z.object({
412
+ account_index: z.literal(accountIndex)
413
+ })).length(1).parse(data.accounts);
414
+ }
400
415
  return data.accounts[0];
401
416
  }
402
417
  /** Reads market context under the same account identity as the snapshot that consumes it. */
@@ -404,9 +419,9 @@ class LighterAccountTracker extends BaseAccountTracker {
404
419
  const authToken = await this.retrieveAuthToken();
405
420
  return authToken ? fetchOrderBookDetails(authToken, this.connector, this.demo) : fetchPublicOrderBookDetails(this.connector, this.demo);
406
421
  }
407
- /** Returns this account's cached auth token, or no token for the explicit empty-key verifier. */
422
+ /** Public access has no credentials; connected accounts reuse their cached auth token. */
408
423
  async retrieveAuthToken() {
409
- if (!this.apiPrivateKey) {
424
+ if (!this.creds?.api_private_key) {
410
425
  return void 0;
411
426
  }
412
427
  if (!this.sdk) {
@@ -414,8 +429,8 @@ class LighterAccountTracker extends BaseAccountTracker {
414
429
  }
415
430
  return retrieveLighterAuthToken(this.sdk, this.connector, {
416
431
  accountIndex: Number(this.accountIdentifier),
417
- apiKeyIndex: this.apiKeyIndex,
418
- apiPrivateKey: this.apiPrivateKey
432
+ apiKeyIndex: this.creds.api_key_index,
433
+ apiPrivateKey: this.creds.api_private_key
419
434
  }, this.demo);
420
435
  }
421
436
  /** Requires credentials for private WebSocket channels even when REST is in verifier mode. */
@@ -500,7 +515,7 @@ class LighterAccountTracker extends BaseAccountTracker {
500
515
  * its tier, and blanking it would announce a change nobody made.
501
516
  */
502
517
  async performAccountTierRead(lifecycleVersion) {
503
- if (!this.sdk || this.demo) {
518
+ if (!this.sdk || this.demo || !this.creds) {
504
519
  return;
505
520
  }
506
521
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pear-protocol/exchanges-sdk",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "Pear Protocol Exchanges SDK",
5
5
  "private": false,
6
6
  "type": "module",
@@ -27,9 +27,10 @@
27
27
  "test:watch": "vitest"
28
28
  },
29
29
  "dependencies": {
30
- "@pear-protocol/core-sdk": "^2.3.0",
31
- "@pear-protocol/types": "^2.10.0",
32
- "@pear-protocol/utils": "^1.2.0",
30
+ "@nktkas/hyperliquid": "0.33.2",
31
+ "@pear-protocol/core-sdk": "^2.5.0",
32
+ "@pear-protocol/types": "^2.12.0",
33
+ "@pear-protocol/utils": "^1.3.0",
33
34
  "bignumber.js": "^11.1.1",
34
35
  "partysocket": "^1.0.3",
35
36
  "viem": "^2.48.8",