@pear-protocol/exchanges-sdk 0.5.0 → 0.5.2

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.
@@ -112,12 +112,13 @@ async function fetchPositionFundingSince(accountIndex, authToken, since, demo =
112
112
  const results = /* @__PURE__ */ new Map();
113
113
  const seenCursors = /* @__PURE__ */ new Set();
114
114
  let cursor;
115
+ const sinceSeconds = Math.max(1, Math.floor(since.getTime() / 1e3));
115
116
  do {
116
117
  const params = new URLSearchParams({
117
118
  account_index: accountIndex.toString(),
118
119
  limit: "100",
119
120
  side: "all",
120
- start_timestamp: since.getTime().toString(),
121
+ start_timestamp: sinceSeconds.toString(),
121
122
  ...cursor ? { cursor } : {}
122
123
  });
123
124
  const response = await proxyFetch(`${getLighterBaseUrl(demo)}/api/v1/positionFunding?${params.toString()}`, {
@@ -125,7 +126,7 @@ async function fetchPositionFundingSince(accountIndex, authToken, since, demo =
125
126
  });
126
127
  const data = await readLighterResponse(response, "/positionFunding");
127
128
  for (const funding of data.position_fundings ?? []) {
128
- if (funding.timestamp >= since.getTime()) {
129
+ if (funding.timestamp >= sinceSeconds) {
129
130
  results.set(`${funding.market_id}:${funding.funding_id}`, funding);
130
131
  }
131
132
  }
@@ -138,12 +139,17 @@ async function fetchPositionFundingSince(accountIndex, authToken, since, demo =
138
139
  return Array.from(results.values());
139
140
  }
140
141
  async function readLighterResponse(response, endpoint, acceptedCodes = [200]) {
141
- if (!response.ok) {
142
+ let data;
143
+ try {
144
+ data = await response.json();
145
+ } catch {
146
+ data = void 0;
147
+ }
148
+ if (!data) {
142
149
  throw new Error(`Lighter ${endpoint} returned ${response.status}`);
143
150
  }
144
- const data = await response.json();
145
151
  if (!acceptedCodes.includes(data.code)) {
146
- throw new Error(`Lighter ${endpoint} returned ${data.code}: ${data.message ?? "unknown error"}`);
152
+ throw new Error(`Lighter ${endpoint} returned ${data.code}: ${data.message ?? `HTTP ${response.status}`}`);
147
153
  }
148
154
  return data;
149
155
  }
@@ -8,7 +8,7 @@ describe("Lighter REST client", () => {
8
8
  it("routes demo account reads to testnet and rejects exchange-level errors", async () => {
9
9
  const fetchMock = vi.fn().mockResolvedValue(
10
10
  new Response(JSON.stringify({ code: 20001, message: "account not found" }), {
11
- status: 200
11
+ status: 400
12
12
  })
13
13
  );
14
14
  vi.stubGlobal("fetch", fetchMock);
@@ -24,7 +24,7 @@ describe("Lighter REST client", () => {
24
24
  "fetch",
25
25
  vi.fn().mockResolvedValue(
26
26
  new Response(JSON.stringify({ code: 21100, message: "account not found" }), {
27
- status: 200
27
+ status: 400
28
28
  })
29
29
  )
30
30
  );
@@ -124,9 +124,15 @@ describe("Lighter REST client", () => {
124
124
  const rows = await fetchPositionFundingSince(42, "auth", /* @__PURE__ */ new Date(1e3), true);
125
125
  expect(rows.map((row) => row.funding_id)).toEqual([1, 2, 3]);
126
126
  expect(fetchMock).toHaveBeenCalledTimes(2);
127
- expect(String(fetchMock.mock.calls[0]?.[0])).toContain("start_timestamp=1000");
127
+ expect(String(fetchMock.mock.calls[0]?.[0])).toContain("start_timestamp=1");
128
128
  expect(String(fetchMock.mock.calls[1]?.[0])).toContain("cursor=page-2");
129
129
  });
130
+ it("floors an epoch watermark to start_timestamp=1 since Lighter rejects 0 in seconds", async () => {
131
+ const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({ code: 200, position_fundings: [] }), { status: 200 }));
132
+ vi.stubGlobal("fetch", fetchMock);
133
+ await fetchPositionFundingSince(42, "auth", /* @__PURE__ */ new Date(0), true);
134
+ expect(String(fetchMock.mock.calls[0]?.[0])).toContain("start_timestamp=1");
135
+ });
130
136
  it("stops trades pagination when the API repeats a cursor", async () => {
131
137
  const page = new Response(JSON.stringify({ code: 200, trades: [], next_cursor: "same-page" }), { status: 200 });
132
138
  const fetchMock = vi.fn().mockResolvedValueOnce(page).mockResolvedValueOnce(
package/dist/index.d.ts CHANGED
@@ -621,7 +621,6 @@ declare enum BybitAccountType {
621
621
 
622
622
  declare enum HyperliquidAccountType {
623
623
  DEFAULT = "default",
624
- DEX_ABSTRACTION = "dexAbstraction",
625
624
  DISABLED = "disabled",
626
625
  PORTFOLIO_MARGIN = "portfolioMargin",
627
626
  UNIFIED_ACCOUNT = "unifiedAccount"
@@ -14,7 +14,7 @@ async function checkHyperliquidAccountStatus(config) {
14
14
  const approvalAddress = metadata && "isSubaccount" in metadata && metadata.isSubaccount ? metadata.mainAccountAddress : config.account.exchangeIdentifier;
15
15
  try {
16
16
  const abstraction = await fetchUserAbstraction(config.account.exchangeIdentifier, config.demo);
17
- const supportedAccountMode = abstraction === HyperliquidAccountType.DEFAULT || abstraction === HyperliquidAccountType.DISABLED || abstraction === HyperliquidAccountType.UNIFIED_ACCOUNT;
17
+ const supportedAccountMode = abstraction === HyperliquidAccountType.DEFAULT || abstraction === HyperliquidAccountType.DISABLED || abstraction === HyperliquidAccountType.UNIFIED_ACCOUNT || abstraction === HyperliquidAccountType.PORTFOLIO_MARGIN;
18
18
  if (!supportedAccountMode) {
19
19
  reasons.push("hyperliquid_account_mode_unsupported");
20
20
  }
@@ -54,7 +54,7 @@ describe("checkHyperliquidAccountStatus", () => {
54
54
  expect(checkApiWalletMock).toHaveBeenCalledWith({ userAddress: mainAccountAddress, agentWalletAddress });
55
55
  expect(checkBuilderFeeApprovalMock).toHaveBeenCalledWith(mainAccountAddress, builderAddress, 10);
56
56
  });
57
- it.each(["portfolioMargin", "dexAbstraction", "somethingNew"])(
57
+ it.each(["dexAbstraction", "somethingNew"])(
58
58
  "warns when account mode %s cannot be represented safely",
59
59
  async (abstraction) => {
60
60
  fetchUserAbstractionMock.mockResolvedValue(abstraction);
@@ -65,6 +65,11 @@ describe("checkHyperliquidAccountStatus", () => {
65
65
  });
66
66
  }
67
67
  );
68
+ it("treats Portfolio Margin as a supported account mode", async () => {
69
+ fetchUserAbstractionMock.mockResolvedValue("portfolioMargin");
70
+ const status = await checkHyperliquidAccountStatus(statusConfig());
71
+ expect(status).toEqual({ status: "active", reasons: [] });
72
+ });
68
73
  it("distinguishes an account-mode API failure from an unsupported mode", async () => {
69
74
  fetchUserAbstractionMock.mockRejectedValue(new Error("503"));
70
75
  const status = await checkHyperliquidAccountStatus(statusConfig());
@@ -16,7 +16,8 @@ function toFundingPaymentEntries(events, instrumentIds) {
16
16
  fundingRate: event.rate,
17
17
  notional: event.change,
18
18
  fundingPerUnit,
19
- timestamp: event.timestamp
19
+ // Lighter reports funding timestamps in seconds; the payment contract expects ms.
20
+ timestamp: event.timestamp * 1e3
20
21
  });
21
22
  return entries;
22
23
  }, []);
@@ -17,7 +17,7 @@ describe("Lighter funding mapping", () => {
17
17
  fundingRate: "0.0001",
18
18
  notional: "-2",
19
19
  fundingPerUnit: "-0.5",
20
- timestamp: 1e3
20
+ timestamp: 1e6
21
21
  },
22
22
  {
23
23
  connector: "lighter",
@@ -25,7 +25,7 @@ describe("Lighter funding mapping", () => {
25
25
  fundingRate: "0.0001",
26
26
  notional: "2",
27
27
  fundingPerUnit: "-0.5",
28
- timestamp: 1e3
28
+ timestamp: 1e6
29
29
  }
30
30
  ]);
31
31
  });
@@ -214,7 +214,7 @@ describe("LighterAccountSyncer funding mapping", () => {
214
214
  fundingRate: "0.0001",
215
215
  notional: "-2",
216
216
  fundingPerUnit: "-0.5",
217
- timestamp: 1e3
217
+ timestamp: 1e6
218
218
  }
219
219
  ]);
220
220
  expect(fetchPositionFundingSince).toHaveBeenCalledWith(ACCOUNT_INDEX, "test-token", /* @__PURE__ */ new Date(1e3), false);
@@ -84,10 +84,8 @@ function mapBalance(summary, spotCoinBalances, accountType, collateralTokenMap =
84
84
  case HyperliquidAccountType.DISABLED:
85
85
  return mapStandardBalance(summary, accountType, collateralTokenMap);
86
86
  case HyperliquidAccountType.UNIFIED_ACCOUNT:
87
- return mapUnifiedBalance(summary, spotCoinBalances, accountType, collateralTokenMap);
88
- case HyperliquidAccountType.DEX_ABSTRACTION:
89
87
  case HyperliquidAccountType.PORTFOLIO_MARGIN:
90
- return null;
88
+ return mapUnifiedBalance(summary, spotCoinBalances, accountType, collateralTokenMap);
91
89
  }
92
90
  }
93
91
  function mapPositions(raw) {
@@ -220,22 +220,27 @@ describe("Hyperliquid balance mapping", () => {
220
220
  const spotBalances = /* @__PURE__ */ new Map([["USDC", { coin: "USDC", token: 0, total: "0", hold: "0" }]]);
221
221
  expect(mapBalance(summary, spotBalances, HyperliquidAccountType.UNIFIED_ACCOUNT)?.marginRatio).toBe("1.0000");
222
222
  });
223
- it("does not fabricate Portfolio Margin or deprecated DEX-abstraction account totals", () => {
224
- const sharedState = buildState({
225
- accountValue: "100",
226
- totalMarginUsed: "10",
227
- crossMaintenanceMarginUsed: "5",
228
- withdrawable: "90",
223
+ it("maps Portfolio Margin balances with the same calculation as Unified Account", () => {
224
+ const state = buildState({
225
+ accountValue: "999",
226
+ totalMarginUsed: "30",
227
+ crossMaintenanceMarginUsed: "10",
228
+ withdrawable: "999",
229
229
  coin: "BTC",
230
- unrealizedPnl: "0"
230
+ unrealizedPnl: "5"
231
231
  });
232
- const { summary } = aggregateClearinghouseStates([
233
- dexState(sharedState, 0),
234
- dexState(sharedState, 0, "builder-dex")
232
+ const { summary } = aggregateClearinghouseStates([dexState(state, 0)]);
233
+ const spotBalances = /* @__PURE__ */ new Map([
234
+ ["USDC", { coin: "USDC", token: 0, total: "100", hold: "5", availableAfterMaintenance: "70" }]
235
235
  ]);
236
- expect(summary.accountValue).toBe("200");
237
- expect(mapBalance(summary, /* @__PURE__ */ new Map(), HyperliquidAccountType.DEX_ABSTRACTION)).toBeNull();
238
- expect(mapBalance(summary, /* @__PURE__ */ new Map(), HyperliquidAccountType.PORTFOLIO_MARGIN)).toBeNull();
236
+ const portfolio = mapBalance(summary, spotBalances, HyperliquidAccountType.PORTFOLIO_MARGIN);
237
+ const unified = mapBalance(summary, spotBalances, HyperliquidAccountType.UNIFIED_ACCOUNT);
238
+ expect(portfolio).toMatchObject({
239
+ accountType: "portfolioMargin",
240
+ totalEquity: "100",
241
+ availableToTrade: "70"
242
+ });
243
+ expect({ ...portfolio, accountType: "unifiedAccount", timestamp: 0 }).toEqual({ ...unified, timestamp: 0 });
239
244
  });
240
245
  it("returns a zeroed aggregate for an empty DEX list", () => {
241
246
  const { summary, assetPositions } = aggregateClearinghouseStates([]);
@@ -281,13 +281,10 @@ class HyperliquidAccountTracker extends BaseAccountTracker {
281
281
  }
282
282
  }
283
283
  buildBalance() {
284
- if (this.accountType === HyperliquidAccountType.DEX_ABSTRACTION || this.accountType === HyperliquidAccountType.PORTFOLIO_MARGIN) {
285
- return null;
286
- }
287
284
  if (!this.hasClearinghouseState || !this.accountType) {
288
285
  return void 0;
289
286
  }
290
- if (this.accountType === HyperliquidAccountType.UNIFIED_ACCOUNT && !this.hasSpotState) {
287
+ if ((this.accountType === HyperliquidAccountType.UNIFIED_ACCOUNT || this.accountType === HyperliquidAccountType.PORTFOLIO_MARGIN) && !this.hasSpotState) {
291
288
  return void 0;
292
289
  }
293
290
  return mapBalance(this.lastSummary, this.spotCoinBalances, this.accountType, this.collateralTokenMap);
@@ -512,23 +512,33 @@ describe("HyperliquidAccountTracker live account-mode handling", () => {
512
512
  expect(updates).toHaveLength(2);
513
513
  expect(updates[1]).toBeNull();
514
514
  });
515
- it("broadcasts explicit null when a live mode becomes known unsupported", () => {
515
+ it("emits a live Portfolio Margin balance once spot state joins the clearinghouse state", () => {
516
516
  const tracker = buildTracker();
517
517
  const updates = [];
518
518
  tracker.trackBalance((balance) => updates.push(balance));
519
- const send = vi.fn();
520
519
  const internals = tracker;
521
- internals.accountType = HyperliquidAccountType.DEFAULT;
520
+ internals.accountType = HyperliquidAccountType.PORTFOLIO_MARGIN;
521
+ internals.hasSpotState = false;
522
522
  internals.hasClearinghouseState = false;
523
- internals.ws = { send };
524
523
  internals.onClearinghouseState({
525
524
  user: address,
526
525
  clearinghouseStates: [["", dexState("BTC", "100", "10")]]
527
526
  });
528
- internals.onWebData3(webData("portfolioMargin"));
529
527
  expect(tracker.getBalance()).toBeNull();
530
- expect(updates).toHaveLength(2);
531
- expect(updates[1]).toBeNull();
528
+ expect(updates).toEqual([]);
529
+ internals.onSpotState({
530
+ user: address,
531
+ spotState: {
532
+ balances: [{ coin: "USDC", token: 0, total: "100", hold: "5", entryNtl: "0" }],
533
+ tokenToAvailableAfterMaintenance: [[0, "90"]]
534
+ }
535
+ });
536
+ expect(tracker.getBalance()).toMatchObject({
537
+ accountType: "portfolioMargin",
538
+ walletBalance: "90",
539
+ availableToTrade: "90"
540
+ });
541
+ expect(updates).toHaveLength(1);
532
542
  });
533
543
  it("retains locked BTC active-asset data even without a user listener", () => {
534
544
  const tracker = buildTracker();
@@ -1,6 +1,5 @@
1
1
  var HyperliquidAccountType = /* @__PURE__ */ ((HyperliquidAccountType2) => {
2
2
  HyperliquidAccountType2["DEFAULT"] = "default";
3
- HyperliquidAccountType2["DEX_ABSTRACTION"] = "dexAbstraction";
4
3
  HyperliquidAccountType2["DISABLED"] = "disabled";
5
4
  HyperliquidAccountType2["PORTFOLIO_MARGIN"] = "portfolioMargin";
6
5
  HyperliquidAccountType2["UNIFIED_ACCOUNT"] = "unifiedAccount";
@@ -12,7 +12,6 @@ const ACCOUNT_TYPE_LABELS = {
12
12
  [BybitAccountType.PORTFOLIO_MARGIN]: "Portfolio Margin",
13
13
  [HyperliquidAccountType.DEFAULT]: "Standard",
14
14
  [HyperliquidAccountType.DISABLED]: "Standard",
15
- [HyperliquidAccountType.DEX_ABSTRACTION]: "Standard (Shared HIP-3 Balance)",
16
15
  [HyperliquidAccountType.PORTFOLIO_MARGIN]: "Portfolio Margin",
17
16
  [HyperliquidAccountType.UNIFIED_ACCOUNT]: "Unified Account",
18
17
  [LighterAccountType.CLASSIC]: "Classic",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pear-protocol/exchanges-sdk",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Pear Protocol Exchanges SDK",
5
5
  "private": false,
6
6
  "type": "module",
@@ -27,9 +27,9 @@
27
27
  "test:watch": "vitest"
28
28
  },
29
29
  "dependencies": {
30
- "@pear-protocol/core-sdk": "^1.7.0",
31
- "@pear-protocol/types": "^1.9.0",
32
- "@pear-protocol/utils": "^0.2.0",
30
+ "@pear-protocol/core-sdk": "^1.11.1",
31
+ "@pear-protocol/types": "^1.14.1",
32
+ "@pear-protocol/utils": "^0.3.0",
33
33
  "bignumber.js": "^11.1.1",
34
34
  "partysocket": "^1.0.3",
35
35
  "viem": "^2.48.8"