@pear-protocol/exchanges-sdk 0.17.1 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -0
- package/dist/client/lighter/rest.js +12 -1
- package/dist/helper/hyperliquid/helper.js +76 -0
- package/dist/helper/index.js +2 -0
- package/dist/helper/lighter/helper.js +39 -15
- package/dist/helper/lighter/subaccount.helper.js +118 -0
- package/dist/helper/subaccount.types.js +1 -0
- package/dist/index.d.ts +270 -49
- package/dist/sdk.js +28 -6
- package/dist/status/hyperliquid/status.js +3 -3
- package/dist/syncer/base.js +1 -21
- package/dist/syncer/hyperliquid/syncer.js +2 -1
- package/dist/syncer/lighter/syncer.js +4 -4
- package/dist/tracker/base.js +4 -0
- package/dist/tracker/equity.js +13 -0
- package/dist/tracker/hyperliquid/balance.js +29 -5
- package/dist/tracker/hyperliquid/decimal.js +5 -1
- package/dist/tracker/hyperliquid/mapper.js +69 -1
- package/dist/tracker/hyperliquid/tracker.js +52 -24
- package/dist/tracker/index.js +24 -1
- package/dist/tracker/lighter/mapper.js +20 -1
- package/dist/tracker/lighter/tracker.js +29 -14
- package/package.json +5 -4
|
@@ -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
|
-
|
|
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;
|
package/dist/tracker/base.js
CHANGED
|
@@ -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,
|
|
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 {
|
|
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 [
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
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
|
|
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,
|
package/dist/tracker/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
422
|
+
/** Public access has no credentials; connected accounts reuse their cached auth token. */
|
|
408
423
|
async retrieveAuthToken() {
|
|
409
|
-
if (!this.
|
|
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.
|
|
418
|
-
apiPrivateKey: this.
|
|
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.
|
|
3
|
+
"version": "0.19.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
|
-
"@
|
|
31
|
-
"@pear-protocol/
|
|
32
|
-
"@pear-protocol/
|
|
30
|
+
"@nktkas/hyperliquid": "0.33.2",
|
|
31
|
+
"@pear-protocol/core-sdk": "^2.8.0",
|
|
32
|
+
"@pear-protocol/types": "^2.15.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",
|