@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.
- package/dist/client/binance/rest.js +4 -3
- package/dist/client/bybit/rest.js +2 -1
- package/dist/client/hyperliquid/rest.js +3 -1
- package/dist/client/lighter/rest.js +28 -18
- package/dist/client/lighter/rest.test.js +11 -5
- package/dist/client/okx/rest.js +2 -1
- package/dist/index.d.ts +7 -1
- package/dist/sdk.js +2 -0
- package/dist/status/hyperliquid/status.js +1 -1
- package/dist/status/hyperliquid/status.test.js +6 -1
- package/dist/syncer/lighter/funding.js +2 -1
- package/dist/syncer/lighter/funding.test.js +2 -2
- package/dist/syncer/lighter/syncer.test.js +1 -1
- package/dist/tracker/hyperliquid/mapper.js +1 -3
- package/dist/tracker/hyperliquid/mapper.test.js +18 -13
- package/dist/tracker/hyperliquid/tracker.js +1 -4
- package/dist/tracker/hyperliquid/tracker.test.js +17 -7
- package/dist/tracker/hyperliquid/types.js +0 -1
- package/dist/tracker/types.js +0 -1
- package/package.json +4 -4
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
121
|
+
start_timestamp: sinceSeconds.toString(),
|
|
117
122
|
...cursor ? { cursor } : {}
|
|
118
123
|
});
|
|
119
|
-
const response = await
|
|
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 >=
|
|
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
|
-
|
|
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 ??
|
|
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:
|
|
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).
|
|
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:
|
|
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).
|
|
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=
|
|
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/client/okx/rest.js
CHANGED
|
@@ -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
|
|
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(["
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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("
|
|
224
|
-
const
|
|
225
|
-
accountValue: "
|
|
226
|
-
totalMarginUsed: "
|
|
227
|
-
crossMaintenanceMarginUsed: "
|
|
228
|
-
withdrawable: "
|
|
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: "
|
|
230
|
+
unrealizedPnl: "5"
|
|
231
231
|
});
|
|
232
|
-
const { summary } = aggregateClearinghouseStates([
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
expect(
|
|
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("
|
|
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.
|
|
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).
|
|
531
|
-
|
|
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";
|
package/dist/tracker/types.js
CHANGED
|
@@ -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.
|
|
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.
|
|
31
|
-
"@pear-protocol/types": "^1.
|
|
32
|
-
"@pear-protocol/utils": "^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"
|