@pear-protocol/exchanges-sdk 0.3.0 → 0.4.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/hyperliquid/client.test.js +41 -0
- package/dist/client/hyperliquid/exchange-ws.js +3 -2
- package/dist/client/hyperliquid/rest.js +26 -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 +101 -32
- package/dist/client/lighter/rest.test.js +152 -0
- package/dist/client/lighter/ws.js +7 -2
- package/dist/client/okx/rest.js +18 -9
- 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 +182 -38
- package/dist/sdk.js +20 -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 +3 -2
|
@@ -0,0 +1,289 @@
|
|
|
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
|
+
});
|
|
@@ -6,13 +6,14 @@ const AssetId = {
|
|
|
6
6
|
UNI: 6,
|
|
7
7
|
AAVE: 7,
|
|
8
8
|
SKY: 8,
|
|
9
|
-
LDO: 9
|
|
9
|
+
LDO: 9,
|
|
10
|
+
AZTEC: 10
|
|
10
11
|
};
|
|
11
12
|
const RouteType = {
|
|
12
13
|
PERP: 0,
|
|
13
14
|
SPOT: 1
|
|
14
15
|
};
|
|
15
|
-
const
|
|
16
|
+
const L2_ASSET_DECIMALS = {
|
|
16
17
|
[AssetId.ETH]: 8,
|
|
17
18
|
[AssetId.LIT]: 8,
|
|
18
19
|
[AssetId.USDC]: 6,
|
|
@@ -20,16 +21,16 @@ const ASSET_DECIMALS = {
|
|
|
20
21
|
[AssetId.UNI]: 8,
|
|
21
22
|
[AssetId.AAVE]: 8,
|
|
22
23
|
[AssetId.SKY]: 8,
|
|
23
|
-
[AssetId.LDO]: 8
|
|
24
|
+
[AssetId.LDO]: 8,
|
|
25
|
+
[AssetId.AZTEC]: 8
|
|
24
26
|
};
|
|
27
|
+
const ASSET_DECIMALS = L2_ASSET_DECIMALS;
|
|
25
28
|
function getAssetDecimals(assetId) {
|
|
26
|
-
const decimals =
|
|
29
|
+
const decimals = L2_ASSET_DECIMALS[assetId];
|
|
27
30
|
if (decimals === void 0) {
|
|
28
|
-
throw new Error(
|
|
29
|
-
`Unknown lighter asset ID: ${assetId}. Provide decimals via ASSET_DECIMALS or use a known AssetId.`
|
|
30
|
-
);
|
|
31
|
+
throw new Error(`Unknown Lighter asset ID: ${assetId}. Read its decimals from the assetDetails endpoint.`);
|
|
31
32
|
}
|
|
32
33
|
return decimals;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
export { ASSET_DECIMALS, AssetId, RouteType, getAssetDecimals };
|
|
36
|
+
export { ASSET_DECIMALS, AssetId, L2_ASSET_DECIMALS, RouteType, getAssetDecimals };
|