@pear-protocol/exchanges-sdk 0.2.0 → 0.4.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 (60) hide show
  1. package/README.md +78 -12
  2. package/dist/client/hyperliquid/client.test.js +41 -0
  3. package/dist/client/hyperliquid/exchange-ws.js +3 -2
  4. package/dist/client/hyperliquid/rest.js +26 -5
  5. package/dist/client/hyperliquid/ws.js +16 -4
  6. package/dist/client/index.js +2 -2
  7. package/dist/client/lighter/constants.js +9 -1
  8. package/dist/client/lighter/constants.test.js +22 -0
  9. package/dist/client/lighter/exchange-ws.js +2 -2
  10. package/dist/client/lighter/rest.js +101 -32
  11. package/dist/client/lighter/rest.test.js +152 -0
  12. package/dist/client/lighter/ws.js +7 -2
  13. package/dist/client/okx/rest.js +18 -9
  14. package/dist/helper/factory.js +2 -2
  15. package/dist/helper/hyperliquid/helper.js +28 -13
  16. package/dist/helper/hyperliquid/helper.test.js +45 -0
  17. package/dist/helper/lighter/helper.js +77 -16
  18. package/dist/helper/lighter/helper.test.js +289 -0
  19. package/dist/helper/lighter/types.js +9 -8
  20. package/dist/index.d.ts +239 -47
  21. package/dist/sdk.js +20 -16
  22. package/dist/sdk.test.js +101 -0
  23. package/dist/status/hyperliquid/status.js +18 -5
  24. package/dist/status/hyperliquid/status.test.js +81 -0
  25. package/dist/status/lighter/status.js +18 -11
  26. package/dist/status/lighter/status.test.js +75 -0
  27. package/dist/syncer/base.js +107 -25
  28. package/dist/syncer/base.test.js +131 -0
  29. package/dist/syncer/binance/syncer.js +48 -20
  30. package/dist/syncer/bybit/syncer.js +39 -15
  31. package/dist/syncer/hyperliquid/funding.js +24 -0
  32. package/dist/syncer/hyperliquid/funding.test.js +63 -0
  33. package/dist/syncer/hyperliquid/syncer.js +183 -24
  34. package/dist/syncer/hyperliquid/syncer.test.js +274 -0
  35. package/dist/syncer/lighter/funding.js +25 -0
  36. package/dist/syncer/lighter/funding.test.js +48 -0
  37. package/dist/syncer/lighter/syncer.js +93 -41
  38. package/dist/syncer/lighter/syncer.test.js +132 -6
  39. package/dist/syncer/okx/syncer.js +41 -17
  40. package/dist/tracker/base.js +23 -6
  41. package/dist/tracker/base.test.js +132 -0
  42. package/dist/tracker/binance/tracker.js +48 -18
  43. package/dist/tracker/binance/tracker.test.js +138 -0
  44. package/dist/tracker/bybit/tracker.js +78 -23
  45. package/dist/tracker/bybit/tracker.test.js +145 -0
  46. package/dist/tracker/hyperliquid/mapper.js +244 -44
  47. package/dist/tracker/hyperliquid/mapper.test.js +255 -0
  48. package/dist/tracker/hyperliquid/tracker.js +241 -74
  49. package/dist/tracker/hyperliquid/tracker.test.js +587 -0
  50. package/dist/tracker/index.js +15 -37
  51. package/dist/tracker/lighter/mapper.js +163 -33
  52. package/dist/tracker/lighter/mapper.test.js +199 -0
  53. package/dist/tracker/lighter/tracker.js +219 -45
  54. package/dist/tracker/lighter/tracker.test.js +515 -0
  55. package/dist/tracker/lighter/types.js +10 -2
  56. package/dist/tracker/okx/tracker.js +50 -16
  57. package/dist/tracker/okx/tracker.test.js +126 -0
  58. package/dist/tracker/types.js +2 -1
  59. package/dist/tracker/utils.test.js +8 -0
  60. package/package.json +6 -5
@@ -0,0 +1,274 @@
1
+ import { vi, describe, beforeEach, it, expect } from 'vitest';
2
+ import { HyperliquidAccountSyncer } from './syncer';
3
+
4
+ const { fetchInfoMock } = vi.hoisted(() => ({ fetchInfoMock: vi.fn() }));
5
+ vi.mock("../../client/hyperliquid/rest", () => ({ fetchInfo: fetchInfoMock }));
6
+ const account = {
7
+ exchangeIdentifier: "0x1111111111111111111111111111111111111111"
8
+ };
9
+ const instrument = {
10
+ id: "HL-BTC",
11
+ base: "BTC"
12
+ };
13
+ function rawFill(time, tid = time) {
14
+ return {
15
+ coin: "BTC",
16
+ px: "100000",
17
+ sz: "0.1",
18
+ side: "B",
19
+ time,
20
+ startPosition: "0",
21
+ dir: "Open Long",
22
+ closedPnl: "0",
23
+ hash: "0xabc",
24
+ oid: 123,
25
+ crossed: true,
26
+ fee: "1",
27
+ builderFee: "0.2",
28
+ tid,
29
+ feeToken: "USDC",
30
+ twapId: null
31
+ };
32
+ }
33
+ function createSyncer() {
34
+ return new HyperliquidAccountSyncer({
35
+ sdk: {},
36
+ connector: "hyperliquid",
37
+ account,
38
+ credentials: { signer_key: "sk" },
39
+ demo: true
40
+ });
41
+ }
42
+ describe("HyperliquidAccountSyncer fill mapping", () => {
43
+ beforeEach(() => {
44
+ fetchInfoMock.mockReset().mockImplementation(async (body) => {
45
+ if (body.type === "userTwapSliceFillsByTime") {
46
+ return [];
47
+ }
48
+ return [
49
+ {
50
+ coin: "BTC",
51
+ px: "100000",
52
+ sz: "0.1",
53
+ side: "B",
54
+ time: 17e11,
55
+ startPosition: "0",
56
+ dir: "Open Long",
57
+ closedPnl: "0",
58
+ hash: "0xabc",
59
+ oid: 123,
60
+ crossed: true,
61
+ fee: "1",
62
+ builderFee: "0.2",
63
+ tid: 456,
64
+ feeToken: "USDC",
65
+ twapId: null
66
+ }
67
+ ];
68
+ });
69
+ });
70
+ it("uses a globally unique identity, excludes builder fee from trade fee, and routes to testnet", async () => {
71
+ const syncer = createSyncer();
72
+ const fills = await syncer.listExternalFills({
73
+ since: /* @__PURE__ */ new Date(1699999e6),
74
+ instruments: [instrument],
75
+ openPositions: [],
76
+ provisionalFills: []
77
+ });
78
+ expect(fills).toEqual([
79
+ expect.objectContaining({
80
+ exchangeFillId: "1700000000000:BTC:456",
81
+ tradeFee: "0.8",
82
+ builderFee: "0.2"
83
+ })
84
+ ]);
85
+ expect(fetchInfoMock).toHaveBeenCalledWith(expect.objectContaining({ type: "userFillsByTime" }), true);
86
+ expect(fetchInfoMock).toHaveBeenCalledWith(
87
+ expect.objectContaining({ type: "userFillsByTime", aggregateByTime: false }),
88
+ true
89
+ );
90
+ expect(fetchInfoMock).toHaveBeenCalledWith(
91
+ expect.objectContaining({ type: "userTwapSliceFillsByTime", aggregateByTime: false }),
92
+ true
93
+ );
94
+ });
95
+ it("fails closed when a maker rebate cannot satisfy the backend external-fill fee contract", async () => {
96
+ fetchInfoMock.mockImplementation(async (body) => {
97
+ if (body.type === "userTwapSliceFillsByTime") return [];
98
+ return [
99
+ {
100
+ coin: "BTC",
101
+ px: "100000",
102
+ sz: "0.1",
103
+ side: "B",
104
+ time: 17e11,
105
+ startPosition: "0",
106
+ dir: "Open Long",
107
+ closedPnl: "0",
108
+ hash: "0xabc",
109
+ oid: 123,
110
+ crossed: false,
111
+ fee: "-0.08",
112
+ builderFee: "0.02",
113
+ tid: 456,
114
+ feeToken: "USDC",
115
+ twapId: null
116
+ }
117
+ ];
118
+ });
119
+ const syncer = createSyncer();
120
+ await expect(
121
+ syncer.listExternalFills({
122
+ since: /* @__PURE__ */ new Date(1699999e6),
123
+ instruments: [instrument],
124
+ openPositions: [],
125
+ provisionalFills: []
126
+ })
127
+ ).rejects.toThrow(
128
+ "Hyperliquid fill 1700000000000:BTC:456 has a negative net trade fee (-0.1); the backend external-fill contract does not accept Hyperliquid rebates."
129
+ );
130
+ });
131
+ it("warns when inclusive fill pagination cannot move past the current timestamp", async () => {
132
+ const consoleWarn = vi.spyOn(console, "warn").mockImplementation(() => void 0);
133
+ const event = {
134
+ coin: "BTC",
135
+ px: "100000",
136
+ sz: "0.1",
137
+ side: "B",
138
+ time: 1e3,
139
+ startPosition: "0",
140
+ dir: "Open Long",
141
+ closedPnl: "0",
142
+ hash: "0xabc",
143
+ oid: 123,
144
+ crossed: true,
145
+ fee: "1",
146
+ builderFee: "0.2",
147
+ tid: 456,
148
+ feeToken: "USDC",
149
+ twapId: null
150
+ };
151
+ fetchInfoMock.mockImplementation(
152
+ async (body) => body.type === "userFillsByTime" ? Array.from({ length: 2e3 }, () => event) : []
153
+ );
154
+ const syncer = createSyncer();
155
+ await syncer.listExternalFills({
156
+ since: /* @__PURE__ */ new Date(1e3),
157
+ instruments: [instrument],
158
+ openPositions: [],
159
+ provisionalFills: []
160
+ });
161
+ expect(consoleWarn).toHaveBeenCalledWith(
162
+ "Hyperliquid fills pagination stalled at timestamp 1000; returned history may be incomplete."
163
+ );
164
+ expect(fetchInfoMock).toHaveBeenCalledTimes(2);
165
+ consoleWarn.mockRestore();
166
+ });
167
+ it("counts raw fills before rejecting history older than the 10,000-fill retention boundary", async () => {
168
+ let page = 0;
169
+ fetchInfoMock.mockImplementation(async (body) => {
170
+ if (body.type !== "userFillsByTime") return [];
171
+ const firstTime = 1e3 + page * 2e3;
172
+ page += 1;
173
+ return Array.from({ length: 2e3 }, (_, index) => rawFill(firstTime + index));
174
+ });
175
+ const syncer = createSyncer();
176
+ await expect(
177
+ syncer.listExternalFills({
178
+ since: /* @__PURE__ */ new Date(0),
179
+ instruments: [instrument],
180
+ openPositions: [],
181
+ provisionalFills: []
182
+ })
183
+ ).rejects.toThrow(
184
+ "Hyperliquid fill history is incomplete: requested since 0 predates oldest reachable fill 1000 within the 10000-fill retention limit."
185
+ );
186
+ expect(fetchInfoMock).toHaveBeenCalledTimes(5);
187
+ expect(fetchInfoMock).toHaveBeenCalledWith(expect.objectContaining({ aggregateByTime: false }), true);
188
+ });
189
+ it("accepts a request exactly at the oldest reachable fill at the retention boundary", async () => {
190
+ let page = 0;
191
+ fetchInfoMock.mockImplementation(async (body) => {
192
+ if (body.type === "userTwapSliceFillsByTime") return [];
193
+ if (page >= 5) return [];
194
+ const firstTime = 1e3 + page * 2e3;
195
+ page += 1;
196
+ return Array.from({ length: 2e3 }, (_, index) => rawFill(firstTime + index));
197
+ });
198
+ const syncer = createSyncer();
199
+ const fills = await syncer.listExternalFills({
200
+ since: /* @__PURE__ */ new Date(1e3),
201
+ instruments: [instrument],
202
+ openPositions: [],
203
+ provisionalFills: []
204
+ });
205
+ expect(fills).toHaveLength(1e4);
206
+ expect(fetchInfoMock).toHaveBeenCalledTimes(7);
207
+ });
208
+ });
209
+ describe("HyperliquidAccountSyncer position and funding snapshots", () => {
210
+ beforeEach(() => {
211
+ fetchInfoMock.mockReset();
212
+ });
213
+ it("de-duplicates DEX descriptors and derives mark price from current position value", async () => {
214
+ fetchInfoMock.mockImplementation(async (body) => {
215
+ if (body.type === "perpDexs") return [null, null, { name: "xyz" }];
216
+ if (body.type === "clearinghouseState" && body.dex === "") {
217
+ return {
218
+ assetPositions: [
219
+ {
220
+ position: {
221
+ coin: "BTC",
222
+ szi: "2",
223
+ entryPx: "150",
224
+ positionValue: "400"
225
+ }
226
+ }
227
+ ]
228
+ };
229
+ }
230
+ if (body.type === "clearinghouseState") return { assetPositions: [] };
231
+ throw new Error(`Unexpected request: ${body.type}`);
232
+ });
233
+ const syncer = createSyncer();
234
+ const positions = await syncer.listExternalPositions({
235
+ instruments: [
236
+ {
237
+ ...instrument,
238
+ precision: { quantity: { contractSize: 1 } }
239
+ }
240
+ ]
241
+ });
242
+ expect(positions).toEqual([
243
+ expect.objectContaining({ symbol: "HL-BTC", size: "2", entryPrice: "150", markPrice: "200" })
244
+ ]);
245
+ expect(fetchInfoMock).toHaveBeenCalledTimes(3);
246
+ });
247
+ it("de-duplicates an inclusive funding boundary and stops when its cursor cannot advance", async () => {
248
+ const consoleWarn = vi.spyOn(console, "warn").mockImplementation(() => void 0);
249
+ const event = {
250
+ time: 1e3,
251
+ hash: "0xabc",
252
+ delta: {
253
+ type: "funding",
254
+ coin: "BTC",
255
+ usdc: "-2",
256
+ szi: "4",
257
+ fundingRate: "0.0001",
258
+ nSamples: null
259
+ }
260
+ };
261
+ fetchInfoMock.mockResolvedValue(Array.from({ length: 500 }, () => event));
262
+ const syncer = createSyncer();
263
+ const payments = await syncer.listExternalFunding({
264
+ since: /* @__PURE__ */ new Date(1e3),
265
+ instruments: [instrument]
266
+ });
267
+ expect(payments).toHaveLength(1);
268
+ expect(fetchInfoMock).toHaveBeenCalledTimes(1);
269
+ expect(consoleWarn).toHaveBeenCalledWith(
270
+ "Hyperliquid funding pagination stalled at timestamp 1000; returned history may be incomplete."
271
+ );
272
+ consoleWarn.mockRestore();
273
+ });
274
+ });
@@ -0,0 +1,25 @@
1
+ import BigNumber from 'bignumber.js';
2
+
3
+ function toFundingPaymentEntries(events, instrumentIds) {
4
+ return events.reduce((entries, event) => {
5
+ const instrumentId = event.market_id.toString();
6
+ if (!instrumentIds.has(instrumentId)) {
7
+ console.warn(`Lighter market id ${event.market_id} not found in instrument id map.`);
8
+ return entries;
9
+ }
10
+ const unsignedSize = new BigNumber(event.position_size).abs();
11
+ const signedSize = event.position_side === "short" ? unsignedSize.negated() : unsignedSize;
12
+ const fundingPerUnit = signedSize.isZero() ? "0" : new BigNumber(event.change).div(signedSize).toFixed();
13
+ entries.push({
14
+ connector: "lighter",
15
+ instrumentId,
16
+ fundingRate: event.rate,
17
+ notional: event.change,
18
+ fundingPerUnit,
19
+ timestamp: event.timestamp
20
+ });
21
+ return entries;
22
+ }, []);
23
+ }
24
+
25
+ export { toFundingPaymentEntries };
@@ -0,0 +1,48 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { toFundingPaymentEntries } from './funding';
3
+
4
+ describe("Lighter funding mapping", () => {
5
+ it("derives funding per unit against signed long and short exposure", () => {
6
+ const result = toFundingPaymentEntries(
7
+ [
8
+ fundingEvent({ funding_id: 1, position_side: "long", change: "-2" }),
9
+ fundingEvent({ funding_id: 2, position_side: "short", change: "2" })
10
+ ],
11
+ /* @__PURE__ */ new Set(["1"])
12
+ );
13
+ expect(result).toEqual([
14
+ {
15
+ connector: "lighter",
16
+ instrumentId: "1",
17
+ fundingRate: "0.0001",
18
+ notional: "-2",
19
+ fundingPerUnit: "-0.5",
20
+ timestamp: 1e3
21
+ },
22
+ {
23
+ connector: "lighter",
24
+ instrumentId: "1",
25
+ fundingRate: "0.0001",
26
+ notional: "2",
27
+ fundingPerUnit: "-0.5",
28
+ timestamp: 1e3
29
+ }
30
+ ]);
31
+ });
32
+ it("skips funding for unknown instruments", () => {
33
+ expect(toFundingPaymentEntries([fundingEvent({})], /* @__PURE__ */ new Set())).toEqual([]);
34
+ });
35
+ });
36
+ function fundingEvent(overrides) {
37
+ return {
38
+ timestamp: 1e3,
39
+ market_id: 1,
40
+ funding_id: 1,
41
+ change: "-2",
42
+ discount: "0",
43
+ rate: "0.0001",
44
+ position_size: "4",
45
+ position_side: "long",
46
+ ...overrides
47
+ };
48
+ }
@@ -1,6 +1,7 @@
1
1
  import BigNumber from 'bignumber.js';
2
- import { fetchTradesSince } from '../../client/lighter/rest';
2
+ import { fetchAccount, fetchTradesSince, fetchPositionFundingSince } from '../../client/lighter/rest';
3
3
  import { BaseAccountSyncer } from '../base';
4
+ import { toFundingPaymentEntries } from './funding';
4
5
 
5
6
  const LIGHTER_FEE_TICK_DENOMINATOR = new BigNumber(1e6);
6
7
  class LighterAccountSyncer extends BaseAccountSyncer {
@@ -9,84 +10,135 @@ class LighterAccountSyncer extends BaseAccountSyncer {
9
10
  apiPrivateKey;
10
11
  constructor(config) {
11
12
  super(config);
12
- const creds = config.credentials;
13
+ const credentials = config.credentials;
13
14
  this.accountIndex = Number(config.account.exchangeIdentifier);
14
- this.apiKeyIndex = creds.api_key_index;
15
- this.apiPrivateKey = creds.api_private_key;
15
+ this.apiKeyIndex = credentials.api_key_index;
16
+ this.apiPrivateKey = credentials.api_private_key;
17
+ }
18
+ async listExternalPositions({
19
+ instruments
20
+ }) {
21
+ const contractSizeByInstrumentId = new Map(
22
+ instruments.map((instrument) => [instrument.id, instrument.precision.quantity.contractSize])
23
+ );
24
+ const response = await fetchAccount(this.accountIndex, this.demo);
25
+ const rawPositions = response.accounts?.[0]?.positions ?? [];
26
+ return rawPositions.reduce((positions, rawPosition) => {
27
+ const instrumentId = rawPosition.market_id.toString();
28
+ const signedSize = new BigNumber(rawPosition.position).times(rawPosition.sign);
29
+ if (signedSize.isZero()) return positions;
30
+ const contractSize = contractSizeByInstrumentId.get(instrumentId) ?? 1;
31
+ const size = signedSize.abs().times(contractSize).toFixed();
32
+ const side = rawPosition.sign === 1 ? "BUY" : "SELL";
33
+ const markPrice = new BigNumber(rawPosition.position_value).abs().div(size).toFixed();
34
+ positions.push({
35
+ symbol: instrumentId,
36
+ size,
37
+ side,
38
+ entryPrice: rawPosition.avg_entry_price,
39
+ markPrice
40
+ });
41
+ return positions;
42
+ }, []);
16
43
  }
17
44
  async listExternalFills({
18
45
  since,
19
46
  instruments
20
47
  }) {
48
+ this.assertAuthenticatedSyncAvailable();
21
49
  const { token } = await this.sdk.core.misc.lighter.createAuthToken({
22
50
  accountIndex: this.accountIndex,
23
51
  apiKeyIndex: this.apiKeyIndex,
24
52
  apiPrivateKey: this.apiPrivateKey
25
53
  });
26
- const trades = await fetchTradesSince(this.accountIndex, token, since);
54
+ const trades = await fetchTradesSince(this.accountIndex, token, since, this.demo);
27
55
  const instrumentIds = new Set(instruments.map((instrument) => instrument.id));
28
- return trades.toSorted((a, b) => a.timestamp - b.timestamp).reduce((acc, trade) => {
29
- const instrumentId = trade.market_id.toString();
56
+ return trades.toSorted((a, b) => a.timestamp - b.timestamp).reduce((fills, rawFill) => {
57
+ const instrumentId = rawFill.market_id.toString();
30
58
  if (!instrumentIds.has(instrumentId)) {
31
- console.warn(`Lighter market id ${trade.market_id} not found in instrument id map.`);
32
- return acc;
59
+ console.warn(`Lighter market id ${rawFill.market_id} not found in instrument id map.`);
60
+ return fills;
33
61
  }
34
- const accountSide = this.getAccountSide(trade);
35
- if (!accountSide) return acc;
62
+ const accountSide = this.getAccountSide(rawFill);
63
+ if (!accountSide) return fills;
36
64
  const side = accountSide === "ask" ? "SELL" : "BUY";
37
- acc.push({
65
+ fills.push({
38
66
  connector: "lighter",
39
- exchangeFillId: trade.trade_id_str ?? trade.trade_id.toString(),
40
- cloid: this.getClientOrderId(trade, accountSide),
67
+ exchangeFillId: rawFill.trade_id_str ?? rawFill.trade_id.toString(),
68
+ cloid: this.getClientOrderId(rawFill, accountSide),
41
69
  symbol: instrumentId,
42
- price: trade.price,
43
- quantity: trade.size,
70
+ price: rawFill.price,
71
+ quantity: rawFill.size,
44
72
  side,
45
- reduceOnly: this.isReduceOnly(trade),
46
- timestamp: trade.timestamp,
47
- tradeFee: this.getTradeFee(trade, accountSide),
48
- builderFee: this.getIntegratorFee(trade, accountSide),
49
- liquidation: trade.type === "liquidation" ? { mark: trade.price } : null
73
+ reduceOnly: this.isReduceOnly(rawFill),
74
+ timestamp: rawFill.timestamp,
75
+ tradeFee: this.getTradeFee(rawFill, accountSide),
76
+ builderFee: this.getIntegratorFee(rawFill, accountSide),
77
+ liquidation: rawFill.type === "liquidation" ? { mark: rawFill.price } : null
50
78
  });
51
- return acc;
79
+ return fills;
52
80
  }, []);
53
81
  }
54
- isReduceOnly(trade) {
55
- if (trade.type === "liquidation" || trade.type === "deleverage" || trade.type === "market-settlement") {
82
+ async listExternalFunding({
83
+ since,
84
+ instruments
85
+ }) {
86
+ this.assertAuthenticatedSyncAvailable();
87
+ const { token } = await this.sdk.core.misc.lighter.createAuthToken({
88
+ accountIndex: this.accountIndex,
89
+ apiKeyIndex: this.apiKeyIndex,
90
+ apiPrivateKey: this.apiPrivateKey
91
+ });
92
+ const fundingEvents = await fetchPositionFundingSince(this.accountIndex, token, since, this.demo);
93
+ const instrumentIds = new Set(instruments.map((instrument) => instrument.id));
94
+ return toFundingPaymentEntries(fundingEvents, instrumentIds);
95
+ }
96
+ isReduceOnly(rawFill) {
97
+ if (rawFill.type === "liquidation" || rawFill.type === "deleverage" || rawFill.type === "market-settlement") {
56
98
  return true;
57
99
  }
58
- const accountSide = this.getAccountSide(trade);
100
+ const accountSide = this.getAccountSide(rawFill);
59
101
  if (!accountSide) {
60
102
  return false;
61
103
  }
62
- const positionSizeBefore = this.getPositionSizeBefore(trade, accountSide);
104
+ const positionSignChanged = this.isMaker(rawFill, accountSide) ? rawFill.maker_position_sign_changed : rawFill.taker_position_sign_changed;
105
+ if (positionSignChanged) {
106
+ return false;
107
+ }
108
+ const positionSizeBefore = this.getPositionSizeBefore(rawFill, accountSide);
63
109
  if (positionSizeBefore.isZero()) {
64
110
  return false;
65
111
  }
66
112
  return positionSizeBefore.isPositive() && accountSide === "ask" || positionSizeBefore.isNegative() && accountSide === "bid";
67
113
  }
68
- getAccountSide(trade) {
69
- if (trade.ask_account_id === this.accountIndex) return "ask";
70
- if (trade.bid_account_id === this.accountIndex) return "bid";
114
+ getAccountSide(rawFill) {
115
+ if (rawFill.ask_account_id === this.accountIndex) return "ask";
116
+ if (rawFill.bid_account_id === this.accountIndex) return "bid";
71
117
  return null;
72
118
  }
73
- getClientOrderId(trade, side) {
74
- return (side === "ask" ? trade.ask_client_id_str : trade.bid_client_id_str) || null;
119
+ getClientOrderId(rawFill, side) {
120
+ return (side === "ask" ? rawFill.ask_client_id_str : rawFill.bid_client_id_str) || null;
75
121
  }
76
- isMaker(trade, side) {
77
- return side === "ask" ? trade.is_maker_ask : !trade.is_maker_ask;
122
+ isMaker(rawFill, side) {
123
+ return side === "ask" ? rawFill.is_maker_ask : !rawFill.is_maker_ask;
78
124
  }
79
- getPositionSizeBefore(trade, side) {
80
- const value = this.isMaker(trade, side) ? trade.maker_position_size_before : trade.taker_position_size_before;
125
+ getPositionSizeBefore(rawFill, side) {
126
+ const value = this.isMaker(rawFill, side) ? rawFill.maker_position_size_before : rawFill.taker_position_size_before;
81
127
  return new BigNumber(value ?? 0);
82
128
  }
83
- getTradeFee(trade, side) {
84
- const rateTick = this.isMaker(trade, side) ? trade.maker_fee : trade.taker_fee;
85
- return this.feeTickToAmount(rateTick, trade.usd_amount);
129
+ getTradeFee(rawFill, side) {
130
+ const rateTick = this.isMaker(rawFill, side) ? rawFill.maker_fee : rawFill.taker_fee;
131
+ return this.feeTickToAmount(rateTick, rawFill.usd_amount);
86
132
  }
87
- getIntegratorFee(trade, side) {
88
- const rateTick = this.isMaker(trade, side) ? trade.integrator_maker_fee : trade.integrator_taker_fee;
89
- return this.feeTickToAmount(rateTick, trade.usd_amount);
133
+ getIntegratorFee(rawFill, side) {
134
+ const rateTick = this.isMaker(rawFill, side) ? rawFill.integrator_maker_fee : rawFill.integrator_taker_fee;
135
+ return this.feeTickToAmount(rateTick, rawFill.usd_amount);
136
+ }
137
+ /** Prevents testnet reads from silently minting a mainnet auth token. */
138
+ assertAuthenticatedSyncAvailable() {
139
+ if (this.demo) {
140
+ throw new Error("Lighter demo authenticated sync is unavailable because backend signing is mainnet-only");
141
+ }
90
142
  }
91
143
  // Lighter fees come as ppm rates (÷1e6), not amounts — the charged fee is that rate times the trade's USD notional.
92
144
  feeTickToAmount(rateTick, usdAmount) {