@pear-protocol/exchanges-sdk 0.6.4 → 0.6.6

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 (92) hide show
  1. package/dist/client/base-ws.js +15 -6
  2. package/dist/client/binance/exchange-ws.js +22 -8
  3. package/dist/client/binance/rest.js +40 -27
  4. package/dist/client/binance/ws.js +21 -2
  5. package/dist/client/bybit/exchange-ws.js +13 -6
  6. package/dist/client/bybit/rest.js +30 -36
  7. package/dist/client/bybit/ws.js +16 -2
  8. package/dist/client/exchange-ws.js +5 -0
  9. package/dist/client/hmac.js +9 -1
  10. package/dist/client/hyperliquid/exchange-ws.js +13 -5
  11. package/dist/client/hyperliquid/rest.js +37 -7
  12. package/dist/client/hyperliquid/ws.js +10 -4
  13. package/dist/client/index.js +9 -2
  14. package/dist/client/lighter/constants.js +4 -0
  15. package/dist/client/lighter/exchange-ws.js +13 -5
  16. package/dist/client/lighter/rest.js +153 -18
  17. package/dist/client/lighter/ws.js +10 -4
  18. package/dist/client/okx/exchange-ws.js +13 -6
  19. package/dist/client/okx/rest.js +27 -24
  20. package/dist/client/okx/ws.js +5 -0
  21. package/dist/helper/factory.js +3 -0
  22. package/dist/helper/hyperliquid/helper.js +18 -23
  23. package/dist/helper/lighter/auth-token.helper.js +68 -0
  24. package/dist/helper/lighter/cctp-deposit.helper.js +297 -0
  25. package/dist/helper/lighter/ethereum/abi.js +55 -11
  26. package/dist/helper/lighter/ethereum/index.js +22 -3
  27. package/dist/helper/lighter/ethereum/usdc.helper.js +77 -0
  28. package/dist/helper/lighter/helper.js +112 -37
  29. package/dist/helper/lighter/index.js +1 -0
  30. package/dist/helper/lighter/onboarding.helper.js +28 -0
  31. package/dist/helper/lighter/types.js +6 -1
  32. package/dist/helper/subaccount.helper.js +20 -0
  33. package/dist/index.d.ts +200 -1
  34. package/dist/sdk.js +18 -7
  35. package/dist/status/base.js +16 -3
  36. package/dist/status/binance/status.js +3 -0
  37. package/dist/status/bybit/status.js +3 -0
  38. package/dist/status/factory.js +3 -0
  39. package/dist/status/hyperliquid/status.js +8 -6
  40. package/dist/status/lighter/status.js +11 -3
  41. package/dist/status/okx/status.js +3 -0
  42. package/dist/syncer/base.js +79 -24
  43. package/dist/syncer/binance/syncer.js +14 -13
  44. package/dist/syncer/bybit/syncer.js +10 -6
  45. package/dist/syncer/factory.js +3 -0
  46. package/dist/syncer/hyperliquid/funding.js +3 -0
  47. package/dist/syncer/hyperliquid/syncer.js +52 -66
  48. package/dist/syncer/lighter/funding.js +3 -0
  49. package/dist/syncer/lighter/syncer.js +26 -25
  50. package/dist/syncer/okx/syncer.js +15 -13
  51. package/dist/tracker/base.js +15 -5
  52. package/dist/tracker/binance/mapper.js +5 -1
  53. package/dist/tracker/binance/tracker.js +63 -23
  54. package/dist/tracker/binance/types.js +2 -2
  55. package/dist/tracker/bybit/mapper.js +7 -1
  56. package/dist/tracker/bybit/tracker.js +68 -24
  57. package/dist/tracker/bybit/types.js +2 -2
  58. package/dist/tracker/hyperliquid/const.js +3 -1
  59. package/dist/tracker/hyperliquid/mapper.js +36 -20
  60. package/dist/tracker/hyperliquid/tracker.js +147 -54
  61. package/dist/tracker/hyperliquid/types.js +2 -2
  62. package/dist/tracker/index.js +3 -0
  63. package/dist/tracker/lighter/mapper.js +41 -31
  64. package/dist/tracker/lighter/tracker.js +68 -27
  65. package/dist/tracker/lighter/types.js +4 -4
  66. package/dist/tracker/okx/mapper.js +8 -0
  67. package/dist/tracker/okx/tracker.js +69 -18
  68. package/dist/tracker/okx/types.js +2 -2
  69. package/dist/tracker/utils.js +4 -0
  70. package/package.json +4 -4
  71. package/dist/client/hyperliquid/client.test.js +0 -41
  72. package/dist/client/lighter/constants.test.js +0 -22
  73. package/dist/client/lighter/rest.test.js +0 -158
  74. package/dist/helper/hyperliquid/helper.test.js +0 -72
  75. package/dist/helper/lighter/helper.test.js +0 -289
  76. package/dist/sdk.test.js +0 -117
  77. package/dist/status/hyperliquid/status.test.js +0 -94
  78. package/dist/status/lighter/status.test.js +0 -86
  79. package/dist/syncer/base.test.js +0 -148
  80. package/dist/syncer/hyperliquid/funding.test.js +0 -63
  81. package/dist/syncer/hyperliquid/syncer.test.js +0 -271
  82. package/dist/syncer/lighter/funding.test.js +0 -48
  83. package/dist/syncer/lighter/syncer.test.js +0 -274
  84. package/dist/tracker/base.test.js +0 -132
  85. package/dist/tracker/binance/tracker.test.js +0 -138
  86. package/dist/tracker/bybit/tracker.test.js +0 -145
  87. package/dist/tracker/hyperliquid/mapper.test.js +0 -260
  88. package/dist/tracker/hyperliquid/tracker.test.js +0 -597
  89. package/dist/tracker/lighter/mapper.test.js +0 -199
  90. package/dist/tracker/lighter/tracker.test.js +0 -515
  91. package/dist/tracker/okx/tracker.test.js +0 -126
  92. package/dist/tracker/utils.test.js +0 -8
@@ -1,132 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { BaseAccountTracker } from './base';
3
- import { HyperliquidAccountType } from './hyperliquid/types';
4
-
5
- class FakeAccountTracker extends BaseAccountTracker {
6
- connected = false;
7
- startCalls = 0;
8
- cleanupCalls = 0;
9
- assetSnapshotCalls = [];
10
- async start() {
11
- this.startCalls++;
12
- this.connected = true;
13
- }
14
- async snapshot() {
15
- return { balance: null, positions: [] };
16
- }
17
- async fetchAssetSnapshots(coins) {
18
- this.assetSnapshotCalls.push(coins);
19
- return Object.fromEntries(coins.map((coin) => [coin, { coin, leverage: "1", marginType: "cross" }]));
20
- }
21
- get isConnected() {
22
- return this.connected;
23
- }
24
- updatePositions(positions, mode = "merge") {
25
- this.onStateUpdate({ positions, positionsMode: mode });
26
- }
27
- updateBalance(balance) {
28
- this.onStateUpdate({ balance });
29
- }
30
- updateTrackedAsset(asset, info) {
31
- this.onStateUpdate({ trackedAssets: /* @__PURE__ */ new Map([[asset, info]]) });
32
- }
33
- ensureAssetTracked() {
34
- }
35
- releaseAsset() {
36
- }
37
- async cleanupConnections() {
38
- this.cleanupCalls++;
39
- this.connected = false;
40
- }
41
- }
42
- function buildConfig() {
43
- return { demo: true, credentials: { api_key: "key", api_secret: "secret", api_pass: "pass" } };
44
- }
45
- describe("BaseAccountTracker", () => {
46
- it("constructs from credentials + demo with no socket side-effect", () => {
47
- const tracker = new FakeAccountTracker(buildConfig());
48
- expect(tracker.isConnected).toBe(false);
49
- expect(tracker.isInitialized).toBe(false);
50
- expect(tracker.startCalls).toBe(0);
51
- });
52
- it("exposes snapshot() on the AccountTracker interface", () => {
53
- const tracker = new FakeAccountTracker(buildConfig());
54
- expect(typeof tracker.snapshot).toBe("function");
55
- });
56
- it("snapshotAssets() dedupes requested coins and skips the read entirely for an empty list", async () => {
57
- const tracker = new FakeAccountTracker(buildConfig());
58
- await expect(tracker.snapshotAssets([])).resolves.toEqual({});
59
- expect(tracker.assetSnapshotCalls).toEqual([]);
60
- const result = await tracker.snapshotAssets(["BTC", "ETH", "BTC"]);
61
- expect(tracker.assetSnapshotCalls).toEqual([["BTC", "ETH"]]);
62
- expect(Object.keys(result)).toEqual(["BTC", "ETH"]);
63
- });
64
- it("start() opens a connection and disconnect() tears down whatever start() created", async () => {
65
- const tracker = new FakeAccountTracker(buildConfig());
66
- await tracker.start();
67
- expect(tracker.isConnected).toBe(true);
68
- expect(tracker.cleanupCalls).toBe(0);
69
- await tracker.disconnect();
70
- expect(tracker.isConnected).toBe(false);
71
- expect(tracker.cleanupCalls).toBe(1);
72
- });
73
- it("distinguishes incremental position deltas from authoritative snapshots", () => {
74
- const tracker = new FakeAccountTracker(buildConfig());
75
- const btc = {
76
- symbol: "BTC",
77
- side: "long",
78
- size: "1",
79
- entryPrice: "100",
80
- unrealizedPnl: "0",
81
- leverage: "2",
82
- marginType: "cross",
83
- liquidationPrice: null
84
- };
85
- const eth = { ...btc, symbol: "ETH" };
86
- tracker.updatePositions(/* @__PURE__ */ new Map([["BTC:long", btc]]));
87
- tracker.updatePositions(/* @__PURE__ */ new Map([["ETH:long", eth]]));
88
- expect(tracker.getPositions()).toEqual([btc, eth]);
89
- tracker.updatePositions(/* @__PURE__ */ new Map([["ETH:long", eth]]), "replace");
90
- expect(tracker.getPositions()).toEqual([eth]);
91
- tracker.updatePositions(/* @__PURE__ */ new Map(), "replace");
92
- expect(tracker.getPositions()).toEqual([]);
93
- });
94
- it("clears and broadcasts a balance that is no longer safe to represent", () => {
95
- const tracker = new FakeAccountTracker(buildConfig());
96
- const balance = {
97
- accountType: HyperliquidAccountType.DEFAULT,
98
- free: { USDC: "100" },
99
- used: { USDC: "0" },
100
- total: { USDC: "100" },
101
- availableToTrade: "100",
102
- totalEquity: "100",
103
- walletBalance: "100",
104
- unrealizedPnl: "0",
105
- initialMarginUsed: "0",
106
- maintenanceMargin: "0",
107
- marginRatio: "0.0000",
108
- assets: [],
109
- timestamp: 1
110
- };
111
- const updates = [];
112
- tracker.trackBalance((update) => updates.push(update));
113
- tracker.updateBalance(balance);
114
- tracker.updateBalance(null);
115
- expect(tracker.getBalance()).toBeNull();
116
- expect(updates).toEqual([balance, null]);
117
- });
118
- it("replays an explicitly cleared balance to listeners registered later", () => {
119
- const tracker = new FakeAccountTracker(buildConfig());
120
- const updates = [];
121
- tracker.updateBalance(null);
122
- tracker.trackBalance((update) => updates.push(update));
123
- expect(tracker.isInitialized).toBe(true);
124
- expect(updates).toEqual([null]);
125
- });
126
- it("does not mark account state initialized from asset metadata alone", () => {
127
- const tracker = new FakeAccountTracker(buildConfig());
128
- tracker.updateTrackedAsset("BTC", { coin: "BTC", leverage: "2", marginType: "cross" });
129
- expect(tracker.getTrackedAsset("BTC")).toEqual({ coin: "BTC", leverage: "2", marginType: "cross" });
130
- expect(tracker.isInitialized).toBe(false);
131
- });
132
- });
@@ -1,138 +0,0 @@
1
- import { vi, describe, beforeEach, it, expect } from 'vitest';
2
- import { BinanceAccountTracker } from './tracker';
3
-
4
- const {
5
- fetchAccountStatusMock,
6
- fetchAllPositionRiskMock,
7
- fetchMultiAssetsMarginMock,
8
- fetchPositionRiskMock,
9
- keepAliveListenKeyMock
10
- } = vi.hoisted(() => ({
11
- fetchAccountStatusMock: vi.fn(),
12
- fetchAllPositionRiskMock: vi.fn(),
13
- fetchMultiAssetsMarginMock: vi.fn(),
14
- fetchPositionRiskMock: vi.fn(),
15
- keepAliveListenKeyMock: vi.fn()
16
- }));
17
- vi.mock("../../client/binance/rest", () => ({
18
- fetchAccountStatus: fetchAccountStatusMock,
19
- fetchAllPositionRisk: fetchAllPositionRiskMock,
20
- fetchMultiAssetsMargin: fetchMultiAssetsMarginMock,
21
- fetchPositionRisk: fetchPositionRiskMock,
22
- keepAliveListenKey: keepAliveListenKeyMock
23
- }));
24
- const creds = {
25
- read: { api_key: "rk", api_secret: "rs" },
26
- write: { api_key: "wk", api_secret: "ws" }
27
- };
28
- function buildTracker() {
29
- return new BinanceAccountTracker({ demo: true, credentials: creds });
30
- }
31
- describe("BinanceAccountTracker.snapshot", () => {
32
- beforeEach(() => {
33
- fetchAccountStatusMock.mockReset();
34
- fetchAllPositionRiskMock.mockReset();
35
- fetchMultiAssetsMarginMock.mockReset();
36
- });
37
- it("splices accountType onto the omit-shaped mapBalance() result", async () => {
38
- fetchAccountStatusMock.mockResolvedValue({
39
- totalWalletBalance: "5000",
40
- totalMarginBalance: "5000",
41
- availableBalance: "4500",
42
- totalUnrealizedProfit: "25",
43
- totalInitialMargin: "200",
44
- totalMaintMargin: "50",
45
- assets: [
46
- {
47
- asset: "USDT",
48
- walletBalance: "5000",
49
- availableBalance: "4500",
50
- unrealizedProfit: "25",
51
- marginBalance: "5000",
52
- initialMargin: "200",
53
- maintMargin: "50",
54
- marginAvailable: true
55
- }
56
- ]
57
- });
58
- fetchAllPositionRiskMock.mockResolvedValue([
59
- {
60
- symbol: "BTCUSDT",
61
- leverage: "10",
62
- marginType: "cross",
63
- isAutoAddMargin: "false",
64
- isolatedMargin: "0",
65
- positionAmt: "0.5",
66
- entryPrice: "50000",
67
- breakEvenPrice: "50000",
68
- unRealizedProfit: "25",
69
- liquidationPrice: "40000",
70
- markPrice: "51000",
71
- maxNotionalValue: "1000000",
72
- positionSide: "BOTH",
73
- notional: "25500",
74
- isolatedWallet: "0",
75
- updateTime: 0
76
- }
77
- ]);
78
- fetchMultiAssetsMarginMock.mockResolvedValue(true);
79
- const snapshot = await buildTracker().snapshot();
80
- expect(snapshot.balance).toMatchObject({
81
- accountType: "MULTI_ASSET",
82
- totalEquity: "5000",
83
- availableToTrade: "4500",
84
- maintenanceMargin: "50",
85
- marginRatio: "0.0100"
86
- });
87
- expect(snapshot.positions).toHaveLength(1);
88
- expect(snapshot.positions[0]).toMatchObject({
89
- symbol: "BTCUSDT",
90
- side: "long",
91
- leverage: "10",
92
- marginType: "cross",
93
- liquidationPrice: "40000"
94
- });
95
- });
96
- it("reports SINGLE_ASSET accountType when multi-assets margin is off", async () => {
97
- fetchAccountStatusMock.mockResolvedValue({
98
- totalWalletBalance: "0",
99
- totalMarginBalance: "0",
100
- availableBalance: "0",
101
- totalUnrealizedProfit: "0",
102
- totalInitialMargin: "0",
103
- totalMaintMargin: "0",
104
- assets: []
105
- });
106
- fetchAllPositionRiskMock.mockResolvedValue([]);
107
- fetchMultiAssetsMarginMock.mockResolvedValue(false);
108
- const snapshot = await buildTracker().snapshot();
109
- expect(snapshot.balance?.accountType).toBe("SINGLE_ASSET");
110
- expect(snapshot.positions).toEqual([]);
111
- });
112
- });
113
- describe("BinanceAccountTracker.snapshotAssets", () => {
114
- beforeEach(() => {
115
- fetchAllPositionRiskMock.mockReset();
116
- });
117
- it("answers any number of symbols from one all-symbols positionRisk call", async () => {
118
- fetchAllPositionRiskMock.mockResolvedValue([
119
- { symbol: "BTCUSDT", leverage: "10", marginType: "cross" },
120
- { symbol: "ETHUSDT", leverage: "5", marginType: "isolated" }
121
- ]);
122
- const result = await buildTracker().snapshotAssets(["BTCUSDT", "ETHUSDT", "DOGEUSDT"]);
123
- expect(fetchAllPositionRiskMock).toHaveBeenCalledTimes(1);
124
- expect(result).toEqual({
125
- BTCUSDT: { coin: "BTCUSDT", leverage: "10", marginType: "cross" },
126
- ETHUSDT: { coin: "ETHUSDT", leverage: "5", marginType: "isolated" },
127
- DOGEUSDT: null
128
- });
129
- });
130
- it("collapses hedge-mode LONG/SHORT rows into one entry per symbol", async () => {
131
- fetchAllPositionRiskMock.mockResolvedValue([
132
- { symbol: "BTCUSDT", leverage: "10", marginType: "cross", positionSide: "LONG" },
133
- { symbol: "BTCUSDT", leverage: "10", marginType: "cross", positionSide: "SHORT" }
134
- ]);
135
- const result = await buildTracker().snapshotAssets(["BTCUSDT"]);
136
- expect(result.BTCUSDT).toEqual({ coin: "BTCUSDT", leverage: "10", marginType: "cross" });
137
- });
138
- });
@@ -1,145 +0,0 @@
1
- import { vi, describe, beforeEach, it, expect } from 'vitest';
2
- import { BybitAccountTracker } from './tracker';
3
-
4
- const { fetchWalletBalanceMock, fetchPositionsMock, fetchAccountInfoMock, fetchPositionLeverageMock } = vi.hoisted(
5
- () => ({
6
- fetchWalletBalanceMock: vi.fn(),
7
- fetchPositionsMock: vi.fn(),
8
- fetchAccountInfoMock: vi.fn(),
9
- fetchPositionLeverageMock: vi.fn()
10
- })
11
- );
12
- vi.mock("../../client/bybit/rest", () => ({
13
- fetchWalletBalance: fetchWalletBalanceMock,
14
- fetchPositions: fetchPositionsMock,
15
- fetchAccountInfo: fetchAccountInfoMock,
16
- fetchPositionLeverage: fetchPositionLeverageMock
17
- }));
18
- const creds = { api_key: "k", api_secret: "s" };
19
- function buildTracker() {
20
- return new BybitAccountTracker({ demo: true, credentials: creds });
21
- }
22
- describe("BybitAccountTracker.snapshot", () => {
23
- beforeEach(() => {
24
- fetchWalletBalanceMock.mockReset();
25
- fetchPositionsMock.mockReset();
26
- fetchAccountInfoMock.mockReset();
27
- });
28
- it("returns mapped balance + positions across USDT and USDC settle coins with no socket opened", async () => {
29
- fetchWalletBalanceMock.mockResolvedValue({
30
- totalEquity: "2000",
31
- totalWalletBalance: "2000",
32
- totalMarginBalance: "2000",
33
- totalAvailableBalance: "1800",
34
- totalPerpUPL: "50",
35
- totalInitialMargin: "100",
36
- totalMaintenanceMargin: "20",
37
- accountMMRate: "0.0100",
38
- coin: [
39
- {
40
- coin: "USDT",
41
- equity: "2000",
42
- usdValue: "2000",
43
- walletBalance: "2000",
44
- unrealisedPnl: "50",
45
- totalPositionIM: "100",
46
- totalPositionMM: "20",
47
- totalOrderIM: "0",
48
- locked: "0",
49
- borrowAmount: "0",
50
- marginCollateral: true,
51
- collateralSwitch: true
52
- }
53
- ]
54
- });
55
- fetchPositionsMock.mockImplementation(async (_creds, settleCoin) => {
56
- if (settleCoin === "USDT") {
57
- return [
58
- {
59
- symbol: "BTCUSDT",
60
- side: "Buy",
61
- size: "1",
62
- avgPrice: "50000",
63
- unrealisedPnl: "10",
64
- leverage: "5",
65
- tradeMode: "0",
66
- liqPrice: "40000"
67
- }
68
- ];
69
- }
70
- return [];
71
- });
72
- fetchAccountInfoMock.mockResolvedValue({ marginMode: "REGULAR_MARGIN", updatedTime: "0" });
73
- const snapshot = await buildTracker().snapshot();
74
- expect(snapshot.balance).toMatchObject({
75
- accountType: "REGULAR_MARGIN",
76
- totalEquity: "2000",
77
- availableToTrade: "1800",
78
- maintenanceMargin: "20",
79
- marginRatio: "0.0100"
80
- });
81
- expect(snapshot.positions).toHaveLength(1);
82
- expect(snapshot.positions[0]).toMatchObject({
83
- symbol: "BTCUSDT",
84
- side: "long",
85
- leverage: "5",
86
- marginType: "cross",
87
- liquidationPrice: "40000"
88
- });
89
- expect(fetchPositionsMock).toHaveBeenCalledWith(creds, "USDT", true);
90
- expect(fetchPositionsMock).toHaveBeenCalledWith(creds, "USDC", true);
91
- expect(fetchPositionLeverageMock).not.toHaveBeenCalled();
92
- });
93
- it("returns a null balance when the wallet fetch is empty", async () => {
94
- fetchWalletBalanceMock.mockResolvedValue(void 0);
95
- fetchPositionsMock.mockResolvedValue([]);
96
- fetchAccountInfoMock.mockResolvedValue({ marginMode: "REGULAR_MARGIN", updatedTime: "0" });
97
- const snapshot = await buildTracker().snapshot();
98
- expect(snapshot.balance).toBeNull();
99
- expect(snapshot.positions).toEqual([]);
100
- });
101
- });
102
- describe("BybitAccountTracker.snapshotAssets", () => {
103
- beforeEach(() => {
104
- fetchPositionsMock.mockReset();
105
- fetchPositionLeverageMock.mockReset();
106
- });
107
- it(">2 symbols: resolves positioned symbols from the two settleCoin reads and fills in the rest per symbol", async () => {
108
- fetchPositionsMock.mockImplementation(
109
- async (_creds, settleCoin) => settleCoin === "USDT" ? [
110
- { symbol: "BTCUSDT", size: "1", leverage: "10", tradeMode: "0" },
111
- { symbol: "ETHUSDT", size: "2", leverage: "5", tradeMode: "1" }
112
- ] : []
113
- );
114
- fetchPositionLeverageMock.mockResolvedValue({ symbol: "DOGEUSDT", size: "0", leverage: "3", tradeMode: "0" });
115
- const result = await buildTracker().snapshotAssets(["BTCUSDT", "ETHUSDT", "DOGEUSDT"]);
116
- expect(fetchPositionsMock).toHaveBeenCalledTimes(2);
117
- expect(fetchPositionLeverageMock).toHaveBeenCalledTimes(1);
118
- expect(fetchPositionLeverageMock).toHaveBeenCalledWith(creds, "DOGEUSDT", true);
119
- expect(result).toEqual({
120
- BTCUSDT: { coin: "BTCUSDT", leverage: "10", marginType: "cross" },
121
- ETHUSDT: { coin: "ETHUSDT", leverage: "5", marginType: "isolated" },
122
- DOGEUSDT: { coin: "DOGEUSDT", leverage: "3", marginType: "cross" }
123
- });
124
- });
125
- it("\u22642 symbols: skips the settleCoin batch and reads per symbol directly", async () => {
126
- fetchPositionLeverageMock.mockResolvedValue({ symbol: "BTCUSDT", size: "0", leverage: "10", tradeMode: "0" });
127
- const result = await buildTracker().snapshotAssets(["BTCUSDT"]);
128
- expect(fetchPositionsMock).not.toHaveBeenCalled();
129
- expect(fetchPositionLeverageMock).toHaveBeenCalledTimes(1);
130
- expect(result.BTCUSDT).toEqual({ coin: "BTCUSDT", leverage: "10", marginType: "cross" });
131
- });
132
- it("maps a failed fill-in to null while the others succeed", async () => {
133
- fetchPositionLeverageMock.mockImplementation(async (_creds, symbol) => {
134
- if (symbol === "NOPEUSDT") throw new Error("10001");
135
- return { symbol, size: "0", leverage: "10", tradeMode: "0" };
136
- });
137
- const result = await buildTracker().snapshotAssets(["BTCUSDT", "NOPEUSDT"]);
138
- expect(result.BTCUSDT).toMatchObject({ leverage: "10", marginType: "cross" });
139
- expect(result.NOPEUSDT).toBeNull();
140
- });
141
- it("throws when every per-symbol read fails", async () => {
142
- fetchPositionLeverageMock.mockRejectedValue(new Error("down"));
143
- await expect(buildTracker().snapshotAssets(["BTCUSDT", "ETHUSDT"])).rejects.toThrow("every position read failed");
144
- });
145
- });
@@ -1,260 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { aggregateClearinghouseStates, mapBalance } from './mapper';
3
- import { HyperliquidAccountType } from './types';
4
-
5
- function buildState(overrides) {
6
- const leverage = overrides.leverage === "isolated" ? { type: "isolated", value: 10, rawUsd: overrides.marginUsed ?? "0" } : { type: "cross", value: 10 };
7
- return {
8
- marginSummary: {
9
- accountValue: overrides.accountValue,
10
- totalNtlPos: "0",
11
- totalRawUsd: overrides.accountValue,
12
- totalMarginUsed: overrides.totalMarginUsed
13
- },
14
- crossMarginSummary: {
15
- accountValue: overrides.crossAccountValue ?? overrides.accountValue,
16
- totalNtlPos: "0",
17
- totalRawUsd: overrides.crossAccountValue ?? overrides.accountValue,
18
- totalMarginUsed: overrides.totalMarginUsed
19
- },
20
- crossMaintenanceMarginUsed: overrides.crossMaintenanceMarginUsed,
21
- withdrawable: overrides.withdrawable,
22
- assetPositions: [
23
- {
24
- type: "oneWay",
25
- position: {
26
- coin: overrides.coin,
27
- szi: "1",
28
- leverage,
29
- entryPx: "1000",
30
- positionValue: overrides.positionValue ?? "1000",
31
- unrealizedPnl: overrides.unrealizedPnl,
32
- returnOnEquity: "0.1",
33
- liquidationPx: "800",
34
- marginUsed: overrides.marginUsed ?? "100",
35
- maxLeverage: overrides.maxLeverage ?? 10,
36
- cumFunding: { allTime: "0", sinceOpen: "0", sinceChange: "0" }
37
- }
38
- }
39
- ],
40
- time: 0
41
- };
42
- }
43
- function dexState(state, collateralToken, dex = "", meta = null) {
44
- return {
45
- dex,
46
- collateralToken,
47
- meta,
48
- state
49
- };
50
- }
51
- describe("Hyperliquid balance mapping", () => {
52
- it("uses the maximum independent cross and isolated risk ratios for Standard accounts", () => {
53
- const state = buildState({
54
- accountValue: "120",
55
- crossAccountValue: "80",
56
- totalMarginUsed: "40",
57
- crossMaintenanceMarginUsed: "5",
58
- withdrawable: "70",
59
- coin: "BTC",
60
- unrealizedPnl: "20",
61
- leverage: "isolated",
62
- positionValue: "1000",
63
- maxLeverage: 10
64
- });
65
- const { summary, assetPositions } = aggregateClearinghouseStates([dexState(state, 0)]);
66
- const balance = mapBalance(summary, /* @__PURE__ */ new Map(), HyperliquidAccountType.DEFAULT);
67
- expect(summary.accountValue).toBe("120");
68
- expect(summary.isolatedMaintenanceMarginUsed).toBe("50");
69
- expect(assetPositions).toHaveLength(1);
70
- expect(balance).toMatchObject({
71
- accountType: "default",
72
- totalEquity: "120",
73
- walletBalance: "100",
74
- initialMarginUsed: "40",
75
- maintenanceMargin: "55",
76
- marginRatio: "0.5000",
77
- availableToTrade: "70"
78
- });
79
- });
80
- it("applies tiered maintenance rates and continuity deductions to isolated positions", () => {
81
- const state = buildState({
82
- accountValue: "5000",
83
- totalMarginUsed: "100",
84
- crossMaintenanceMarginUsed: "0",
85
- withdrawable: "4900",
86
- coin: "BTC",
87
- unrealizedPnl: "0",
88
- leverage: "isolated",
89
- positionValue: "2000",
90
- maxLeverage: 50
91
- });
92
- const meta = {
93
- collateralToken: 0,
94
- universe: [{ name: "BTC", szDecimals: 5, maxLeverage: 50, marginTableId: 1 }],
95
- marginTables: [
96
- [
97
- 1,
98
- {
99
- description: "tiered",
100
- marginTiers: [
101
- { lowerBound: "0", maxLeverage: 50 },
102
- { lowerBound: "1000", maxLeverage: 25 }
103
- ]
104
- }
105
- ]
106
- ]
107
- };
108
- const { summary } = aggregateClearinghouseStates([dexState(state, 0, "", meta)]);
109
- expect(summary.isolatedMaintenanceMarginUsed).toBe("30");
110
- });
111
- it("uses the maximum per-DEX Standard margin ratio instead of a blended aggregate ratio", () => {
112
- const higherRiskState = buildState({
113
- accountValue: "100",
114
- totalMarginUsed: "20",
115
- crossMaintenanceMarginUsed: "10",
116
- withdrawable: "80",
117
- coin: "BTC",
118
- unrealizedPnl: "0"
119
- });
120
- const lowerRiskState = buildState({
121
- accountValue: "900",
122
- totalMarginUsed: "90",
123
- crossMaintenanceMarginUsed: "45",
124
- withdrawable: "810",
125
- coin: "xyz:ETH",
126
- unrealizedPnl: "0"
127
- });
128
- const { summary } = aggregateClearinghouseStates([
129
- dexState(higherRiskState, 0),
130
- dexState(lowerRiskState, 360, "xyz")
131
- ]);
132
- const balance = mapBalance(summary, /* @__PURE__ */ new Map(), HyperliquidAccountType.DEFAULT);
133
- expect(balance).toMatchObject({
134
- totalEquity: "1000",
135
- maintenanceMargin: "55",
136
- marginRatio: "0.1000"
137
- });
138
- });
139
- it("maps network-specific collateral token indexes through spot metadata", () => {
140
- const state = buildState({
141
- accountValue: "100",
142
- totalMarginUsed: "10",
143
- crossMaintenanceMarginUsed: "5",
144
- withdrawable: "90",
145
- coin: "test:ETH",
146
- unrealizedPnl: "0"
147
- });
148
- const { summary } = aggregateClearinghouseStates([dexState(state, 1452, "test")]);
149
- const balance = mapBalance(summary, /* @__PURE__ */ new Map(), HyperliquidAccountType.DEFAULT, /* @__PURE__ */ new Map([[1452, "USDH"]]));
150
- expect(balance).toMatchObject({
151
- free: { USDH: "90" },
152
- used: { USDH: "10" },
153
- total: { USDH: "100" },
154
- assets: [{ asset: "USDH" }]
155
- });
156
- });
157
- it("reports at least full utilization when Standard maintenance is positive without collateral", () => {
158
- const state = buildState({
159
- accountValue: "0",
160
- crossAccountValue: "0",
161
- totalMarginUsed: "10",
162
- crossMaintenanceMarginUsed: "5",
163
- withdrawable: "0",
164
- coin: "BTC",
165
- unrealizedPnl: "-100"
166
- });
167
- const { summary } = aggregateClearinghouseStates([dexState(state, 0)]);
168
- expect(mapBalance(summary, /* @__PURE__ */ new Map(), HyperliquidAccountType.DEFAULT)?.marginRatio).toBe("1.0000");
169
- });
170
- it("uses authoritative spot holds and post-maintenance availability for Unified accounts", () => {
171
- const usdcState = buildState({
172
- accountValue: "999",
173
- totalMarginUsed: "30",
174
- crossMaintenanceMarginUsed: "10",
175
- withdrawable: "999",
176
- coin: "BTC",
177
- unrealizedPnl: "5",
178
- leverage: "isolated",
179
- marginUsed: "20",
180
- positionValue: "0"
181
- });
182
- const usdhState = buildState({
183
- accountValue: "999",
184
- totalMarginUsed: "40",
185
- crossMaintenanceMarginUsed: "20",
186
- withdrawable: "999",
187
- coin: "xyz:ETH",
188
- unrealizedPnl: "0"
189
- });
190
- const { summary } = aggregateClearinghouseStates([dexState(usdcState, 0), dexState(usdhState, 360, "xyz")]);
191
- const spotBalances = /* @__PURE__ */ new Map([
192
- ["USDC", { coin: "USDC", token: 0, total: "100", hold: "5", availableAfterMaintenance: "70" }],
193
- ["USDH", { coin: "USDH", token: 360, total: "400", hold: "10", availableAfterMaintenance: "380" }]
194
- ]);
195
- const balance = mapBalance(summary, spotBalances, HyperliquidAccountType.UNIFIED_ACCOUNT);
196
- expect(balance).toMatchObject({
197
- accountType: "unifiedAccount",
198
- walletBalance: "495",
199
- totalEquity: "500",
200
- availableToTrade: "450",
201
- marginRatio: "0.1250"
202
- });
203
- expect(balance?.free).toEqual({ USDC: "95", USDH: "390" });
204
- expect(balance?.used).toEqual({ USDC: "5", USDH: "10" });
205
- expect(balance?.assets).toEqual([
206
- expect.objectContaining({ asset: "USDC", walletBalance: "95", total: "100" }),
207
- expect.objectContaining({ asset: "USDH", walletBalance: "400", total: "400" })
208
- ]);
209
- });
210
- it("reports at least full utilization when Unified cross maintenance has no available collateral", () => {
211
- const state = buildState({
212
- accountValue: "0",
213
- totalMarginUsed: "10",
214
- crossMaintenanceMarginUsed: "5",
215
- withdrawable: "0",
216
- coin: "BTC",
217
- unrealizedPnl: "-100"
218
- });
219
- const { summary } = aggregateClearinghouseStates([dexState(state, 0)]);
220
- const spotBalances = /* @__PURE__ */ new Map([["USDC", { coin: "USDC", token: 0, total: "0", hold: "0" }]]);
221
- expect(mapBalance(summary, spotBalances, HyperliquidAccountType.UNIFIED_ACCOUNT)?.marginRatio).toBe("1.0000");
222
- });
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
- coin: "BTC",
230
- unrealizedPnl: "5"
231
- });
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
- ]);
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 });
244
- });
245
- it("returns a zeroed aggregate for an empty DEX list", () => {
246
- const { summary, assetPositions } = aggregateClearinghouseStates([]);
247
- expect(summary).toMatchObject({
248
- accountValue: "0",
249
- totalMarginUsed: "0",
250
- totalRawUsd: "0",
251
- crossMaintenanceMarginUsed: "0",
252
- isolatedMaintenanceMarginUsed: "0",
253
- maximumStandardMarginRatio: "0.0000",
254
- withdrawable: "0",
255
- totalUnrealizedPnl: "0"
256
- });
257
- expect(summary.byCollateralToken.size).toBe(0);
258
- expect(assetPositions).toEqual([]);
259
- });
260
- });