@pear-protocol/exchanges-sdk 0.3.0 → 0.5.0
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/README.md +78 -12
- package/dist/client/binance/rest.js +4 -3
- package/dist/client/bybit/rest.js +2 -1
- package/dist/client/hyperliquid/client.test.js +41 -0
- package/dist/client/hyperliquid/exchange-ws.js +3 -2
- package/dist/client/hyperliquid/rest.js +28 -5
- package/dist/client/hyperliquid/ws.js +16 -4
- package/dist/client/index.js +2 -2
- package/dist/client/lighter/constants.js +9 -1
- package/dist/client/lighter/constants.test.js +22 -0
- package/dist/client/lighter/exchange-ws.js +2 -2
- package/dist/client/lighter/rest.js +110 -37
- package/dist/client/lighter/rest.test.js +152 -0
- package/dist/client/lighter/ws.js +7 -2
- package/dist/client/okx/rest.js +20 -10
- package/dist/helper/factory.js +2 -2
- package/dist/helper/hyperliquid/helper.js +28 -13
- package/dist/helper/hyperliquid/helper.test.js +45 -0
- package/dist/helper/lighter/helper.js +77 -16
- package/dist/helper/lighter/helper.test.js +289 -0
- package/dist/helper/lighter/types.js +9 -8
- package/dist/index.d.ts +189 -38
- package/dist/sdk.js +22 -16
- package/dist/sdk.test.js +101 -0
- package/dist/status/hyperliquid/status.js +18 -5
- package/dist/status/hyperliquid/status.test.js +81 -0
- package/dist/status/lighter/status.js +18 -11
- package/dist/status/lighter/status.test.js +75 -0
- package/dist/syncer/base.js +0 -2
- package/dist/syncer/base.test.js +0 -1
- package/dist/syncer/hyperliquid/syncer.js +96 -43
- package/dist/syncer/hyperliquid/syncer.test.js +274 -0
- package/dist/syncer/lighter/funding.js +25 -0
- package/dist/syncer/lighter/funding.test.js +48 -0
- package/dist/syncer/lighter/syncer.js +30 -6
- package/dist/syncer/lighter/syncer.test.js +132 -6
- package/dist/tracker/base.js +23 -6
- package/dist/tracker/base.test.js +132 -0
- package/dist/tracker/binance/tracker.js +48 -18
- package/dist/tracker/binance/tracker.test.js +138 -0
- package/dist/tracker/bybit/tracker.js +78 -23
- package/dist/tracker/bybit/tracker.test.js +145 -0
- package/dist/tracker/hyperliquid/mapper.js +244 -44
- package/dist/tracker/hyperliquid/mapper.test.js +255 -0
- package/dist/tracker/hyperliquid/tracker.js +241 -74
- package/dist/tracker/hyperliquid/tracker.test.js +587 -0
- package/dist/tracker/index.js +15 -37
- package/dist/tracker/lighter/mapper.js +163 -33
- package/dist/tracker/lighter/mapper.test.js +199 -0
- package/dist/tracker/lighter/tracker.js +219 -45
- package/dist/tracker/lighter/tracker.test.js +515 -0
- package/dist/tracker/lighter/types.js +10 -2
- package/dist/tracker/okx/tracker.js +50 -16
- package/dist/tracker/okx/tracker.test.js +126 -0
- package/dist/tracker/types.js +2 -1
- package/dist/tracker/utils.test.js +8 -0
- package/package.json +6 -5
|
@@ -0,0 +1,152 @@
|
|
|
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: 200
|
|
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: 200
|
|
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=1000");
|
|
128
|
+
expect(String(fetchMock.mock.calls[1]?.[0])).toContain("cursor=page-2");
|
|
129
|
+
});
|
|
130
|
+
it("stops trades pagination when the API repeats a cursor", async () => {
|
|
131
|
+
const page = new Response(JSON.stringify({ code: 200, trades: [], next_cursor: "same-page" }), { status: 200 });
|
|
132
|
+
const fetchMock = vi.fn().mockResolvedValueOnce(page).mockResolvedValueOnce(
|
|
133
|
+
new Response(JSON.stringify({ code: 200, trades: [], next_cursor: "same-page" }), { status: 200 })
|
|
134
|
+
);
|
|
135
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
136
|
+
await expect(fetchTradesSince(42, "auth", /* @__PURE__ */ new Date(1e3), true)).resolves.toEqual([]);
|
|
137
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
138
|
+
expect(String(fetchMock.mock.calls[1]?.[0])).toContain("cursor=same-page");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
function fundingRow(overrides) {
|
|
142
|
+
return {
|
|
143
|
+
timestamp: overrides.timestamp,
|
|
144
|
+
market_id: 1,
|
|
145
|
+
funding_id: overrides.funding_id,
|
|
146
|
+
change: "-2",
|
|
147
|
+
discount: "0",
|
|
148
|
+
rate: "0.0001",
|
|
149
|
+
position_size: "4",
|
|
150
|
+
position_side: "long"
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { BaseWs } from '../base-ws';
|
|
2
|
-
import {
|
|
2
|
+
import { getLighterWsUrl } from './constants';
|
|
3
3
|
|
|
4
4
|
const PING_INTERVAL_MS = 9e4;
|
|
5
5
|
class LighterWs extends BaseWs {
|
|
6
|
+
constructor(demo = false) {
|
|
7
|
+
super();
|
|
8
|
+
this.demo = demo;
|
|
9
|
+
}
|
|
10
|
+
demo;
|
|
6
11
|
getWsUrl() {
|
|
7
|
-
return
|
|
12
|
+
return getLighterWsUrl(this.demo);
|
|
8
13
|
}
|
|
9
14
|
getPingIntervalMs() {
|
|
10
15
|
return PING_INTERVAL_MS;
|
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
|
}
|
|
@@ -44,14 +45,23 @@ async function fetchBalance(creds, demo) {
|
|
|
44
45
|
async function fetchPositions(creds, demo) {
|
|
45
46
|
return signedGet(creds, "/api/v5/account/positions", { instType: "SWAP" }, demo);
|
|
46
47
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
const LEVERAGE_INFO_MAX_INST_IDS = 20;
|
|
49
|
+
async function fetchLeverageInfos(creds, instIds, demo) {
|
|
50
|
+
const chunks = [];
|
|
51
|
+
for (let i = 0; i < instIds.length; i += LEVERAGE_INFO_MAX_INST_IDS) {
|
|
52
|
+
chunks.push(instIds.slice(i, i + LEVERAGE_INFO_MAX_INST_IDS));
|
|
53
|
+
}
|
|
54
|
+
const results = await Promise.all(
|
|
55
|
+
chunks.map(
|
|
56
|
+
(chunk) => signedGet(
|
|
57
|
+
creds,
|
|
58
|
+
"/api/v5/account/leverage-info",
|
|
59
|
+
{ instId: chunk.join(","), mgnMode: "cross" },
|
|
60
|
+
demo
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
);
|
|
64
|
+
return results.flat();
|
|
55
65
|
}
|
|
56
66
|
const FILLS_HISTORY_LIMIT = 100;
|
|
57
67
|
async function fetchFillsHistorySince(creds, since, demo) {
|
|
@@ -81,4 +91,4 @@ async function fetchFillsHistorySince(creds, since, demo) {
|
|
|
81
91
|
return Array.from(byId.values());
|
|
82
92
|
}
|
|
83
93
|
|
|
84
|
-
export { fetchAccountConfig, fetchBalance, fetchFillsHistorySince,
|
|
94
|
+
export { fetchAccountConfig, fetchBalance, fetchFillsHistorySince, fetchLeverageInfos, fetchPositions };
|
package/dist/helper/factory.js
CHANGED
|
@@ -4,9 +4,9 @@ import { createLighterHelper } from './lighter';
|
|
|
4
4
|
function createExchangeHelper(config) {
|
|
5
5
|
switch (config.exchange) {
|
|
6
6
|
case "hyperliquid":
|
|
7
|
-
return createHyperliquidHelper();
|
|
7
|
+
return createHyperliquidHelper(config.demo ?? false);
|
|
8
8
|
case "lighter":
|
|
9
|
-
return createLighterHelper(config.sdk);
|
|
9
|
+
return createLighterHelper(config.sdk, config.demo ?? false);
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
|
|
@@ -1,21 +1,36 @@
|
|
|
1
1
|
import { fetchInfo } from '../../client/hyperliquid/rest';
|
|
2
2
|
|
|
3
|
-
function createHyperliquidHelper() {
|
|
3
|
+
function createHyperliquidHelper(demo = false) {
|
|
4
4
|
return {
|
|
5
|
+
/** Checks that the requested API wallet exists and has not expired. */
|
|
5
6
|
async checkApiWallet(params) {
|
|
6
|
-
const agents = await fetchInfo(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
const agents = await fetchInfo(
|
|
8
|
+
{
|
|
9
|
+
type: "extraAgents",
|
|
10
|
+
user: params.userAddress
|
|
11
|
+
},
|
|
12
|
+
demo
|
|
13
|
+
);
|
|
14
|
+
const now = Date.now();
|
|
15
|
+
return agents.some(
|
|
16
|
+
(agent) => agent.address.toLowerCase() === params.agentWalletAddress.toLowerCase() && (agent.validUntil === null || agent.validUntil > now)
|
|
17
|
+
);
|
|
11
18
|
},
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
+
/** Checks that the approval covers the exact builder fee requested by orders. */
|
|
20
|
+
async checkBuilderFeeApproval(userAddress, builderAddress, requiredFeeTenthsBps) {
|
|
21
|
+
const requiredFee = requiredFeeTenthsBps ?? 1;
|
|
22
|
+
if (!Number.isInteger(requiredFee) || requiredFee <= 0) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const maxBuilderFee = await fetchInfo(
|
|
26
|
+
{
|
|
27
|
+
type: "maxBuilderFee",
|
|
28
|
+
user: userAddress,
|
|
29
|
+
builder: builderAddress
|
|
30
|
+
},
|
|
31
|
+
demo
|
|
32
|
+
);
|
|
33
|
+
return Number(maxBuilderFee) >= requiredFee;
|
|
19
34
|
}
|
|
20
35
|
};
|
|
21
36
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { vi, describe, beforeEach, it, expect } from 'vitest';
|
|
2
|
+
import { createHyperliquidHelper } from './helper';
|
|
3
|
+
|
|
4
|
+
const { fetchInfoMock } = vi.hoisted(() => ({ fetchInfoMock: vi.fn() }));
|
|
5
|
+
vi.mock("../../client/hyperliquid/rest", () => ({ fetchInfo: fetchInfoMock }));
|
|
6
|
+
const userAddress = "0x1111111111111111111111111111111111111111";
|
|
7
|
+
const agentWalletAddress = "0x2222222222222222222222222222222222222222";
|
|
8
|
+
const builderAddress = "0x3333333333333333333333333333333333333333";
|
|
9
|
+
describe("createHyperliquidHelper", () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
fetchInfoMock.mockReset();
|
|
12
|
+
});
|
|
13
|
+
it("accepts unexpired and non-expiring API wallets on the selected network", async () => {
|
|
14
|
+
fetchInfoMock.mockResolvedValue([
|
|
15
|
+
{ name: "expired", address: agentWalletAddress, validUntil: Date.now() - 1 },
|
|
16
|
+
{ name: "active", address: agentWalletAddress.toUpperCase(), validUntil: null }
|
|
17
|
+
]);
|
|
18
|
+
const valid = await createHyperliquidHelper(true).checkApiWallet({ userAddress, agentWalletAddress });
|
|
19
|
+
expect(valid).toBe(true);
|
|
20
|
+
expect(fetchInfoMock).toHaveBeenCalledWith({ type: "extraAgents", user: userAddress }, true);
|
|
21
|
+
});
|
|
22
|
+
it("rejects an expired API wallet", async () => {
|
|
23
|
+
fetchInfoMock.mockResolvedValue([{ name: "expired", address: agentWalletAddress, validUntil: Date.now() - 1 }]);
|
|
24
|
+
await expect(createHyperliquidHelper().checkApiWallet({ userAddress, agentWalletAddress })).resolves.toBe(false);
|
|
25
|
+
});
|
|
26
|
+
it("requires the approval to cover the requested fee rather than merely be positive", async () => {
|
|
27
|
+
fetchInfoMock.mockResolvedValue(6);
|
|
28
|
+
const helper = createHyperliquidHelper(true);
|
|
29
|
+
await expect(helper.checkBuilderFeeApproval(userAddress, builderAddress, 10)).resolves.toBe(false);
|
|
30
|
+
await expect(helper.checkBuilderFeeApproval(userAddress, builderAddress, 6)).resolves.toBe(true);
|
|
31
|
+
expect(fetchInfoMock).toHaveBeenCalledWith(
|
|
32
|
+
{ type: "maxBuilderFee", user: userAddress, builder: builderAddress },
|
|
33
|
+
true
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
it("preserves the two-argument helper contract as an any-positive-approval check", async () => {
|
|
37
|
+
fetchInfoMock.mockResolvedValue(1);
|
|
38
|
+
const approved = await createHyperliquidHelper().checkBuilderFeeApproval(userAddress, builderAddress);
|
|
39
|
+
expect(approved).toBe(true);
|
|
40
|
+
expect(fetchInfoMock).toHaveBeenCalledWith(
|
|
41
|
+
{ type: "maxBuilderFee", user: userAddress, builder: builderAddress },
|
|
42
|
+
false
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -1,12 +1,46 @@
|
|
|
1
1
|
import { parseUnits, parseEther } from 'viem';
|
|
2
|
-
import { fetchApiKeys, fetchAccount, fetchAccountsByL1Address } from '../../client/lighter/rest';
|
|
2
|
+
import { fetchApiKeys, fetchAccount, fetchAccountsByL1Address, fetchAssetDetails } from '../../client/lighter/rest';
|
|
3
3
|
import { writeWithdraw, writeDeposit } from './ethereum';
|
|
4
|
-
import {
|
|
4
|
+
import { AssetId } from './types';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
const MIN_USER_API_KEY_INDEX = 4;
|
|
7
|
+
const MAX_API_KEY_INDEX = 254;
|
|
8
|
+
function findAvailableApiKeyIndex(apiKeys) {
|
|
9
|
+
const occupied = new Set(apiKeys.map((key) => key.api_key_index));
|
|
10
|
+
for (let index = MIN_USER_API_KEY_INDEX; index <= MAX_API_KEY_INDEX; index += 1) {
|
|
11
|
+
if (!occupied.has(index)) return index;
|
|
12
|
+
}
|
|
13
|
+
throw new Error(
|
|
14
|
+
`No available Lighter API key slots: non-reserved indices ${MIN_USER_API_KEY_INDEX}-${MAX_API_KEY_INDEX} are occupied`
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
function validateDecimals(decimals, source) {
|
|
18
|
+
if (!Number.isSafeInteger(decimals) || decimals < 0 || decimals > 255) {
|
|
19
|
+
throw new Error(`Invalid ${source} decimals: ${decimals}`);
|
|
20
|
+
}
|
|
21
|
+
return decimals;
|
|
22
|
+
}
|
|
23
|
+
async function resolveAssetDecimals(assetId, field, suppliedDecimals, demo) {
|
|
24
|
+
if (suppliedDecimals !== void 0) return validateDecimals(suppliedDecimals, field);
|
|
25
|
+
const assets = await fetchAssetDetails(demo);
|
|
26
|
+
const asset = assets.find((candidate) => candidate.asset_id === assetId);
|
|
27
|
+
if (!asset) throw new Error(`Lighter asset ${assetId} was not returned by assetDetails`);
|
|
28
|
+
return validateDecimals(asset[field], `assetDetails.${field}`);
|
|
29
|
+
}
|
|
30
|
+
function validateApprovalRequirements(requirements) {
|
|
31
|
+
for (const [name, value] of [
|
|
32
|
+
["minPerpsMakerFee", requirements.minPerpsMakerFee],
|
|
33
|
+
["minPerpsTakerFee", requirements.minPerpsTakerFee]
|
|
34
|
+
]) {
|
|
35
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
36
|
+
throw new Error(`Invalid Lighter integrator ${name}: ${value}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function createLighterHelper(sdk, demo = false) {
|
|
7
41
|
return {
|
|
8
42
|
async lookupAccount(l1Address) {
|
|
9
|
-
const data = await fetchAccountsByL1Address(l1Address);
|
|
43
|
+
const data = await fetchAccountsByL1Address(l1Address, demo);
|
|
10
44
|
if (data.code === 21100) {
|
|
11
45
|
throw new Error("Account not registered on Lighter");
|
|
12
46
|
}
|
|
@@ -20,16 +54,24 @@ function createLighterHelper(sdk) {
|
|
|
20
54
|
subAccountIndexes: accounts.filter((s) => s.account_type !== 0).map((s) => s.index)
|
|
21
55
|
};
|
|
22
56
|
},
|
|
23
|
-
async checkIntegratorApproval(accountIndex, integratorAccountIndex) {
|
|
24
|
-
|
|
57
|
+
async checkIntegratorApproval(accountIndex, integratorAccountIndex, requirements) {
|
|
58
|
+
if (requirements) validateApprovalRequirements(requirements);
|
|
59
|
+
const data = await fetchAccount(accountIndex, demo);
|
|
25
60
|
const account = data.accounts[0];
|
|
26
61
|
if (!account) return false;
|
|
62
|
+
const minMakerFee = requirements?.minPerpsMakerFee ?? 0;
|
|
63
|
+
const minTakerFee = requirements?.minPerpsTakerFee ?? 0;
|
|
27
64
|
const integrators = account.approved_integrators ?? [];
|
|
28
|
-
return integrators.some(
|
|
65
|
+
return integrators.some(
|
|
66
|
+
(integrator) => integrator.account_index === integratorAccountIndex && integrator.approval_expiry > Date.now() && integrator.max_perps_maker_fee >= minMakerFee && integrator.max_perps_taker_fee >= minTakerFee
|
|
67
|
+
);
|
|
29
68
|
},
|
|
30
69
|
async connectWallet(walletClient, params) {
|
|
31
|
-
|
|
32
|
-
|
|
70
|
+
if (demo) {
|
|
71
|
+
throw new Error("Lighter demo wallet connection is unavailable because backend signing is mainnet-only");
|
|
72
|
+
}
|
|
73
|
+
const apiKeys = await fetchApiKeys(params.accountIndex, demo);
|
|
74
|
+
const apiKeyIndex = findAvailableApiKeyIndex(apiKeys);
|
|
33
75
|
const { privateKey, messageToSign, txInfo } = await sdk.core.misc.lighter.createApiKey({
|
|
34
76
|
accountIndex: params.accountIndex,
|
|
35
77
|
apiKeyIndex
|
|
@@ -47,6 +89,9 @@ function createLighterHelper(sdk) {
|
|
|
47
89
|
return { txHash, apiKeyIndex, apiPrivateKey: privateKey };
|
|
48
90
|
},
|
|
49
91
|
async approveIntegrator(walletClient, params) {
|
|
92
|
+
if (demo) {
|
|
93
|
+
throw new Error("Lighter demo integrator approval is unavailable because backend signing is mainnet-only");
|
|
94
|
+
}
|
|
50
95
|
const { messageToSign, txInfo } = await sdk.core.misc.lighter.prepareApproveIntegrator({
|
|
51
96
|
accountIndex: params.accountIndex,
|
|
52
97
|
apiKeyIndex: params.apiKeyIndex,
|
|
@@ -65,28 +110,44 @@ function createLighterHelper(sdk) {
|
|
|
65
110
|
return { txHash };
|
|
66
111
|
},
|
|
67
112
|
async deposit(walletClient, params) {
|
|
113
|
+
if (demo) {
|
|
114
|
+
throw new Error("Lighter demo deposits are unavailable because the testnet gateway is not verified");
|
|
115
|
+
}
|
|
116
|
+
const l1Decimals = await resolveAssetDecimals(params.assetId, "l1_decimals", params.l1Decimals, demo);
|
|
117
|
+
const amount = parseUnits(params.amount, l1Decimals);
|
|
118
|
+
let value;
|
|
119
|
+
if (params.assetId === AssetId.ETH) {
|
|
120
|
+
value = parseEther(params.nativeValue ?? params.amount);
|
|
121
|
+
if (value !== amount) {
|
|
122
|
+
throw new Error("Lighter native ETH deposit amount and msg.value must match");
|
|
123
|
+
}
|
|
124
|
+
} else if (params.nativeValue !== void 0 && parseEther(params.nativeValue) !== 0n) {
|
|
125
|
+
throw new Error("Lighter nativeValue is only valid for ETH deposits");
|
|
126
|
+
}
|
|
68
127
|
const txHash = await writeDeposit(walletClient, {
|
|
69
128
|
to: params.to,
|
|
70
129
|
assetIndex: params.assetId,
|
|
71
130
|
routeType: params.routeType,
|
|
72
|
-
//
|
|
73
|
-
amount
|
|
74
|
-
|
|
75
|
-
// msg.value (18-decimal wei), separate from the accounting `amount`.
|
|
76
|
-
value: params.nativeValue ? parseEther(params.nativeValue) : void 0
|
|
131
|
+
// Ethereum deposits use the L1 token precision, not Lighter's L2/base-amount precision.
|
|
132
|
+
amount,
|
|
133
|
+
value
|
|
77
134
|
});
|
|
78
135
|
return { txHash };
|
|
79
136
|
},
|
|
80
137
|
async withdraw(walletClient, params) {
|
|
138
|
+
if (demo) {
|
|
139
|
+
throw new Error("Lighter demo withdrawals are unavailable because the testnet gateway is not verified");
|
|
140
|
+
}
|
|
141
|
+
const l2Decimals = await resolveAssetDecimals(params.assetId, "decimals", params.l2Decimals, demo);
|
|
81
142
|
const txHash = await writeWithdraw(walletClient, {
|
|
82
143
|
accountIndex: params.accountIndex,
|
|
83
144
|
assetIndex: params.assetId,
|
|
84
145
|
routeType: params.routeType,
|
|
85
|
-
baseAmount: parseUnits(params.amount,
|
|
146
|
+
baseAmount: parseUnits(params.amount, l2Decimals)
|
|
86
147
|
});
|
|
87
148
|
return { txHash };
|
|
88
149
|
}
|
|
89
150
|
};
|
|
90
151
|
}
|
|
91
152
|
|
|
92
|
-
export { createLighterHelper };
|
|
153
|
+
export { MAX_API_KEY_INDEX, MIN_USER_API_KEY_INDEX, createLighterHelper, findAvailableApiKeyIndex };
|