@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,41 +0,0 @@
1
- import { describe, afterEach, vi, it, expect } from 'vitest';
2
- import { fetchInfo } from './rest';
3
- import { HyperliquidWs } from './ws';
4
-
5
- class InspectableHyperliquidWs extends HyperliquidWs {
6
- /** Exposes the selected URL for a transport regression assertion. */
7
- url() {
8
- return this.getWsUrl();
9
- }
10
- /** Exposes heartbeat cadence for a transport regression assertion. */
11
- pingInterval() {
12
- return this.getPingIntervalMs();
13
- }
14
- /** Exposes the heartbeat payload for a transport regression assertion. */
15
- pingMessage() {
16
- return this.buildPingMessage();
17
- }
18
- }
19
- describe("Hyperliquid network clients", () => {
20
- afterEach(() => {
21
- vi.unstubAllGlobals();
22
- });
23
- it.each([
24
- [false, "https://api.hyperliquid.xyz/info"],
25
- [true, "https://api.hyperliquid-testnet.xyz/info"]
26
- ])("routes info requests to the selected network", async (demo, expectedUrl) => {
27
- const fetchMock = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ ok: true }) });
28
- vi.stubGlobal("fetch", fetchMock);
29
- await fetchInfo({ type: "perpDexs" }, demo);
30
- expect(fetchMock).toHaveBeenCalledWith(expectedUrl, expect.objectContaining({ method: "POST" }));
31
- });
32
- it("uses testnet websocket routing and Hyperliquid heartbeats in demo mode", () => {
33
- const ws = new InspectableHyperliquidWs(true);
34
- expect(ws.url()).toBe("wss://api.hyperliquid-testnet.xyz/ws");
35
- expect(ws.pingInterval()).toBe(3e4);
36
- expect(ws.pingMessage()).toEqual({ method: "ping" });
37
- });
38
- it("keeps no-argument websocket construction on mainnet", () => {
39
- expect(new InspectableHyperliquidWs().url()).toBe("wss://api.hyperliquid.xyz/ws");
40
- });
41
- });
@@ -1,22 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { getLighterBaseUrl, getLighterWsUrl } from './constants';
3
- import { LighterWs } from './ws';
4
-
5
- class InspectableLighterWs extends LighterWs {
6
- url() {
7
- return this.getWsUrl();
8
- }
9
- }
10
- describe("Lighter network endpoints", () => {
11
- it("routes demo traffic to testnet", () => {
12
- expect(getLighterBaseUrl(true)).toBe("https://testnet.zklighter.elliot.ai");
13
- expect(getLighterWsUrl(true)).toBe("wss://testnet.zklighter.elliot.ai/stream");
14
- });
15
- it("routes non-demo traffic to mainnet", () => {
16
- expect(getLighterBaseUrl(false)).toBe("https://mainnet.zklighter.elliot.ai");
17
- expect(getLighterWsUrl(false)).toBe("wss://mainnet.zklighter.elliot.ai/stream");
18
- });
19
- it("keeps no-argument websocket construction on mainnet", () => {
20
- expect(new InspectableLighterWs().url()).toBe("wss://mainnet.zklighter.elliot.ai/stream");
21
- });
22
- });
@@ -1,158 +0,0 @@
1
- import { afterEach, vi, describe, it, expect } from 'vitest';
2
- import { fetchAccount, fetchAssetDetails, fetchAccountsByL1Address, fetchAccountLimits, fetchPositionFundingSince, fetchTradesSince } from './rest';
3
-
4
- afterEach(() => {
5
- vi.unstubAllGlobals();
6
- });
7
- describe("Lighter REST client", () => {
8
- it("routes demo account reads to testnet and rejects exchange-level errors", async () => {
9
- const fetchMock = vi.fn().mockResolvedValue(
10
- new Response(JSON.stringify({ code: 20001, message: "account not found" }), {
11
- status: 400
12
- })
13
- );
14
- vi.stubGlobal("fetch", fetchMock);
15
- await expect(fetchAccount(42, true)).rejects.toThrow("Lighter /account returned 20001: account not found");
16
- expect(fetchMock.mock.calls[0]?.[0]).toBe("https://testnet.zklighter.elliot.ai/api/v1/account?by=index&value=42");
17
- });
18
- it("rejects HTTP failures before reading endpoint data", async () => {
19
- vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response("unavailable", { status: 503 })));
20
- await expect(fetchAssetDetails(false)).rejects.toThrow("Lighter /assetDetails returned 503");
21
- });
22
- it("preserves the account-not-registered lookup result for the helper", async () => {
23
- vi.stubGlobal(
24
- "fetch",
25
- vi.fn().mockResolvedValue(
26
- new Response(JSON.stringify({ code: 21100, message: "account not found" }), {
27
- status: 400
28
- })
29
- )
30
- );
31
- await expect(fetchAccountsByL1Address("0xabc", true)).resolves.toEqual({
32
- code: 21100,
33
- message: "account not found"
34
- });
35
- });
36
- it("paginates and de-duplicates subaccounts while stopping a repeated cursor", async () => {
37
- const fetchMock = vi.fn().mockResolvedValueOnce(
38
- new Response(
39
- JSON.stringify({
40
- code: 200,
41
- sub_accounts: [
42
- { account_type: 0, index: 1 },
43
- { account_type: 1, index: 2 }
44
- ],
45
- next_cursor: "page-2"
46
- }),
47
- { status: 200 }
48
- )
49
- ).mockResolvedValueOnce(
50
- new Response(
51
- JSON.stringify({
52
- code: 200,
53
- sub_accounts: [
54
- { account_type: 1, index: 2 },
55
- { account_type: 1, index: 3 }
56
- ],
57
- next_cursor: "page-2"
58
- }),
59
- { status: 200 }
60
- )
61
- );
62
- vi.stubGlobal("fetch", fetchMock);
63
- await expect(fetchAccountsByL1Address("0xabc", true)).resolves.toMatchObject({
64
- code: 200,
65
- sub_accounts: [
66
- { account_type: 0, index: 1 },
67
- { account_type: 1, index: 2 },
68
- { account_type: 1, index: 3 }
69
- ]
70
- });
71
- expect(fetchMock).toHaveBeenCalledTimes(2);
72
- expect(String(fetchMock.mock.calls[1]?.[0])).toContain("cursor=page-2");
73
- });
74
- it("reads authenticated account limits from the selected environment", async () => {
75
- const fetchMock = vi.fn().mockResolvedValue(
76
- new Response(JSON.stringify({ code: 200, user_tier: "premium" }), {
77
- status: 200
78
- })
79
- );
80
- vi.stubGlobal("fetch", fetchMock);
81
- await expect(fetchAccountLimits(42, "auth-token", true)).resolves.toMatchObject({ user_tier: "premium" });
82
- expect(fetchMock).toHaveBeenCalledWith(
83
- "https://testnet.zklighter.elliot.ai/api/v1/accountLimits?account_index=42",
84
- { headers: { authorization: "auth-token" } }
85
- );
86
- });
87
- it("returns typed L1 and L2 decimals from assetDetails", async () => {
88
- const fetchMock = vi.fn().mockResolvedValue(
89
- new Response(
90
- JSON.stringify({
91
- code: 200,
92
- asset_details: [{ asset_id: 1, symbol: "ETH", l1_decimals: 18, decimals: 8 }]
93
- }),
94
- { status: 200 }
95
- )
96
- );
97
- vi.stubGlobal("fetch", fetchMock);
98
- await expect(fetchAssetDetails(false)).resolves.toEqual([
99
- { asset_id: 1, symbol: "ETH", l1_decimals: 18, decimals: 8 }
100
- ]);
101
- expect(fetchMock.mock.calls[0]?.[0]).toBe("https://mainnet.zklighter.elliot.ai/api/v1/assetDetails");
102
- });
103
- it("paginates and de-duplicates funding rows by market and funding id", async () => {
104
- const duplicate = fundingRow({ funding_id: 2, timestamp: 2e3 });
105
- const fetchMock = vi.fn().mockResolvedValueOnce(
106
- new Response(
107
- JSON.stringify({
108
- code: 200,
109
- position_fundings: [fundingRow({ funding_id: 1, timestamp: 1e3 }), duplicate],
110
- next_cursor: "page-2"
111
- }),
112
- { status: 200 }
113
- )
114
- ).mockResolvedValueOnce(
115
- new Response(
116
- JSON.stringify({
117
- code: 200,
118
- position_fundings: [duplicate, fundingRow({ funding_id: 3, timestamp: 3e3 })]
119
- }),
120
- { status: 200 }
121
- )
122
- );
123
- vi.stubGlobal("fetch", fetchMock);
124
- const rows = await fetchPositionFundingSince(42, "auth", /* @__PURE__ */ new Date(1e3), true);
125
- expect(rows.map((row) => row.funding_id)).toEqual([1, 2, 3]);
126
- expect(fetchMock).toHaveBeenCalledTimes(2);
127
- expect(String(fetchMock.mock.calls[0]?.[0])).toContain("start_timestamp=1");
128
- expect(String(fetchMock.mock.calls[1]?.[0])).toContain("cursor=page-2");
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
- });
136
- it("stops trades pagination when the API repeats a cursor", async () => {
137
- const page = new Response(JSON.stringify({ code: 200, trades: [], next_cursor: "same-page" }), { status: 200 });
138
- const fetchMock = vi.fn().mockResolvedValueOnce(page).mockResolvedValueOnce(
139
- new Response(JSON.stringify({ code: 200, trades: [], next_cursor: "same-page" }), { status: 200 })
140
- );
141
- vi.stubGlobal("fetch", fetchMock);
142
- await expect(fetchTradesSince(42, "auth", /* @__PURE__ */ new Date(1e3), true)).resolves.toEqual([]);
143
- expect(fetchMock).toHaveBeenCalledTimes(2);
144
- expect(String(fetchMock.mock.calls[1]?.[0])).toContain("cursor=same-page");
145
- });
146
- });
147
- function fundingRow(overrides) {
148
- return {
149
- timestamp: overrides.timestamp,
150
- market_id: 1,
151
- funding_id: overrides.funding_id,
152
- change: "-2",
153
- discount: "0",
154
- rate: "0.0001",
155
- position_size: "4",
156
- position_side: "long"
157
- };
158
- }
@@ -1,72 +0,0 @@
1
- import { vi, describe, beforeEach, it, expect } from 'vitest';
2
- import { createHyperliquidHelper } from './helper';
3
-
4
- const { fetchInfoMock, fetchUserRoleMock } = vi.hoisted(() => ({
5
- fetchInfoMock: vi.fn(),
6
- fetchUserRoleMock: vi.fn()
7
- }));
8
- vi.mock("../../client/hyperliquid/rest", () => ({ fetchInfo: fetchInfoMock, fetchUserRole: fetchUserRoleMock }));
9
- const userAddress = "0x1111111111111111111111111111111111111111";
10
- const agentWalletAddress = "0x2222222222222222222222222222222222222222";
11
- const builderAddress = "0x3333333333333333333333333333333333333333";
12
- const masterAddress = "0x4444444444444444444444444444444444444444";
13
- describe("createHyperliquidHelper", () => {
14
- beforeEach(() => {
15
- fetchInfoMock.mockReset();
16
- fetchUserRoleMock.mockReset();
17
- });
18
- it("reports an address Hyperliquid does not know as not activated", async () => {
19
- fetchUserRoleMock.mockResolvedValue({ role: "missing" });
20
- const activated = await createHyperliquidHelper(true).checkAccountActivation(userAddress);
21
- expect(activated).toBe(false);
22
- expect(fetchUserRoleMock).toHaveBeenCalledWith(userAddress, true);
23
- });
24
- it.each([
25
- ["user", { role: "user" }],
26
- ["agent", { role: "agent", data: { user: masterAddress } }],
27
- ["vault", { role: "vault" }],
28
- ["subAccount", { role: "subAccount", data: { master: masterAddress } }]
29
- ])("reports the %s role as activated", async (_role, response) => {
30
- fetchUserRoleMock.mockResolvedValue(response);
31
- await expect(createHyperliquidHelper().checkAccountActivation(userAddress)).resolves.toBe(true);
32
- expect(fetchUserRoleMock).toHaveBeenCalledWith(userAddress, false);
33
- });
34
- it("surfaces a failed role request as an error instead of an inactive account", async () => {
35
- fetchUserRoleMock.mockRejectedValue(new Error("Hyperliquid info request failed: 429"));
36
- await expect(createHyperliquidHelper().checkAccountActivation(userAddress)).rejects.toThrow(
37
- "Hyperliquid info request failed: 429"
38
- );
39
- });
40
- it("accepts unexpired and non-expiring API wallets on the selected network", async () => {
41
- fetchInfoMock.mockResolvedValue([
42
- { name: "expired", address: agentWalletAddress, validUntil: Date.now() - 1 },
43
- { name: "active", address: agentWalletAddress.toUpperCase(), validUntil: null }
44
- ]);
45
- const valid = await createHyperliquidHelper(true).checkApiWallet({ userAddress, agentWalletAddress });
46
- expect(valid).toBe(true);
47
- expect(fetchInfoMock).toHaveBeenCalledWith({ type: "extraAgents", user: userAddress }, true);
48
- });
49
- it("rejects an expired API wallet", async () => {
50
- fetchInfoMock.mockResolvedValue([{ name: "expired", address: agentWalletAddress, validUntil: Date.now() - 1 }]);
51
- await expect(createHyperliquidHelper().checkApiWallet({ userAddress, agentWalletAddress })).resolves.toBe(false);
52
- });
53
- it("requires the approval to cover the requested fee rather than merely be positive", async () => {
54
- fetchInfoMock.mockResolvedValue(6);
55
- const helper = createHyperliquidHelper(true);
56
- await expect(helper.checkBuilderFeeApproval(userAddress, builderAddress, 10)).resolves.toBe(false);
57
- await expect(helper.checkBuilderFeeApproval(userAddress, builderAddress, 6)).resolves.toBe(true);
58
- expect(fetchInfoMock).toHaveBeenCalledWith(
59
- { type: "maxBuilderFee", user: userAddress, builder: builderAddress },
60
- true
61
- );
62
- });
63
- it("preserves the two-argument helper contract as an any-positive-approval check", async () => {
64
- fetchInfoMock.mockResolvedValue(1);
65
- const approved = await createHyperliquidHelper().checkBuilderFeeApproval(userAddress, builderAddress);
66
- expect(approved).toBe(true);
67
- expect(fetchInfoMock).toHaveBeenCalledWith(
68
- { type: "maxBuilderFee", user: userAddress, builder: builderAddress },
69
- false
70
- );
71
- });
72
- });
@@ -1,289 +0,0 @@
1
- import { vi, describe, beforeEach, it, expect } from 'vitest';
2
- import { createLighterHelper, findAvailableApiKeyIndex, MAX_API_KEY_INDEX, MIN_USER_API_KEY_INDEX } from './helper';
3
-
4
- const {
5
- fetchAccountMock,
6
- fetchAccountsByL1AddressMock,
7
- fetchApiKeysMock,
8
- fetchAssetDetailsMock,
9
- writeDepositMock,
10
- writeWithdrawMock
11
- } = vi.hoisted(() => ({
12
- fetchAccountMock: vi.fn(),
13
- fetchAccountsByL1AddressMock: vi.fn(),
14
- fetchApiKeysMock: vi.fn(),
15
- fetchAssetDetailsMock: vi.fn(),
16
- writeDepositMock: vi.fn(),
17
- writeWithdrawMock: vi.fn()
18
- }));
19
- vi.mock("../../client/lighter/rest", () => ({
20
- fetchAccount: fetchAccountMock,
21
- fetchAccountsByL1Address: fetchAccountsByL1AddressMock,
22
- fetchApiKeys: fetchApiKeysMock,
23
- fetchAssetDetails: fetchAssetDetailsMock
24
- }));
25
- vi.mock("./ethereum", () => ({
26
- writeDeposit: writeDepositMock,
27
- writeWithdraw: writeWithdrawMock
28
- }));
29
- const createApiKeyMock = vi.fn();
30
- const changePubKeyMock = vi.fn();
31
- const prepareApproveIntegratorMock = vi.fn();
32
- const submitApproveIntegratorMock = vi.fn();
33
- const signMessageMock = vi.fn();
34
- const sdk = {
35
- core: {
36
- misc: {
37
- lighter: {
38
- createApiKey: createApiKeyMock,
39
- changePubKey: changePubKeyMock,
40
- prepareApproveIntegrator: prepareApproveIntegratorMock,
41
- submitApproveIntegrator: submitApproveIntegratorMock
42
- }
43
- }
44
- }
45
- };
46
- const walletClient = {
47
- account: { address: "0x0000000000000000000000000000000000000001" },
48
- signMessage: signMessageMock
49
- };
50
- describe("Lighter helper asset decimals", () => {
51
- beforeEach(() => {
52
- fetchAssetDetailsMock.mockReset();
53
- writeDepositMock.mockReset().mockResolvedValue("0xdeposit");
54
- writeWithdrawMock.mockReset().mockResolvedValue("0xwithdraw");
55
- });
56
- it("uses assetDetails.l1_decimals for Ethereum deposits", async () => {
57
- fetchAssetDetailsMock.mockResolvedValue([{ asset_id: 1, symbol: "ETH", l1_decimals: 18, decimals: 8 }]);
58
- const helper = createLighterHelper(sdk);
59
- await helper.deposit(walletClient, {
60
- to: "0x0000000000000000000000000000000000000002",
61
- assetId: 1,
62
- routeType: 1,
63
- amount: "1"
64
- });
65
- expect(fetchAssetDetailsMock).toHaveBeenCalledWith(false);
66
- expect(writeDepositMock).toHaveBeenCalledWith(
67
- walletClient,
68
- expect.objectContaining({
69
- assetIndex: 1,
70
- amount: 1000000000000000000n,
71
- value: 1000000000000000000n
72
- })
73
- );
74
- });
75
- it("rejects demo deposits and withdrawals before reading assets or invoking the wallet writer", async () => {
76
- const helper = createLighterHelper(sdk, true);
77
- await expect(
78
- helper.deposit(walletClient, {
79
- to: "0x0000000000000000000000000000000000000002",
80
- assetId: 1,
81
- routeType: 1,
82
- amount: "1"
83
- })
84
- ).rejects.toThrow("testnet gateway is not verified");
85
- await expect(
86
- helper.withdraw(walletClient, {
87
- accountIndex: 42,
88
- assetId: 1,
89
- routeType: 1,
90
- amount: "1"
91
- })
92
- ).rejects.toThrow("testnet gateway is not verified");
93
- expect(fetchAssetDetailsMock).not.toHaveBeenCalled();
94
- expect(writeDepositMock).not.toHaveBeenCalled();
95
- expect(writeWithdrawMock).not.toHaveBeenCalled();
96
- });
97
- it("uses six-decimal USDC L1 precision without sending native value", async () => {
98
- fetchAssetDetailsMock.mockResolvedValue([{ asset_id: 3, symbol: "USDC", l1_decimals: 6, decimals: 6 }]);
99
- const helper = createLighterHelper(sdk);
100
- await helper.deposit(walletClient, {
101
- to: "0x0000000000000000000000000000000000000002",
102
- assetId: 3,
103
- routeType: 0,
104
- amount: "1.25"
105
- });
106
- expect(writeDepositMock).toHaveBeenCalledWith(
107
- walletClient,
108
- expect.objectContaining({ assetIndex: 3, amount: 1250000n, value: void 0 })
109
- );
110
- });
111
- it("rejects mismatched native ETH amount and msg.value before submitting", async () => {
112
- fetchAssetDetailsMock.mockResolvedValue([{ asset_id: 1, symbol: "ETH", l1_decimals: 18, decimals: 8 }]);
113
- const helper = createLighterHelper(sdk);
114
- await expect(
115
- helper.deposit(walletClient, {
116
- to: "0x0000000000000000000000000000000000000002",
117
- assetId: 1,
118
- routeType: 1,
119
- amount: "1",
120
- nativeValue: "0.5"
121
- })
122
- ).rejects.toThrow("native ETH deposit amount and msg.value must match");
123
- expect(writeDepositMock).not.toHaveBeenCalled();
124
- });
125
- it("uses assetDetails.decimals for Lighter withdrawal base amounts", async () => {
126
- fetchAssetDetailsMock.mockResolvedValue([{ asset_id: 1, symbol: "ETH", l1_decimals: 18, decimals: 8 }]);
127
- const helper = createLighterHelper(sdk, false);
128
- await helper.withdraw(walletClient, { accountIndex: 42, assetId: 1, routeType: 1, amount: "1" });
129
- expect(fetchAssetDetailsMock).toHaveBeenCalledWith(false);
130
- expect(writeWithdrawMock).toHaveBeenCalledWith(
131
- walletClient,
132
- expect.objectContaining({ assetIndex: 1, baseAmount: 100000000n })
133
- );
134
- });
135
- it("accepts cached authoritative decimals without another assetDetails request", async () => {
136
- const helper = createLighterHelper(sdk);
137
- await helper.deposit(walletClient, {
138
- to: "0x0000000000000000000000000000000000000002",
139
- assetId: 999,
140
- routeType: 1,
141
- amount: "1.25",
142
- l1Decimals: 6
143
- });
144
- expect(fetchAssetDetailsMock).not.toHaveBeenCalled();
145
- expect(writeDepositMock).toHaveBeenCalledWith(
146
- walletClient,
147
- expect.objectContaining({ assetIndex: 999, amount: 1250000n })
148
- );
149
- });
150
- });
151
- describe("Lighter helper integrator approval validation", () => {
152
- const approval = {
153
- account_index: 728059,
154
- name: "Pear",
155
- max_perps_taker_fee: 600,
156
- max_perps_maker_fee: 500,
157
- max_spot_taker_fee: 0,
158
- max_spot_maker_fee: 0,
159
- approval_expiry: 4e12
160
- };
161
- beforeEach(() => {
162
- fetchAccountMock.mockReset().mockResolvedValue({
163
- code: 200,
164
- accounts: [{ approved_integrators: [approval] }]
165
- });
166
- });
167
- it("requires a future approval with sufficient maker and taker fee caps", async () => {
168
- const helper = createLighterHelper(sdk, true);
169
- await expect(
170
- helper.checkIntegratorApproval(42, approval.account_index, {
171
- minPerpsMakerFee: 500,
172
- minPerpsTakerFee: 600
173
- })
174
- ).resolves.toBe(true);
175
- expect(fetchAccountMock).toHaveBeenCalledWith(42, true);
176
- });
177
- it("rejects expired approvals even when no fee requirements are supplied", async () => {
178
- fetchAccountMock.mockResolvedValue({
179
- code: 200,
180
- accounts: [{ approved_integrators: [{ ...approval, approval_expiry: 1 }] }]
181
- });
182
- const helper = createLighterHelper(sdk);
183
- await expect(helper.checkIntegratorApproval(42, approval.account_index)).resolves.toBe(false);
184
- });
185
- it("rejects approval caps below either configured requirement", async () => {
186
- const helper = createLighterHelper(sdk);
187
- await expect(
188
- helper.checkIntegratorApproval(42, approval.account_index, {
189
- minPerpsMakerFee: 501,
190
- minPerpsTakerFee: 600
191
- })
192
- ).resolves.toBe(false);
193
- await expect(
194
- helper.checkIntegratorApproval(42, approval.account_index, {
195
- minPerpsMakerFee: 500,
196
- minPerpsTakerFee: 601
197
- })
198
- ).resolves.toBe(false);
199
- });
200
- });
201
- describe("Lighter helper API key allocation", () => {
202
- beforeEach(() => {
203
- fetchApiKeysMock.mockReset();
204
- createApiKeyMock.mockReset();
205
- changePubKeyMock.mockReset();
206
- signMessageMock.mockReset();
207
- });
208
- it("uses the first free non-reserved index instead of growing past gaps", () => {
209
- expect(findAvailableApiKeyIndex([{ api_key_index: 0 }, { api_key_index: 4 }, { api_key_index: 6 }])).toBe(5);
210
- });
211
- it("throws once every documented user API-key slot from 4 through 254 is occupied", () => {
212
- const keys = Array.from({ length: MAX_API_KEY_INDEX - MIN_USER_API_KEY_INDEX + 1 }, (_, offset) => ({
213
- api_key_index: MIN_USER_API_KEY_INDEX + offset
214
- }));
215
- expect(() => findAvailableApiKeyIndex(keys)).toThrow("No available Lighter API key slots");
216
- });
217
- it("fails before signing when the account has no free user API-key slot", async () => {
218
- fetchApiKeysMock.mockResolvedValue(
219
- Array.from({ length: MAX_API_KEY_INDEX - MIN_USER_API_KEY_INDEX + 1 }, (_, offset) => ({
220
- api_key_index: MIN_USER_API_KEY_INDEX + offset
221
- }))
222
- );
223
- const helper = createLighterHelper(sdk);
224
- await expect(helper.connectWallet(walletClient, { accountIndex: 42 })).rejects.toThrow(
225
- "No available Lighter API key slots"
226
- );
227
- expect(fetchApiKeysMock).toHaveBeenCalledWith(42, false);
228
- expect(createApiKeyMock).not.toHaveBeenCalled();
229
- expect(signMessageMock).not.toHaveBeenCalled();
230
- expect(changePubKeyMock).not.toHaveBeenCalled();
231
- });
232
- it("submits mainnet API-key preparation and association without unsupported fields", async () => {
233
- fetchApiKeysMock.mockResolvedValue([]);
234
- createApiKeyMock.mockResolvedValue({ privateKey: "private", messageToSign: "message", txInfo: "tx-info" });
235
- signMessageMock.mockResolvedValue("0xsignature");
236
- changePubKeyMock.mockResolvedValue({ txHash: "tx-hash" });
237
- await createLighterHelper(sdk).connectWallet(walletClient, { accountIndex: 42 });
238
- expect(createApiKeyMock).toHaveBeenCalledWith({ accountIndex: 42, apiKeyIndex: 4 });
239
- expect(changePubKeyMock).toHaveBeenCalledWith({
240
- l1Signature: "0xsignature",
241
- txInfo: "tx-info",
242
- accountIndex: 42,
243
- apiKeyIndex: 4
244
- });
245
- });
246
- it("rejects demo wallet connection before public reads or backend signing", async () => {
247
- await expect(createLighterHelper(sdk, true).connectWallet(walletClient, { accountIndex: 42 })).rejects.toThrow(
248
- "backend signing is mainnet-only"
249
- );
250
- expect(fetchApiKeysMock).not.toHaveBeenCalled();
251
- expect(createApiKeyMock).not.toHaveBeenCalled();
252
- });
253
- });
254
- describe("Lighter helper integrator approval submission", () => {
255
- beforeEach(() => {
256
- prepareApproveIntegratorMock.mockReset().mockResolvedValue({ messageToSign: "message", txInfo: "tx-info" });
257
- submitApproveIntegratorMock.mockReset().mockResolvedValue({ txHash: "tx-hash" });
258
- signMessageMock.mockReset().mockResolvedValue("0xsignature");
259
- });
260
- it("submits mainnet approval preparation and association without unsupported fields", async () => {
261
- await createLighterHelper(sdk).approveIntegrator(walletClient, {
262
- accountIndex: 42,
263
- apiKeyIndex: 4,
264
- apiPrivateKey: "private"
265
- });
266
- expect(prepareApproveIntegratorMock).toHaveBeenCalledWith({
267
- accountIndex: 42,
268
- apiKeyIndex: 4,
269
- apiPrivateKey: "private"
270
- });
271
- expect(submitApproveIntegratorMock).toHaveBeenCalledWith({
272
- l1Signature: "0xsignature",
273
- txInfo: "tx-info",
274
- accountIndex: 42,
275
- apiKeyIndex: 4
276
- });
277
- });
278
- it("rejects demo approval before backend signing", async () => {
279
- await expect(
280
- createLighterHelper(sdk, true).approveIntegrator(walletClient, {
281
- accountIndex: 42,
282
- apiKeyIndex: 4,
283
- apiPrivateKey: "private"
284
- })
285
- ).rejects.toThrow("backend signing is mainnet-only");
286
- expect(prepareApproveIntegratorMock).not.toHaveBeenCalled();
287
- expect(submitApproveIntegratorMock).not.toHaveBeenCalled();
288
- });
289
- });