@pear-protocol/exchanges-sdk 0.6.5 → 0.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/base-ws.js +15 -6
- package/dist/client/binance/exchange-ws.js +22 -8
- package/dist/client/binance/rest.js +40 -27
- package/dist/client/binance/ws.js +21 -2
- package/dist/client/bybit/exchange-ws.js +13 -6
- package/dist/client/bybit/rest.js +30 -36
- package/dist/client/bybit/ws.js +16 -2
- package/dist/client/exchange-ws.js +5 -0
- package/dist/client/hmac.js +9 -1
- package/dist/client/hyperliquid/exchange-ws.js +13 -5
- package/dist/client/hyperliquid/rest.js +37 -7
- package/dist/client/hyperliquid/ws.js +10 -4
- package/dist/client/index.js +9 -2
- package/dist/client/lighter/constants.js +4 -0
- package/dist/client/lighter/exchange-ws.js +13 -5
- package/dist/client/lighter/rest.js +153 -18
- package/dist/client/lighter/ws.js +10 -4
- package/dist/client/okx/exchange-ws.js +13 -6
- package/dist/client/okx/rest.js +27 -24
- package/dist/client/okx/ws.js +5 -0
- package/dist/helper/factory.js +3 -0
- package/dist/helper/hyperliquid/helper.js +18 -23
- package/dist/helper/lighter/auth-token.helper.js +68 -0
- package/dist/helper/lighter/cctp-deposit.helper.js +297 -0
- package/dist/helper/lighter/ethereum/abi.js +55 -11
- package/dist/helper/lighter/ethereum/index.js +22 -3
- package/dist/helper/lighter/ethereum/usdc.helper.js +77 -0
- package/dist/helper/lighter/helper.js +116 -37
- package/dist/helper/lighter/index.js +1 -0
- package/dist/helper/lighter/onboarding.helper.js +28 -0
- package/dist/helper/lighter/types.js +6 -1
- package/dist/helper/subaccount.helper.js +20 -0
- package/dist/index.d.ts +202 -1
- package/dist/sdk.js +18 -7
- package/dist/status/base.js +16 -3
- package/dist/status/binance/status.js +3 -0
- package/dist/status/bybit/status.js +3 -0
- package/dist/status/factory.js +3 -0
- package/dist/status/hyperliquid/status.js +8 -6
- package/dist/status/lighter/status.js +11 -3
- package/dist/status/okx/status.js +3 -0
- package/dist/syncer/base.js +63 -24
- package/dist/syncer/binance/syncer.js +14 -13
- package/dist/syncer/bybit/syncer.js +10 -6
- package/dist/syncer/factory.js +3 -0
- package/dist/syncer/hyperliquid/funding.js +3 -0
- package/dist/syncer/hyperliquid/syncer.js +52 -66
- package/dist/syncer/lighter/funding.js +3 -0
- package/dist/syncer/lighter/syncer.js +26 -25
- package/dist/syncer/okx/syncer.js +15 -13
- package/dist/tracker/base.js +15 -5
- package/dist/tracker/binance/mapper.js +5 -1
- package/dist/tracker/binance/tracker.js +63 -23
- package/dist/tracker/binance/types.js +2 -2
- package/dist/tracker/bybit/mapper.js +7 -1
- package/dist/tracker/bybit/tracker.js +68 -24
- package/dist/tracker/bybit/types.js +2 -2
- package/dist/tracker/hyperliquid/const.js +3 -1
- package/dist/tracker/hyperliquid/mapper.js +36 -20
- package/dist/tracker/hyperliquid/tracker.js +147 -54
- package/dist/tracker/hyperliquid/types.js +2 -2
- package/dist/tracker/index.js +3 -0
- package/dist/tracker/lighter/mapper.js +41 -31
- package/dist/tracker/lighter/tracker.js +68 -27
- package/dist/tracker/lighter/types.js +4 -4
- package/dist/tracker/okx/mapper.js +8 -0
- package/dist/tracker/okx/tracker.js +69 -18
- package/dist/tracker/okx/types.js +2 -2
- package/dist/tracker/utils.js +4 -0
- package/package.json +3 -3
- package/dist/client/hyperliquid/client.test.js +0 -41
- package/dist/client/lighter/constants.test.js +0 -22
- package/dist/client/lighter/rest.test.js +0 -158
- package/dist/helper/hyperliquid/helper.test.js +0 -72
- package/dist/helper/lighter/helper.test.js +0 -289
- package/dist/sdk.test.js +0 -117
- package/dist/status/hyperliquid/status.test.js +0 -94
- package/dist/status/lighter/status.test.js +0 -86
- package/dist/syncer/base.test.js +0 -178
- package/dist/syncer/hyperliquid/funding.test.js +0 -63
- package/dist/syncer/hyperliquid/syncer.test.js +0 -271
- package/dist/syncer/lighter/funding.test.js +0 -48
- package/dist/syncer/lighter/syncer.test.js +0 -274
- package/dist/tracker/base.test.js +0 -132
- package/dist/tracker/binance/tracker.test.js +0 -138
- package/dist/tracker/bybit/tracker.test.js +0 -145
- package/dist/tracker/hyperliquid/mapper.test.js +0 -260
- package/dist/tracker/hyperliquid/tracker.test.js +0 -597
- package/dist/tracker/lighter/mapper.test.js +0 -199
- package/dist/tracker/lighter/tracker.test.js +0 -515
- package/dist/tracker/okx/tracker.test.js +0 -126
- package/dist/tracker/utils.test.js +0 -8
package/dist/sdk.test.js
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { vi, describe, beforeEach, it, expect } from 'vitest';
|
|
2
|
-
import { ExchangesSDK } from './sdk';
|
|
3
|
-
|
|
4
|
-
const { checkAccountStatusMock, createAccountSyncerMock, createAccountTrackerMock, createExchangeWsMock } = vi.hoisted(
|
|
5
|
-
() => ({
|
|
6
|
-
checkAccountStatusMock: vi.fn(),
|
|
7
|
-
createAccountSyncerMock: vi.fn(),
|
|
8
|
-
createAccountTrackerMock: vi.fn(),
|
|
9
|
-
createExchangeWsMock: vi.fn()
|
|
10
|
-
})
|
|
11
|
-
);
|
|
12
|
-
vi.mock("./status", () => ({
|
|
13
|
-
checkAccountStatus: checkAccountStatusMock
|
|
14
|
-
}));
|
|
15
|
-
vi.mock("./syncer", () => ({
|
|
16
|
-
createAccountSyncer: createAccountSyncerMock
|
|
17
|
-
}));
|
|
18
|
-
vi.mock("./client", () => ({
|
|
19
|
-
createExchangeWs: createExchangeWsMock
|
|
20
|
-
}));
|
|
21
|
-
vi.mock("./tracker", () => ({
|
|
22
|
-
createAccountTracker: createAccountTrackerMock
|
|
23
|
-
}));
|
|
24
|
-
const connection = {
|
|
25
|
-
connector: "okx",
|
|
26
|
-
account: { id: "account-1" },
|
|
27
|
-
credentials: { api_key: "k", api_secret: "s", api_pass: "p" }
|
|
28
|
-
};
|
|
29
|
-
function buildSdk(credentials = vi.fn().mockResolvedValue(connection), demo = true) {
|
|
30
|
-
const sdk = {
|
|
31
|
-
core: { accounts: { credentials } }
|
|
32
|
-
};
|
|
33
|
-
return { exchangesSdk: new ExchangesSDK({ sdk, demo }), credentials, sdk };
|
|
34
|
-
}
|
|
35
|
-
describe("ExchangesSDK.snapshot", () => {
|
|
36
|
-
beforeEach(() => {
|
|
37
|
-
checkAccountStatusMock.mockReset().mockResolvedValue({ status: "active", reasons: [] });
|
|
38
|
-
createAccountSyncerMock.mockReset().mockReturnValue({});
|
|
39
|
-
createExchangeWsMock.mockReset();
|
|
40
|
-
createAccountTrackerMock.mockReset();
|
|
41
|
-
});
|
|
42
|
-
it("fetches credentials, builds a ws-free tracker, and returns tracker.snapshot() without opening a socket", async () => {
|
|
43
|
-
const fakeSnapshot = { balance: null, positions: [] };
|
|
44
|
-
const trackerSnapshot = vi.fn().mockResolvedValue(fakeSnapshot);
|
|
45
|
-
createAccountTrackerMock.mockReturnValue({ snapshot: trackerSnapshot });
|
|
46
|
-
const { exchangesSdk, credentials, sdk } = buildSdk();
|
|
47
|
-
const result = await exchangesSdk.snapshot("trade-account-1");
|
|
48
|
-
expect(credentials).toHaveBeenCalledWith("trade-account-1");
|
|
49
|
-
expect(createAccountTrackerMock).toHaveBeenCalledWith(
|
|
50
|
-
"okx",
|
|
51
|
-
expect.objectContaining({
|
|
52
|
-
demo: true,
|
|
53
|
-
account: connection.account,
|
|
54
|
-
credentials: connection.credentials,
|
|
55
|
-
sdk
|
|
56
|
-
})
|
|
57
|
-
);
|
|
58
|
-
expect(trackerSnapshot).toHaveBeenCalledTimes(1);
|
|
59
|
-
expect(result).toBe(fakeSnapshot);
|
|
60
|
-
expect(createExchangeWsMock).not.toHaveBeenCalled();
|
|
61
|
-
});
|
|
62
|
-
it("snapshotAssets() delegates the coin list to a ws-free tracker", async () => {
|
|
63
|
-
const fakeAssets = { BTC: { coin: "BTC", leverage: "10", marginType: "cross" } };
|
|
64
|
-
const trackerSnapshotAssets = vi.fn().mockResolvedValue(fakeAssets);
|
|
65
|
-
createAccountTrackerMock.mockReturnValue({ snapshotAssets: trackerSnapshotAssets });
|
|
66
|
-
const { exchangesSdk, credentials } = buildSdk();
|
|
67
|
-
const result = await exchangesSdk.snapshotAssets("trade-account-1", ["BTC", "ETH"]);
|
|
68
|
-
expect(credentials).toHaveBeenCalledWith("trade-account-1");
|
|
69
|
-
expect(trackerSnapshotAssets).toHaveBeenCalledWith(["BTC", "ETH"]);
|
|
70
|
-
expect(result).toBe(fakeAssets);
|
|
71
|
-
expect(createExchangeWsMock).not.toHaveBeenCalled();
|
|
72
|
-
});
|
|
73
|
-
it("propagates credential fetch failures", async () => {
|
|
74
|
-
const { exchangesSdk } = buildSdk(vi.fn().mockRejectedValue(new Error("no access")));
|
|
75
|
-
await expect(exchangesSdk.snapshot("trade-account-1")).rejects.toThrow("no access");
|
|
76
|
-
expect(createAccountTrackerMock).not.toHaveBeenCalled();
|
|
77
|
-
expect(createExchangeWsMock).not.toHaveBeenCalled();
|
|
78
|
-
});
|
|
79
|
-
it("connect() is credential-only and opens no socket", async () => {
|
|
80
|
-
const { exchangesSdk } = buildSdk();
|
|
81
|
-
const result = await exchangesSdk.connect("trade-account-1");
|
|
82
|
-
expect(result).toEqual(connection);
|
|
83
|
-
expect(createExchangeWsMock).not.toHaveBeenCalled();
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
describe("ExchangesSDK configured network routing", () => {
|
|
87
|
-
beforeEach(() => {
|
|
88
|
-
checkAccountStatusMock.mockReset().mockResolvedValue({ status: "active", reasons: [] });
|
|
89
|
-
createAccountSyncerMock.mockReset().mockReturnValue({});
|
|
90
|
-
createAccountTrackerMock.mockReset().mockReturnValue({});
|
|
91
|
-
});
|
|
92
|
-
it("uses the SDK demo option for account-bound operations", async () => {
|
|
93
|
-
const { exchangesSdk } = buildSdk(void 0, true);
|
|
94
|
-
await exchangesSdk.status(connection);
|
|
95
|
-
exchangesSdk.createTracker(connection);
|
|
96
|
-
exchangesSdk.createSyncer(connection);
|
|
97
|
-
expect(checkAccountStatusMock).toHaveBeenCalledWith("okx", expect.objectContaining({ demo: true }));
|
|
98
|
-
expect(createAccountTrackerMock).toHaveBeenCalledWith("okx", expect.objectContaining({ demo: true }));
|
|
99
|
-
expect(createAccountSyncerMock).toHaveBeenCalledWith("okx", expect.objectContaining({ demo: true }));
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
describe("ExchangesSDK status options", () => {
|
|
103
|
-
beforeEach(() => {
|
|
104
|
-
checkAccountStatusMock.mockReset().mockResolvedValue({ status: "active", reasons: [] });
|
|
105
|
-
});
|
|
106
|
-
it("passes per-call options through to the status check unchanged", async () => {
|
|
107
|
-
const options = { builderAddress: "0xabc", builderFeeTenthsBps: 60 };
|
|
108
|
-
const { exchangesSdk } = buildSdk();
|
|
109
|
-
await exchangesSdk.status(connection, options);
|
|
110
|
-
expect(checkAccountStatusMock).toHaveBeenCalledWith("okx", expect.objectContaining({ statusOptions: options }));
|
|
111
|
-
});
|
|
112
|
-
it("leaves the options absent when the caller passes none", async () => {
|
|
113
|
-
const { exchangesSdk } = buildSdk();
|
|
114
|
-
await exchangesSdk.status(connection);
|
|
115
|
-
expect(checkAccountStatusMock).toHaveBeenCalledWith("okx", expect.objectContaining({ statusOptions: void 0 }));
|
|
116
|
-
});
|
|
117
|
-
});
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { vi, describe, beforeEach, it, expect } from 'vitest';
|
|
2
|
-
import { checkHyperliquidAccountStatus } from './status';
|
|
3
|
-
|
|
4
|
-
const { checkApiWalletMock, checkBuilderFeeApprovalMock, fetchUserAbstractionMock } = vi.hoisted(() => ({
|
|
5
|
-
checkApiWalletMock: vi.fn(),
|
|
6
|
-
checkBuilderFeeApprovalMock: vi.fn(),
|
|
7
|
-
fetchUserAbstractionMock: vi.fn()
|
|
8
|
-
}));
|
|
9
|
-
vi.mock("../../helper/factory", () => ({
|
|
10
|
-
createExchangeHelper: () => ({
|
|
11
|
-
checkApiWallet: checkApiWalletMock,
|
|
12
|
-
checkBuilderFeeApproval: checkBuilderFeeApprovalMock
|
|
13
|
-
})
|
|
14
|
-
}));
|
|
15
|
-
vi.mock("../../client/hyperliquid/rest", () => ({
|
|
16
|
-
fetchUserAbstraction: fetchUserAbstractionMock
|
|
17
|
-
}));
|
|
18
|
-
const subaccountAddress = "0x1111111111111111111111111111111111111111";
|
|
19
|
-
const mainAccountAddress = "0x2222222222222222222222222222222222222222";
|
|
20
|
-
const agentWalletAddress = "0x3333333333333333333333333333333333333333";
|
|
21
|
-
const builderAddress = "0x4444444444444444444444444444444444444444";
|
|
22
|
-
function statusConfig() {
|
|
23
|
-
return {
|
|
24
|
-
sdk: {},
|
|
25
|
-
connector: "hyperliquid",
|
|
26
|
-
account: {
|
|
27
|
-
id: crypto.randomUUID(),
|
|
28
|
-
alias: "HL",
|
|
29
|
-
connector: "hyperliquid",
|
|
30
|
-
exchangeIdentifier: subaccountAddress,
|
|
31
|
-
metadata: {
|
|
32
|
-
agentWalletAddress,
|
|
33
|
-
isSubaccount: true,
|
|
34
|
-
mainAccountAddress
|
|
35
|
-
},
|
|
36
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
37
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
38
|
-
},
|
|
39
|
-
credentials: { signer_key: "sk" },
|
|
40
|
-
statusOptions: { builderAddress, builderFeeTenthsBps: 10 },
|
|
41
|
-
demo: true
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
describe("checkHyperliquidAccountStatus", () => {
|
|
45
|
-
beforeEach(() => {
|
|
46
|
-
checkApiWalletMock.mockReset().mockResolvedValue(true);
|
|
47
|
-
checkBuilderFeeApprovalMock.mockReset().mockResolvedValue(true);
|
|
48
|
-
fetchUserAbstractionMock.mockReset().mockResolvedValue("disabled");
|
|
49
|
-
});
|
|
50
|
-
it("checks subaccount approvals against the main account and mode against the trading account", async () => {
|
|
51
|
-
const status = await checkHyperliquidAccountStatus(statusConfig());
|
|
52
|
-
expect(status).toEqual({ status: "active", reasons: [] });
|
|
53
|
-
expect(fetchUserAbstractionMock).toHaveBeenCalledWith(subaccountAddress, true);
|
|
54
|
-
expect(checkApiWalletMock).toHaveBeenCalledWith({ userAddress: mainAccountAddress, agentWalletAddress });
|
|
55
|
-
expect(checkBuilderFeeApprovalMock).toHaveBeenCalledWith(mainAccountAddress, builderAddress, 10);
|
|
56
|
-
});
|
|
57
|
-
it.each(["dexAbstraction", "somethingNew"])(
|
|
58
|
-
"warns when account mode %s cannot be represented safely",
|
|
59
|
-
async (abstraction) => {
|
|
60
|
-
fetchUserAbstractionMock.mockResolvedValue(abstraction);
|
|
61
|
-
const status = await checkHyperliquidAccountStatus(statusConfig());
|
|
62
|
-
expect(status).toEqual({
|
|
63
|
-
status: "warning",
|
|
64
|
-
reasons: ["hyperliquid_account_mode_unsupported"]
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
);
|
|
68
|
-
it("treats Portfolio Margin as a supported account mode", async () => {
|
|
69
|
-
fetchUserAbstractionMock.mockResolvedValue("portfolioMargin");
|
|
70
|
-
const status = await checkHyperliquidAccountStatus(statusConfig());
|
|
71
|
-
expect(status).toEqual({ status: "active", reasons: [] });
|
|
72
|
-
});
|
|
73
|
-
it("distinguishes an account-mode API failure from an unsupported mode", async () => {
|
|
74
|
-
fetchUserAbstractionMock.mockRejectedValue(new Error("503"));
|
|
75
|
-
const status = await checkHyperliquidAccountStatus(statusConfig());
|
|
76
|
-
expect(status).toEqual({
|
|
77
|
-
status: "warning",
|
|
78
|
-
reasons: ["hyperliquid_account_mode_check_failed"]
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
it("fails closed when the required builder fee is missing", async () => {
|
|
82
|
-
const config = statusConfig();
|
|
83
|
-
config.statusOptions = { builderAddress };
|
|
84
|
-
await expect(checkHyperliquidAccountStatus(config)).rejects.toThrow("Missing required hyperliquid connect options");
|
|
85
|
-
});
|
|
86
|
-
it("skips the builder-fee check when no builder address is configured, keeping the other checks", async () => {
|
|
87
|
-
const config = statusConfig();
|
|
88
|
-
config.statusOptions = { builderFeeTenthsBps: 10 };
|
|
89
|
-
const status = await checkHyperliquidAccountStatus(config);
|
|
90
|
-
expect(status).toEqual({ status: "active", reasons: [] });
|
|
91
|
-
expect(checkBuilderFeeApprovalMock).not.toHaveBeenCalled();
|
|
92
|
-
expect(checkApiWalletMock).toHaveBeenCalled();
|
|
93
|
-
});
|
|
94
|
-
});
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { vi, describe, beforeEach, it, expect } from 'vitest';
|
|
2
|
-
import { checkLighterAccountStatus } from './status';
|
|
3
|
-
|
|
4
|
-
const { checkIntegratorApprovalMock, createAuthTokenMock, createExchangeHelperMock, fetchAccountMetadataMock } = vi.hoisted(() => ({
|
|
5
|
-
checkIntegratorApprovalMock: vi.fn(),
|
|
6
|
-
createAuthTokenMock: vi.fn(),
|
|
7
|
-
createExchangeHelperMock: vi.fn(),
|
|
8
|
-
fetchAccountMetadataMock: vi.fn()
|
|
9
|
-
}));
|
|
10
|
-
vi.mock("../../client/lighter/rest", () => ({ fetchAccountMetadata: fetchAccountMetadataMock }));
|
|
11
|
-
vi.mock("../../helper/factory", () => ({ createExchangeHelper: createExchangeHelperMock }));
|
|
12
|
-
function config() {
|
|
13
|
-
return {
|
|
14
|
-
sdk: {
|
|
15
|
-
core: {
|
|
16
|
-
misc: {
|
|
17
|
-
lighter: {
|
|
18
|
-
createAuthToken: createAuthTokenMock
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
},
|
|
23
|
-
connector: "lighter",
|
|
24
|
-
account: { exchangeIdentifier: "42" },
|
|
25
|
-
credentials: { api_key_index: 4, api_private_key: "private" },
|
|
26
|
-
statusOptions: {
|
|
27
|
-
integratorAccountIndex: 77,
|
|
28
|
-
integratorMinPerpsMakerFee: 500,
|
|
29
|
-
integratorMinPerpsTakerFee: 600
|
|
30
|
-
},
|
|
31
|
-
demo: true
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
describe("checkLighterAccountStatus", () => {
|
|
35
|
-
beforeEach(() => {
|
|
36
|
-
createAuthTokenMock.mockReset().mockResolvedValue({ token: "token" });
|
|
37
|
-
fetchAccountMetadataMock.mockReset().mockResolvedValue(void 0);
|
|
38
|
-
checkIntegratorApprovalMock.mockReset().mockResolvedValue(true);
|
|
39
|
-
createExchangeHelperMock.mockReset().mockReturnValue({
|
|
40
|
-
checkIntegratorApproval: checkIntegratorApprovalMock
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
it("fails demo auth closed without minting a mainnet token and still checks testnet approval", async () => {
|
|
44
|
-
await expect(checkLighterAccountStatus(config())).resolves.toEqual({
|
|
45
|
-
status: "warning",
|
|
46
|
-
reasons: ["lighter_api_key_invalid"]
|
|
47
|
-
});
|
|
48
|
-
expect(createAuthTokenMock).not.toHaveBeenCalled();
|
|
49
|
-
expect(fetchAccountMetadataMock).not.toHaveBeenCalled();
|
|
50
|
-
expect(createExchangeHelperMock).toHaveBeenCalledWith(expect.objectContaining({ demo: true }));
|
|
51
|
-
expect(checkIntegratorApprovalMock).toHaveBeenCalledWith(42, 77, {
|
|
52
|
-
minPerpsMakerFee: 500,
|
|
53
|
-
minPerpsTakerFee: 600
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
it("authenticates mainnet status without unsupported payload fields", async () => {
|
|
57
|
-
const mainnetConfig = config();
|
|
58
|
-
mainnetConfig.demo = false;
|
|
59
|
-
await expect(checkLighterAccountStatus(mainnetConfig)).resolves.toEqual({ status: "active", reasons: [] });
|
|
60
|
-
expect(createAuthTokenMock).toHaveBeenCalledWith({
|
|
61
|
-
accountIndex: 42,
|
|
62
|
-
apiKeyIndex: 4,
|
|
63
|
-
apiPrivateKey: "private"
|
|
64
|
-
});
|
|
65
|
-
expect(fetchAccountMetadataMock).toHaveBeenCalledWith(42, "token", false);
|
|
66
|
-
});
|
|
67
|
-
it("fails closed when required approval thresholds are absent", async () => {
|
|
68
|
-
const missingThresholds = config();
|
|
69
|
-
missingThresholds.statusOptions = { integratorAccountIndex: 77 };
|
|
70
|
-
await expect(checkLighterAccountStatus(missingThresholds)).rejects.toThrow(
|
|
71
|
-
"Missing required lighter connect options"
|
|
72
|
-
);
|
|
73
|
-
expect(fetchAccountMetadataMock).not.toHaveBeenCalled();
|
|
74
|
-
});
|
|
75
|
-
it("skips the integrator check when no integrator is configured, keeping the api-key check", async () => {
|
|
76
|
-
const noIntegrator = config();
|
|
77
|
-
noIntegrator.demo = false;
|
|
78
|
-
noIntegrator.statusOptions = {
|
|
79
|
-
integratorMinPerpsMakerFee: 500,
|
|
80
|
-
integratorMinPerpsTakerFee: 600
|
|
81
|
-
};
|
|
82
|
-
await expect(checkLighterAccountStatus(noIntegrator)).resolves.toEqual({ status: "active", reasons: [] });
|
|
83
|
-
expect(checkIntegratorApprovalMock).not.toHaveBeenCalled();
|
|
84
|
-
expect(fetchAccountMetadataMock).toHaveBeenCalled();
|
|
85
|
-
});
|
|
86
|
-
});
|
package/dist/syncer/base.test.js
DELETED
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
import { FetchError } from '@pear-protocol/core-sdk';
|
|
2
|
-
import { describe, afterEach, vi, it, expect } from 'vitest';
|
|
3
|
-
import { BaseAccountSyncer } from './base';
|
|
4
|
-
|
|
5
|
-
function extFill(overrides = {}) {
|
|
6
|
-
return {
|
|
7
|
-
exchangeFillId: "tx-default",
|
|
8
|
-
cloid: null,
|
|
9
|
-
connector: "hyperliquid",
|
|
10
|
-
symbol: "BTC",
|
|
11
|
-
price: "100",
|
|
12
|
-
quantity: "1",
|
|
13
|
-
side: "BUY",
|
|
14
|
-
reduceOnly: false,
|
|
15
|
-
timestamp: 1e3,
|
|
16
|
-
tradeFee: "0",
|
|
17
|
-
builderFee: "0",
|
|
18
|
-
liquidation: null,
|
|
19
|
-
...overrides
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
function exchangePos(overrides = {}) {
|
|
23
|
-
return { symbol: "BTC", size: "1", side: "BUY", entryPrice: "100", markPrice: "100", ...overrides };
|
|
24
|
-
}
|
|
25
|
-
class FakeSyncer extends BaseAccountSyncer {
|
|
26
|
-
positionsFn = vi.fn();
|
|
27
|
-
fillsFn = vi.fn();
|
|
28
|
-
listExternalPositions(params) {
|
|
29
|
-
return this.positionsFn(params);
|
|
30
|
-
}
|
|
31
|
-
listExternalFills(params) {
|
|
32
|
-
return this.fillsFn(params);
|
|
33
|
-
}
|
|
34
|
-
listExternalFunding() {
|
|
35
|
-
return Promise.resolve([]);
|
|
36
|
-
}
|
|
37
|
-
isReduceOnly() {
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
function makeSyncer(args) {
|
|
42
|
-
const fills = {
|
|
43
|
-
since: vi.fn().mockResolvedValue({
|
|
44
|
-
sinceMs: 0,
|
|
45
|
-
neverSynced: args.neverSynced,
|
|
46
|
-
firstSyncStartedAt: args.neverSynced ? FIRST_SYNC_STARTED_AT : void 0,
|
|
47
|
-
confirmedExchangeFillIds: []
|
|
48
|
-
}),
|
|
49
|
-
reconcile: vi.fn().mockResolvedValue({}),
|
|
50
|
-
confirm: vi.fn().mockResolvedValue({}),
|
|
51
|
-
create: vi.fn().mockResolvedValue({}),
|
|
52
|
-
createBulk: vi.fn().mockResolvedValue({})
|
|
53
|
-
};
|
|
54
|
-
const sync = { fills };
|
|
55
|
-
const sdk = {
|
|
56
|
-
core: {
|
|
57
|
-
positions: { openLite: vi.fn().mockResolvedValue({ positions: [] }) },
|
|
58
|
-
fills: { provisionals: vi.fn().mockResolvedValue({ fills: [] }), details: vi.fn() },
|
|
59
|
-
instruments: { lite: vi.fn().mockResolvedValue({ instruments: [] }) },
|
|
60
|
-
sync
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
const config = {
|
|
64
|
-
sdk,
|
|
65
|
-
connector: "hyperliquid",
|
|
66
|
-
account: { id: "acct-1" },
|
|
67
|
-
credentials: {},
|
|
68
|
-
demo: false
|
|
69
|
-
};
|
|
70
|
-
return { syncer: new FakeSyncer(config), sync };
|
|
71
|
-
}
|
|
72
|
-
const NEWEST_TS = 105e4;
|
|
73
|
-
const FIRST_SYNC_STARTED_AT = 1e6;
|
|
74
|
-
function trimmableFills(btcEraTs) {
|
|
75
|
-
return [
|
|
76
|
-
extFill({ exchangeFillId: "a", symbol: "BTC", side: "BUY", quantity: "1", timestamp: btcEraTs.open }),
|
|
77
|
-
extFill({ exchangeFillId: "b", symbol: "BTC", side: "SELL", quantity: "1", timestamp: btcEraTs.close }),
|
|
78
|
-
extFill({ exchangeFillId: "e", symbol: "SOL", side: "BUY", quantity: "2", timestamp: NEWEST_TS })
|
|
79
|
-
];
|
|
80
|
-
}
|
|
81
|
-
const SOL_SNAPSHOT = [exchangePos({ symbol: "SOL", size: "2" })];
|
|
82
|
-
describe("BaseAccountSyncer \u2014 first-sync plumbing", () => {
|
|
83
|
-
afterEach(() => {
|
|
84
|
-
vi.restoreAllMocks();
|
|
85
|
-
});
|
|
86
|
-
it("neverSynced=true: the posted snapshot is fetched AFTER the fill stream", async () => {
|
|
87
|
-
const { syncer, sync } = makeSyncer({ neverSynced: true });
|
|
88
|
-
syncer.positionsFn.mockResolvedValue(SOL_SNAPSHOT);
|
|
89
|
-
syncer.fillsFn.mockResolvedValue([
|
|
90
|
-
extFill({ exchangeFillId: "e", symbol: "SOL", side: "BUY", quantity: "2", timestamp: NEWEST_TS })
|
|
91
|
-
]);
|
|
92
|
-
await syncer.synchronizeFills();
|
|
93
|
-
expect(syncer.positionsFn).toHaveBeenCalledTimes(1);
|
|
94
|
-
expect(syncer.fillsFn.mock.invocationCallOrder[0]).toBeLessThan(
|
|
95
|
-
syncer.positionsFn.mock.invocationCallOrder[0] ?? 0
|
|
96
|
-
);
|
|
97
|
-
expect(sync.fills.create).toHaveBeenCalledTimes(1);
|
|
98
|
-
expect(sync.fills.create.mock.calls[0]?.[0]?.exchangePositions).toBe(SOL_SNAPSHOT);
|
|
99
|
-
});
|
|
100
|
-
it("captures an empty first-sync checkpoint before fetching fills", async () => {
|
|
101
|
-
const { syncer, sync } = makeSyncer({ neverSynced: true });
|
|
102
|
-
syncer.positionsFn.mockResolvedValue([]);
|
|
103
|
-
syncer.fillsFn.mockResolvedValue([]);
|
|
104
|
-
await syncer.synchronizeFills();
|
|
105
|
-
expect(sync.fills.since.mock.invocationCallOrder[0]).toBeLessThan(syncer.fillsFn.mock.invocationCallOrder[0] ?? 0);
|
|
106
|
-
expect(sync.fills.create.mock.calls[0]?.[0]?.meta).toEqual({
|
|
107
|
-
externalMaxTs: null,
|
|
108
|
-
firstSyncStartedAt: FIRST_SYNC_STARTED_AT
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
it("bumps the posted watermark past the overlap window when trimmed fills fall inside it", async () => {
|
|
112
|
-
const { syncer, sync } = makeSyncer({ neverSynced: true });
|
|
113
|
-
syncer.positionsFn.mockResolvedValue(SOL_SNAPSHOT);
|
|
114
|
-
syncer.fillsFn.mockResolvedValue(trimmableFills({ open: 1e6, close: NEWEST_TS - 2e4 }));
|
|
115
|
-
await syncer.synchronizeFills();
|
|
116
|
-
expect(sync.fills.create.mock.calls[0]?.[0]?.meta.externalMaxTs).toBe(NEWEST_TS + 6e4 + 1);
|
|
117
|
-
});
|
|
118
|
-
it("carries the raw newest fill untouched next to a bumped watermark", async () => {
|
|
119
|
-
const { syncer, sync } = makeSyncer({ neverSynced: true });
|
|
120
|
-
syncer.positionsFn.mockResolvedValue(SOL_SNAPSHOT);
|
|
121
|
-
syncer.fillsFn.mockResolvedValue(trimmableFills({ open: 1e6, close: NEWEST_TS - 2e4 }));
|
|
122
|
-
await syncer.synchronizeFills();
|
|
123
|
-
expect(sync.fills.create.mock.calls[0]?.[0]?.meta.newestFillTs).toBe(NEWEST_TS);
|
|
124
|
-
});
|
|
125
|
-
it("leaves the watermark unbumped when the trimmed era is entirely older than the overlap window", async () => {
|
|
126
|
-
const { syncer, sync } = makeSyncer({ neverSynced: true });
|
|
127
|
-
syncer.positionsFn.mockResolvedValue(SOL_SNAPSHOT);
|
|
128
|
-
syncer.fillsFn.mockResolvedValue(trimmableFills({ open: 1e5, close: 2e5 }));
|
|
129
|
-
await syncer.synchronizeFills();
|
|
130
|
-
expect(sync.fills.create.mock.calls[0]?.[0]?.meta.externalMaxTs).toBe(NEWEST_TS);
|
|
131
|
-
});
|
|
132
|
-
it("logs the trim exactly once when a trim happened", async () => {
|
|
133
|
-
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {
|
|
134
|
-
});
|
|
135
|
-
const { syncer } = makeSyncer({ neverSynced: true });
|
|
136
|
-
syncer.positionsFn.mockResolvedValue(SOL_SNAPSHOT);
|
|
137
|
-
syncer.fillsFn.mockResolvedValue(trimmableFills({ open: 1e5, close: 2e5 }));
|
|
138
|
-
await syncer.synchronizeFills();
|
|
139
|
-
const trimLogs = logSpy.mock.calls.filter((call) => String(call[0]).includes("first-sync trim"));
|
|
140
|
-
expect(trimLogs).toHaveLength(1);
|
|
141
|
-
expect(String(trimLogs[0]?.[0])).toContain("hyperliquid");
|
|
142
|
-
});
|
|
143
|
-
it("swallows a SYNC_CREATE_* deferral so the next tick retries the same window", async () => {
|
|
144
|
-
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {
|
|
145
|
-
});
|
|
146
|
-
const { syncer, sync } = makeSyncer({ neverSynced: false });
|
|
147
|
-
syncer.positionsFn.mockResolvedValue(SOL_SNAPSHOT);
|
|
148
|
-
syncer.fillsFn.mockResolvedValue([
|
|
149
|
-
extFill({ exchangeFillId: "e", symbol: "SOL", side: "BUY", quantity: "2", timestamp: NEWEST_TS })
|
|
150
|
-
]);
|
|
151
|
-
sync.fills.create.mockRejectedValue(new FetchError("deferred", "Conflict", 409, "SYNC_CREATE_EXECUTION_IN_FLIGHT"));
|
|
152
|
-
await expect(syncer.synchronizeFills()).resolves.toBeUndefined();
|
|
153
|
-
const deferLogs = logSpy.mock.calls.filter((call) => String(call[0]).includes("SYNC_CREATE_EXECUTION_IN_FLIGHT"));
|
|
154
|
-
expect(deferLogs).toHaveLength(1);
|
|
155
|
-
});
|
|
156
|
-
it("propagates a 409 that is not a sync-create deferral", async () => {
|
|
157
|
-
const { syncer, sync } = makeSyncer({ neverSynced: false });
|
|
158
|
-
syncer.positionsFn.mockResolvedValue(SOL_SNAPSHOT);
|
|
159
|
-
syncer.fillsFn.mockResolvedValue([
|
|
160
|
-
extFill({ exchangeFillId: "e", symbol: "SOL", side: "BUY", quantity: "2", timestamp: NEWEST_TS })
|
|
161
|
-
]);
|
|
162
|
-
sync.fills.create.mockRejectedValue(new FetchError("conflict", "Conflict", 409, "NOOP_ERROR"));
|
|
163
|
-
await expect(syncer.synchronizeFills()).rejects.toThrow("conflict");
|
|
164
|
-
});
|
|
165
|
-
it("neverSynced=false: positions fetched once, no bump, no trim log", async () => {
|
|
166
|
-
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {
|
|
167
|
-
});
|
|
168
|
-
const { syncer, sync } = makeSyncer({ neverSynced: false });
|
|
169
|
-
syncer.positionsFn.mockResolvedValue(SOL_SNAPSHOT);
|
|
170
|
-
syncer.fillsFn.mockResolvedValue([
|
|
171
|
-
extFill({ exchangeFillId: "e", symbol: "SOL", side: "BUY", quantity: "2", timestamp: NEWEST_TS })
|
|
172
|
-
]);
|
|
173
|
-
await syncer.synchronizeFills();
|
|
174
|
-
expect(syncer.positionsFn).toHaveBeenCalledTimes(1);
|
|
175
|
-
expect(sync.fills.create.mock.calls[0]?.[0]?.meta).toEqual({ externalMaxTs: NEWEST_TS, newestFillTs: NEWEST_TS });
|
|
176
|
-
expect(logSpy).not.toHaveBeenCalled();
|
|
177
|
-
});
|
|
178
|
-
});
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { toFundingPaymentEntries } from './funding';
|
|
3
|
-
|
|
4
|
-
const fundingEvent = (over) => ({
|
|
5
|
-
time: over.time,
|
|
6
|
-
hash: "0xabc",
|
|
7
|
-
delta: {
|
|
8
|
-
type: "funding",
|
|
9
|
-
coin: over.coin,
|
|
10
|
-
usdc: over.usdc ?? "1.5",
|
|
11
|
-
szi: over.szi ?? "10",
|
|
12
|
-
fundingRate: over.fundingRate,
|
|
13
|
-
nSamples: 1
|
|
14
|
-
}
|
|
15
|
-
});
|
|
16
|
-
describe("toFundingPaymentEntries", () => {
|
|
17
|
-
const map = {
|
|
18
|
-
BTC: "HL-BTC",
|
|
19
|
-
ETH: "HL-ETH"
|
|
20
|
-
};
|
|
21
|
-
it("maps a funding event to a funding-payment entry (rate + notional + per-size)", () => {
|
|
22
|
-
const result = toFundingPaymentEntries(
|
|
23
|
-
[fundingEvent({ coin: "BTC", fundingRate: "-0.0001", usdc: "-2.5", szi: "5", time: 1e3 })],
|
|
24
|
-
map
|
|
25
|
-
);
|
|
26
|
-
expect(result).toEqual([
|
|
27
|
-
{
|
|
28
|
-
connector: "hyperliquid",
|
|
29
|
-
instrumentId: "HL-BTC",
|
|
30
|
-
fundingRate: "-0.0001",
|
|
31
|
-
notional: "-2.5",
|
|
32
|
-
// notional / szi = -2.5 / 5
|
|
33
|
-
fundingPerUnit: "-0.5",
|
|
34
|
-
timestamp: 1e3
|
|
35
|
-
}
|
|
36
|
-
]);
|
|
37
|
-
});
|
|
38
|
-
it("uses signed division by szi (per-size is the attribution coefficient)", () => {
|
|
39
|
-
const result = toFundingPaymentEntries(
|
|
40
|
-
[fundingEvent({ coin: "BTC", fundingRate: "0.0001", usdc: "2.5", szi: "-5", time: 1e3 })],
|
|
41
|
-
map
|
|
42
|
-
);
|
|
43
|
-
expect(result[0]?.fundingPerUnit).toBe("-0.5");
|
|
44
|
-
});
|
|
45
|
-
it("uses zero per-size when size is zero (no divide by zero)", () => {
|
|
46
|
-
const result = toFundingPaymentEntries(
|
|
47
|
-
[fundingEvent({ coin: "BTC", fundingRate: "0.0001", usdc: "0", szi: "0", time: 1e3 })],
|
|
48
|
-
map
|
|
49
|
-
);
|
|
50
|
-
expect(result[0]?.fundingPerUnit).toBe("0");
|
|
51
|
-
});
|
|
52
|
-
it("skips events whose coin is not in the instrument map", () => {
|
|
53
|
-
const result = toFundingPaymentEntries(
|
|
54
|
-
[
|
|
55
|
-
fundingEvent({ coin: "BTC", fundingRate: "0.0001", time: 1e3 }),
|
|
56
|
-
fundingEvent({ coin: "DOGE", fundingRate: "0.0002", time: 2e3 }),
|
|
57
|
-
fundingEvent({ coin: "ETH", fundingRate: "0.0003", time: 3e3 })
|
|
58
|
-
],
|
|
59
|
-
map
|
|
60
|
-
);
|
|
61
|
-
expect(result.map((entry) => entry.instrumentId)).toEqual(["HL-BTC", "HL-ETH"]);
|
|
62
|
-
});
|
|
63
|
-
});
|