@pear-protocol/exchanges-sdk 0.4.0 → 0.5.1

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.
@@ -1,3 +1,4 @@
1
+ import { proxyFetch } from '@pear-protocol/utils';
1
2
  import { hmacSha256Hex } from '../hmac';
2
3
 
3
4
  const BASE_URL = "https://fapi.binance.com";
@@ -8,7 +9,7 @@ async function signedGet(creds, path, params, demo) {
8
9
  const allParams = { ...params, timestamp, recvWindow: "5000" };
9
10
  const queryString = new URLSearchParams(allParams).toString();
10
11
  const signature = await hmacSha256Hex(creds.read.api_secret, queryString);
11
- const response = await fetch(`${baseUrl}${path}?${queryString}&signature=${signature}`, {
12
+ const response = await proxyFetch(`${baseUrl}${path}?${queryString}&signature=${signature}`, {
12
13
  headers: { "X-MBX-APIKEY": creds.read.api_key }
13
14
  });
14
15
  if (!response.ok) {
@@ -20,7 +21,7 @@ async function signedGet(creds, path, params, demo) {
20
21
  }
21
22
  async function createListenKey(apiKey, demo = false) {
22
23
  const baseUrl = demo ? BASE_URL_TESTNET : BASE_URL;
23
- const response = await fetch(`${baseUrl}/fapi/v1/listenKey`, {
24
+ const response = await proxyFetch(`${baseUrl}/fapi/v1/listenKey`, {
24
25
  method: "POST",
25
26
  headers: { "X-MBX-APIKEY": apiKey }
26
27
  });
@@ -32,7 +33,7 @@ async function createListenKey(apiKey, demo = false) {
32
33
  }
33
34
  async function keepAliveListenKey(apiKey, demo = false) {
34
35
  const baseUrl = demo ? BASE_URL_TESTNET : BASE_URL;
35
- const response = await fetch(`${baseUrl}/fapi/v1/listenKey`, {
36
+ const response = await proxyFetch(`${baseUrl}/fapi/v1/listenKey`, {
36
37
  method: "PUT",
37
38
  headers: { "X-MBX-APIKEY": apiKey }
38
39
  });
@@ -1,3 +1,4 @@
1
+ import { proxyFetch } from '@pear-protocol/utils';
1
2
  import { hmacSha256Hex } from '../hmac';
2
3
 
3
4
  const BASE_URL = "https://api.bybit.com";
@@ -9,7 +10,7 @@ async function signedGet(creds, path, params, demo) {
9
10
  const queryString = new URLSearchParams(params).toString();
10
11
  const signPayload = `${timestamp}${creds.api_key}${recvWindow}${queryString}`;
11
12
  const signature = await hmacSha256Hex(creds.api_secret, signPayload);
12
- const response = await fetch(`${baseUrl}${path}?${queryString}`, {
13
+ const response = await proxyFetch(`${baseUrl}${path}?${queryString}`, {
13
14
  headers: {
14
15
  "X-BAPI-API-KEY": creds.api_key,
15
16
  "X-BAPI-SIGN": signature,
@@ -1,10 +1,12 @@
1
+ import { proxyFetch } from '@pear-protocol/utils';
2
+
1
3
  const HL_INFO_URLS = {
2
4
  mainnet: "https://api.hyperliquid.xyz/info",
3
5
  testnet: "https://api.hyperliquid-testnet.xyz/info"
4
6
  };
5
7
  async function fetchInfo(body, demo = false) {
6
8
  const url = demo ? HL_INFO_URLS.testnet : HL_INFO_URLS.mainnet;
7
- const res = await fetch(url, {
9
+ const res = await proxyFetch(url, {
8
10
  method: "POST",
9
11
  headers: { "Content-Type": "application/json" },
10
12
  body: JSON.stringify(body)
@@ -1,8 +1,9 @@
1
+ import { proxyFetch } from '@pear-protocol/utils';
1
2
  import { getLighterBaseUrl } from './constants';
2
3
 
3
4
  async function fetchApiKeys(accountIndex, demo = false) {
4
5
  const url = `${getLighterBaseUrl(demo)}/api/v1/apikeys?account_index=${accountIndex}`;
5
- const response = await fetch(url);
6
+ const response = await proxyFetch(url);
6
7
  const data = await readLighterResponse(response, "/apikeys");
7
8
  return data.api_keys;
8
9
  }
@@ -16,7 +17,7 @@ async function fetchAccountsByL1Address(l1Address, demo = false) {
16
17
  l1_address: l1Address,
17
18
  ...cursor ? { cursor } : {}
18
19
  });
19
- const response = await fetch(`${getLighterBaseUrl(demo)}/api/v1/accountsByL1Address?${params.toString()}`);
20
+ const response = await proxyFetch(`${getLighterBaseUrl(demo)}/api/v1/accountsByL1Address?${params.toString()}`);
20
21
  const data = await readLighterResponse(response, "/accountsByL1Address", [200, 21100]);
21
22
  if (data.code === 21100) return data;
22
23
  message ??= data.message;
@@ -36,22 +37,25 @@ async function fetchAccountsByL1Address(l1Address, demo = false) {
36
37
  };
37
38
  }
38
39
  async function fetchAccount(accountIndex, demo = false) {
39
- const response = await fetch(`${getLighterBaseUrl(demo)}/api/v1/account?by=index&value=${accountIndex}`);
40
+ const response = await proxyFetch(`${getLighterBaseUrl(demo)}/api/v1/account?by=index&value=${accountIndex}`);
40
41
  return readLighterResponse(response, "/account");
41
42
  }
42
43
  async function fetchAccountLimits(accountIndex, authToken, demo = false) {
43
- const response = await fetch(`${getLighterBaseUrl(demo)}/api/v1/accountLimits?account_index=${accountIndex}`, {
44
+ const response = await proxyFetch(`${getLighterBaseUrl(demo)}/api/v1/accountLimits?account_index=${accountIndex}`, {
44
45
  headers: { authorization: authToken }
45
46
  });
46
47
  return readLighterResponse(response, "/accountLimits");
47
48
  }
48
49
  async function fetchAccountMetadata(accountIndex, authToken, demo = false) {
49
- const response = await fetch(`${getLighterBaseUrl(demo)}/api/v1/accountMetadata?by=index&value=${accountIndex}`, {
50
- headers: {
51
- accept: "application/json",
52
- authorization: authToken
50
+ const response = await proxyFetch(
51
+ `${getLighterBaseUrl(demo)}/api/v1/accountMetadata?by=index&value=${accountIndex}`,
52
+ {
53
+ headers: {
54
+ accept: "application/json",
55
+ authorization: authToken
56
+ }
53
57
  }
54
- });
58
+ );
55
59
  const data = await readLighterResponse(response, "/accountMetadata");
56
60
  const matchesAccount = data.account_metadatas?.some((metadata) => metadata.account_index === accountIndex);
57
61
  if (!matchesAccount) {
@@ -59,12 +63,12 @@ async function fetchAccountMetadata(accountIndex, authToken, demo = false) {
59
63
  }
60
64
  }
61
65
  async function fetchOrderBookDetails(demo = false) {
62
- const response = await fetch(`${getLighterBaseUrl(demo)}/api/v1/orderBookDetails?filter=perp`);
66
+ const response = await proxyFetch(`${getLighterBaseUrl(demo)}/api/v1/orderBookDetails?filter=perp`);
63
67
  const data = await readLighterResponse(response, "/orderBookDetails");
64
68
  return data.order_book_details;
65
69
  }
66
70
  async function fetchAssetDetails(demo) {
67
- const response = await fetch(`${getLighterBaseUrl(demo)}/api/v1/assetDetails`);
71
+ const response = await proxyFetch(`${getLighterBaseUrl(demo)}/api/v1/assetDetails`);
68
72
  const data = await readLighterResponse(response, "/assetDetails");
69
73
  return data.asset_details;
70
74
  }
@@ -85,7 +89,7 @@ async function fetchTradesSince(accountIndex, authToken, since, demo = false) {
85
89
  aggregate: "false",
86
90
  ...cursor ? { cursor } : {}
87
91
  });
88
- const response = await fetch(`${getLighterBaseUrl(demo)}/api/v1/trades?${params.toString()}`, {
92
+ const response = await proxyFetch(`${getLighterBaseUrl(demo)}/api/v1/trades?${params.toString()}`, {
89
93
  headers: { authorization: authToken }
90
94
  });
91
95
  const data = await readLighterResponse(response, "/trades");
@@ -108,20 +112,21 @@ async function fetchPositionFundingSince(accountIndex, authToken, since, demo =
108
112
  const results = /* @__PURE__ */ new Map();
109
113
  const seenCursors = /* @__PURE__ */ new Set();
110
114
  let cursor;
115
+ const sinceSeconds = Math.max(1, Math.floor(since.getTime() / 1e3));
111
116
  do {
112
117
  const params = new URLSearchParams({
113
118
  account_index: accountIndex.toString(),
114
119
  limit: "100",
115
120
  side: "all",
116
- start_timestamp: since.getTime().toString(),
121
+ start_timestamp: sinceSeconds.toString(),
117
122
  ...cursor ? { cursor } : {}
118
123
  });
119
- const response = await fetch(`${getLighterBaseUrl(demo)}/api/v1/positionFunding?${params.toString()}`, {
124
+ const response = await proxyFetch(`${getLighterBaseUrl(demo)}/api/v1/positionFunding?${params.toString()}`, {
120
125
  headers: { authorization: authToken }
121
126
  });
122
127
  const data = await readLighterResponse(response, "/positionFunding");
123
128
  for (const funding of data.position_fundings ?? []) {
124
- if (funding.timestamp >= since.getTime()) {
129
+ if (funding.timestamp >= sinceSeconds) {
125
130
  results.set(`${funding.market_id}:${funding.funding_id}`, funding);
126
131
  }
127
132
  }
@@ -134,12 +139,17 @@ async function fetchPositionFundingSince(accountIndex, authToken, since, demo =
134
139
  return Array.from(results.values());
135
140
  }
136
141
  async function readLighterResponse(response, endpoint, acceptedCodes = [200]) {
137
- if (!response.ok) {
142
+ let data;
143
+ try {
144
+ data = await response.json();
145
+ } catch {
146
+ data = void 0;
147
+ }
148
+ if (!data) {
138
149
  throw new Error(`Lighter ${endpoint} returned ${response.status}`);
139
150
  }
140
- const data = await response.json();
141
151
  if (!acceptedCodes.includes(data.code)) {
142
- 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}`}`);
143
153
  }
144
154
  return data;
145
155
  }
@@ -8,12 +8,12 @@ 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);
15
15
  await expect(fetchAccount(42, true)).rejects.toThrow("Lighter /account returned 20001: account not found");
16
- expect(fetchMock).toHaveBeenCalledWith("https://testnet.zklighter.elliot.ai/api/v1/account?by=index&value=42");
16
+ expect(fetchMock.mock.calls[0]?.[0]).toBe("https://testnet.zklighter.elliot.ai/api/v1/account?by=index&value=42");
17
17
  });
18
18
  it("rejects HTTP failures before reading endpoint data", async () => {
19
19
  vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response("unavailable", { status: 503 })));
@@ -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
  );
@@ -98,7 +98,7 @@ describe("Lighter REST client", () => {
98
98
  await expect(fetchAssetDetails(false)).resolves.toEqual([
99
99
  { asset_id: 1, symbol: "ETH", l1_decimals: 18, decimals: 8 }
100
100
  ]);
101
- expect(fetchMock).toHaveBeenCalledWith("https://mainnet.zklighter.elliot.ai/api/v1/assetDetails");
101
+ expect(fetchMock.mock.calls[0]?.[0]).toBe("https://mainnet.zklighter.elliot.ai/api/v1/assetDetails");
102
102
  });
103
103
  it("paginates and de-duplicates funding rows by market and funding id", async () => {
104
104
  const duplicate = fundingRow({ funding_id: 2, timestamp: 2e3 });
@@ -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(
@@ -1,3 +1,4 @@
1
+ import { proxyFetch } from '@pear-protocol/utils';
1
2
  import { hmacSha256Base64 } from '../hmac';
2
3
 
3
4
  const REST_BASE_URL = "https://www.okx.com";
@@ -17,7 +18,7 @@ async function signedGet(creds, path, params, demo) {
17
18
  if (demo) {
18
19
  headers["x-simulated-trading"] = "1";
19
20
  }
20
- const response = await fetch(`${REST_BASE_URL}${fullPath}`, { headers });
21
+ const response = await proxyFetch(`${REST_BASE_URL}${fullPath}`, { headers });
21
22
  if (!response.ok) {
22
23
  throw new Error(`OKX API error: ${response.status} ${response.statusText}`);
23
24
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { HttpTypes, Connector, BinanceCredentialsDTO, BybitCredentialsDTO, OkxCredentialsDTO, TradeAccount } from '@pear-protocol/types';
2
2
  import PearSDK from '@pear-protocol/core-sdk';
3
+ import { ProxyConfig } from '@pear-protocol/utils';
3
4
  import { WebSocket } from 'partysocket';
4
5
  import { WalletClient, Transport, Chain, Account } from 'viem';
5
6
 
@@ -8,6 +9,12 @@ type ExchangesSDKConfig = {
8
9
  sdk: PearSDK;
9
10
  demo?: boolean;
10
11
  options?: ConnectOptions;
12
+ /**
13
+ * Optional forward-proxy for the exchange REST calls. When enabled, requests are re-pointed at
14
+ * `proxy.url` with the original venue host in `X-Proxy-Target`. Injected here because the SDK
15
+ * also runs in the browser, where env vars are unavailable.
16
+ */
17
+ proxy?: ProxyConfig;
11
18
  };
12
19
  type AccountHealthStatus = 'active' | 'warning';
13
20
  type AccountWarningReason = 'lighter_api_key_invalid' | 'lighter_integrator_not_approved' | 'hyperliquid_api_wallet_invalid' | 'hyperliquid_builder_fee_not_approved' | 'hyperliquid_account_mode_check_failed' | 'hyperliquid_account_mode_unsupported' | 'api_key_invalid';
@@ -614,7 +621,6 @@ declare enum BybitAccountType {
614
621
 
615
622
  declare enum HyperliquidAccountType {
616
623
  DEFAULT = "default",
617
- DEX_ABSTRACTION = "dexAbstraction",
618
624
  DISABLED = "disabled",
619
625
  PORTFOLIO_MARGIN = "portfolioMargin",
620
626
  UNIFIED_ACCOUNT = "unifiedAccount"
package/dist/sdk.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { configureProxy } from '@pear-protocol/utils';
1
2
  import './helper';
2
3
  import { createExchangeHelper } from './helper/factory';
3
4
  import { checkAccountStatus } from './status';
@@ -12,6 +13,7 @@ class ExchangesSDK {
12
13
  this.sdk = config.sdk;
13
14
  this.demo = config.demo ?? false;
14
15
  this.options = config.options;
16
+ if (config.proxy) configureProxy(config.proxy);
15
17
  }
16
18
  async connect(tradeAccountId) {
17
19
  return this.fetchCredentials(tradeAccountId);
@@ -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.4.0",
3
+ "version": "0.5.1",
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.5.0",
31
- "@pear-protocol/types": "^1.6.0",
32
- "@pear-protocol/utils": "^0.1.0",
30
+ "@pear-protocol/core-sdk": "^1.7.0",
31
+ "@pear-protocol/types": "^1.9.0",
32
+ "@pear-protocol/utils": "^0.2.0",
33
33
  "bignumber.js": "^11.1.1",
34
34
  "partysocket": "^1.0.3",
35
35
  "viem": "^2.48.8"