@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.
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 +116 -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 +202 -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 +63 -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 +3 -3
  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 -178
  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
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
- });
@@ -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
- });