@pear-protocol/exchanges-sdk 0.6.5 → 0.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +112 -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 +200 -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
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
import { vi, describe, beforeEach, it, expect } from 'vitest';
|
|
2
|
-
import { HyperliquidAccountSyncer } from './syncer';
|
|
3
|
-
|
|
4
|
-
const { fetchInfoMock } = vi.hoisted(() => ({ fetchInfoMock: vi.fn() }));
|
|
5
|
-
vi.mock("../../client/hyperliquid/rest", () => ({ fetchInfo: fetchInfoMock }));
|
|
6
|
-
const account = {
|
|
7
|
-
exchangeIdentifier: "0x1111111111111111111111111111111111111111"
|
|
8
|
-
};
|
|
9
|
-
const instrument = {
|
|
10
|
-
id: "HL-BTC",
|
|
11
|
-
base: "BTC"
|
|
12
|
-
};
|
|
13
|
-
function rawFill(time, tid = time) {
|
|
14
|
-
return {
|
|
15
|
-
coin: "BTC",
|
|
16
|
-
px: "100000",
|
|
17
|
-
sz: "0.1",
|
|
18
|
-
side: "B",
|
|
19
|
-
time,
|
|
20
|
-
startPosition: "0",
|
|
21
|
-
dir: "Open Long",
|
|
22
|
-
closedPnl: "0",
|
|
23
|
-
hash: "0xabc",
|
|
24
|
-
oid: 123,
|
|
25
|
-
crossed: true,
|
|
26
|
-
fee: "1",
|
|
27
|
-
builderFee: "0.2",
|
|
28
|
-
tid,
|
|
29
|
-
feeToken: "USDC",
|
|
30
|
-
twapId: null
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
function createSyncer() {
|
|
34
|
-
return new HyperliquidAccountSyncer({
|
|
35
|
-
sdk: {},
|
|
36
|
-
connector: "hyperliquid",
|
|
37
|
-
account,
|
|
38
|
-
credentials: { signer_key: "sk" },
|
|
39
|
-
demo: true
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
describe("HyperliquidAccountSyncer fill mapping", () => {
|
|
43
|
-
beforeEach(() => {
|
|
44
|
-
fetchInfoMock.mockReset().mockImplementation(async (body) => {
|
|
45
|
-
if (body.type === "userTwapSliceFillsByTime") {
|
|
46
|
-
return [];
|
|
47
|
-
}
|
|
48
|
-
return [
|
|
49
|
-
{
|
|
50
|
-
coin: "BTC",
|
|
51
|
-
px: "100000",
|
|
52
|
-
sz: "0.1",
|
|
53
|
-
side: "B",
|
|
54
|
-
time: 17e11,
|
|
55
|
-
startPosition: "0",
|
|
56
|
-
dir: "Open Long",
|
|
57
|
-
closedPnl: "0",
|
|
58
|
-
hash: "0xabc",
|
|
59
|
-
oid: 123,
|
|
60
|
-
crossed: true,
|
|
61
|
-
fee: "1",
|
|
62
|
-
builderFee: "0.2",
|
|
63
|
-
tid: 456,
|
|
64
|
-
feeToken: "USDC",
|
|
65
|
-
twapId: null
|
|
66
|
-
}
|
|
67
|
-
];
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
it("uses a globally unique identity, excludes builder fee from trade fee, and routes to testnet", async () => {
|
|
71
|
-
const syncer = createSyncer();
|
|
72
|
-
const fills = await syncer.listExternalFills({
|
|
73
|
-
since: /* @__PURE__ */ new Date(1699999e6),
|
|
74
|
-
instruments: [instrument],
|
|
75
|
-
openPositions: [],
|
|
76
|
-
provisionalFills: []
|
|
77
|
-
});
|
|
78
|
-
expect(fills).toEqual([
|
|
79
|
-
expect.objectContaining({
|
|
80
|
-
exchangeFillId: "456",
|
|
81
|
-
tradeFee: "0.8",
|
|
82
|
-
builderFee: "0.2"
|
|
83
|
-
})
|
|
84
|
-
]);
|
|
85
|
-
expect(fetchInfoMock).toHaveBeenCalledWith(expect.objectContaining({ type: "userFillsByTime" }), true);
|
|
86
|
-
expect(fetchInfoMock).toHaveBeenCalledWith(
|
|
87
|
-
expect.objectContaining({ type: "userFillsByTime", aggregateByTime: true }),
|
|
88
|
-
true
|
|
89
|
-
);
|
|
90
|
-
expect(fetchInfoMock).toHaveBeenCalledWith(
|
|
91
|
-
expect.objectContaining({ type: "userTwapSliceFillsByTime", aggregateByTime: true }),
|
|
92
|
-
true
|
|
93
|
-
);
|
|
94
|
-
});
|
|
95
|
-
it("fails closed when a maker rebate cannot satisfy the backend external-fill fee contract", async () => {
|
|
96
|
-
fetchInfoMock.mockImplementation(async (body) => {
|
|
97
|
-
if (body.type === "userTwapSliceFillsByTime") return [];
|
|
98
|
-
return [
|
|
99
|
-
{
|
|
100
|
-
coin: "BTC",
|
|
101
|
-
px: "100000",
|
|
102
|
-
sz: "0.1",
|
|
103
|
-
side: "B",
|
|
104
|
-
time: 17e11,
|
|
105
|
-
startPosition: "0",
|
|
106
|
-
dir: "Open Long",
|
|
107
|
-
closedPnl: "0",
|
|
108
|
-
hash: "0xabc",
|
|
109
|
-
oid: 123,
|
|
110
|
-
crossed: false,
|
|
111
|
-
fee: "-0.08",
|
|
112
|
-
builderFee: "0.02",
|
|
113
|
-
tid: 456,
|
|
114
|
-
feeToken: "USDC",
|
|
115
|
-
twapId: null
|
|
116
|
-
}
|
|
117
|
-
];
|
|
118
|
-
});
|
|
119
|
-
const syncer = createSyncer();
|
|
120
|
-
await expect(
|
|
121
|
-
syncer.listExternalFills({
|
|
122
|
-
since: /* @__PURE__ */ new Date(1699999e6),
|
|
123
|
-
instruments: [instrument],
|
|
124
|
-
openPositions: [],
|
|
125
|
-
provisionalFills: []
|
|
126
|
-
})
|
|
127
|
-
).rejects.toThrow(
|
|
128
|
-
"Hyperliquid fill 456 has a negative net trade fee (-0.1); the backend external-fill contract does not accept Hyperliquid rebates."
|
|
129
|
-
);
|
|
130
|
-
});
|
|
131
|
-
it("warns when inclusive fill pagination cannot move past the current timestamp", async () => {
|
|
132
|
-
const consoleWarn = vi.spyOn(console, "warn").mockImplementation(() => void 0);
|
|
133
|
-
const event = {
|
|
134
|
-
coin: "BTC",
|
|
135
|
-
px: "100000",
|
|
136
|
-
sz: "0.1",
|
|
137
|
-
side: "B",
|
|
138
|
-
time: 1e3,
|
|
139
|
-
startPosition: "0",
|
|
140
|
-
dir: "Open Long",
|
|
141
|
-
closedPnl: "0",
|
|
142
|
-
hash: "0xabc",
|
|
143
|
-
oid: 123,
|
|
144
|
-
crossed: true,
|
|
145
|
-
fee: "1",
|
|
146
|
-
builderFee: "0.2",
|
|
147
|
-
tid: 456,
|
|
148
|
-
feeToken: "USDC",
|
|
149
|
-
twapId: null
|
|
150
|
-
};
|
|
151
|
-
fetchInfoMock.mockImplementation(
|
|
152
|
-
async (body) => body.type === "userFillsByTime" ? Array.from({ length: 2e3 }, () => event) : []
|
|
153
|
-
);
|
|
154
|
-
const syncer = createSyncer();
|
|
155
|
-
await syncer.listExternalFills({
|
|
156
|
-
since: /* @__PURE__ */ new Date(1e3),
|
|
157
|
-
instruments: [instrument],
|
|
158
|
-
openPositions: [],
|
|
159
|
-
provisionalFills: []
|
|
160
|
-
});
|
|
161
|
-
expect(consoleWarn).toHaveBeenCalledWith(
|
|
162
|
-
"Hyperliquid fills pagination stalled at timestamp 1000; returned history may be incomplete."
|
|
163
|
-
);
|
|
164
|
-
expect(fetchInfoMock).toHaveBeenCalledTimes(2);
|
|
165
|
-
consoleWarn.mockRestore();
|
|
166
|
-
});
|
|
167
|
-
it("imports the reachable history of a never-synced account whose older fills fell out of retention", async () => {
|
|
168
|
-
let page = 0;
|
|
169
|
-
fetchInfoMock.mockImplementation(async (body) => {
|
|
170
|
-
if (body.type === "userTwapSliceFillsByTime") return [];
|
|
171
|
-
if (page >= 5) return [];
|
|
172
|
-
const firstTime = 1e3 + page * 2e3;
|
|
173
|
-
page += 1;
|
|
174
|
-
return Array.from({ length: 2e3 }, (_, index) => rawFill(firstTime + index));
|
|
175
|
-
});
|
|
176
|
-
const syncer = createSyncer();
|
|
177
|
-
const fills = await syncer.listExternalFills({
|
|
178
|
-
since: /* @__PURE__ */ new Date(0),
|
|
179
|
-
instruments: [instrument],
|
|
180
|
-
openPositions: [],
|
|
181
|
-
provisionalFills: []
|
|
182
|
-
});
|
|
183
|
-
expect(fills).toHaveLength(1e4);
|
|
184
|
-
expect(fetchInfoMock).toHaveBeenCalledWith(expect.objectContaining({ aggregateByTime: true }), true);
|
|
185
|
-
});
|
|
186
|
-
it("pages through a full retention window of fills", async () => {
|
|
187
|
-
let page = 0;
|
|
188
|
-
fetchInfoMock.mockImplementation(async (body) => {
|
|
189
|
-
if (body.type === "userTwapSliceFillsByTime") return [];
|
|
190
|
-
if (page >= 5) return [];
|
|
191
|
-
const firstTime = 1e3 + page * 2e3;
|
|
192
|
-
page += 1;
|
|
193
|
-
return Array.from({ length: 2e3 }, (_, index) => rawFill(firstTime + index));
|
|
194
|
-
});
|
|
195
|
-
const syncer = createSyncer();
|
|
196
|
-
const fills = await syncer.listExternalFills({
|
|
197
|
-
since: /* @__PURE__ */ new Date(1e3),
|
|
198
|
-
instruments: [instrument],
|
|
199
|
-
openPositions: [],
|
|
200
|
-
provisionalFills: []
|
|
201
|
-
});
|
|
202
|
-
expect(fills).toHaveLength(1e4);
|
|
203
|
-
expect(fetchInfoMock).toHaveBeenCalledTimes(7);
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
describe("HyperliquidAccountSyncer position and funding snapshots", () => {
|
|
207
|
-
beforeEach(() => {
|
|
208
|
-
fetchInfoMock.mockReset();
|
|
209
|
-
});
|
|
210
|
-
it("de-duplicates DEX descriptors and derives mark price from current position value", async () => {
|
|
211
|
-
fetchInfoMock.mockImplementation(async (body) => {
|
|
212
|
-
if (body.type === "perpDexs") return [null, null, { name: "xyz" }];
|
|
213
|
-
if (body.type === "clearinghouseState" && body.dex === "") {
|
|
214
|
-
return {
|
|
215
|
-
assetPositions: [
|
|
216
|
-
{
|
|
217
|
-
position: {
|
|
218
|
-
coin: "BTC",
|
|
219
|
-
szi: "2",
|
|
220
|
-
entryPx: "150",
|
|
221
|
-
positionValue: "400"
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
]
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
if (body.type === "clearinghouseState") return { assetPositions: [] };
|
|
228
|
-
throw new Error(`Unexpected request: ${body.type}`);
|
|
229
|
-
});
|
|
230
|
-
const syncer = createSyncer();
|
|
231
|
-
const positions = await syncer.listExternalPositions({
|
|
232
|
-
instruments: [
|
|
233
|
-
{
|
|
234
|
-
...instrument,
|
|
235
|
-
precision: { quantity: { contractSize: 1 } }
|
|
236
|
-
}
|
|
237
|
-
]
|
|
238
|
-
});
|
|
239
|
-
expect(positions).toEqual([
|
|
240
|
-
expect.objectContaining({ symbol: "HL-BTC", size: "2", entryPrice: "150", markPrice: "200" })
|
|
241
|
-
]);
|
|
242
|
-
expect(fetchInfoMock).toHaveBeenCalledTimes(3);
|
|
243
|
-
});
|
|
244
|
-
it("de-duplicates an inclusive funding boundary and stops when its cursor cannot advance", async () => {
|
|
245
|
-
const consoleWarn = vi.spyOn(console, "warn").mockImplementation(() => void 0);
|
|
246
|
-
const event = {
|
|
247
|
-
time: 1e3,
|
|
248
|
-
hash: "0xabc",
|
|
249
|
-
delta: {
|
|
250
|
-
type: "funding",
|
|
251
|
-
coin: "BTC",
|
|
252
|
-
usdc: "-2",
|
|
253
|
-
szi: "4",
|
|
254
|
-
fundingRate: "0.0001",
|
|
255
|
-
nSamples: null
|
|
256
|
-
}
|
|
257
|
-
};
|
|
258
|
-
fetchInfoMock.mockResolvedValue(Array.from({ length: 500 }, () => event));
|
|
259
|
-
const syncer = createSyncer();
|
|
260
|
-
const payments = await syncer.listExternalFunding({
|
|
261
|
-
since: /* @__PURE__ */ new Date(1e3),
|
|
262
|
-
instruments: [instrument]
|
|
263
|
-
});
|
|
264
|
-
expect(payments).toHaveLength(1);
|
|
265
|
-
expect(fetchInfoMock).toHaveBeenCalledTimes(1);
|
|
266
|
-
expect(consoleWarn).toHaveBeenCalledWith(
|
|
267
|
-
"Hyperliquid funding pagination stalled at timestamp 1000; returned history may be incomplete."
|
|
268
|
-
);
|
|
269
|
-
consoleWarn.mockRestore();
|
|
270
|
-
});
|
|
271
|
-
});
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { toFundingPaymentEntries } from './funding';
|
|
3
|
-
|
|
4
|
-
describe("Lighter funding mapping", () => {
|
|
5
|
-
it("derives funding per unit against signed long and short exposure", () => {
|
|
6
|
-
const result = toFundingPaymentEntries(
|
|
7
|
-
[
|
|
8
|
-
fundingEvent({ funding_id: 1, position_side: "long", change: "-2" }),
|
|
9
|
-
fundingEvent({ funding_id: 2, position_side: "short", change: "2" })
|
|
10
|
-
],
|
|
11
|
-
/* @__PURE__ */ new Set(["1"])
|
|
12
|
-
);
|
|
13
|
-
expect(result).toEqual([
|
|
14
|
-
{
|
|
15
|
-
connector: "lighter",
|
|
16
|
-
instrumentId: "1",
|
|
17
|
-
fundingRate: "0.0001",
|
|
18
|
-
notional: "-2",
|
|
19
|
-
fundingPerUnit: "-0.5",
|
|
20
|
-
timestamp: 1e6
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
connector: "lighter",
|
|
24
|
-
instrumentId: "1",
|
|
25
|
-
fundingRate: "0.0001",
|
|
26
|
-
notional: "2",
|
|
27
|
-
fundingPerUnit: "-0.5",
|
|
28
|
-
timestamp: 1e6
|
|
29
|
-
}
|
|
30
|
-
]);
|
|
31
|
-
});
|
|
32
|
-
it("skips funding for unknown instruments", () => {
|
|
33
|
-
expect(toFundingPaymentEntries([fundingEvent({})], /* @__PURE__ */ new Set())).toEqual([]);
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
function fundingEvent(overrides) {
|
|
37
|
-
return {
|
|
38
|
-
timestamp: 1e3,
|
|
39
|
-
market_id: 1,
|
|
40
|
-
funding_id: 1,
|
|
41
|
-
change: "-2",
|
|
42
|
-
discount: "0",
|
|
43
|
-
rate: "0.0001",
|
|
44
|
-
position_size: "4",
|
|
45
|
-
position_side: "long",
|
|
46
|
-
...overrides
|
|
47
|
-
};
|
|
48
|
-
}
|
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
import { vi, describe, beforeEach, it, expect } from 'vitest';
|
|
2
|
-
import { fetchTradesSince, fetchPositionFundingSince, fetchAccount } from '../../client/lighter/rest';
|
|
3
|
-
import { LighterAccountSyncer } from './syncer';
|
|
4
|
-
|
|
5
|
-
vi.mock("../../client/lighter/rest", () => ({
|
|
6
|
-
fetchAccount: vi.fn(),
|
|
7
|
-
fetchPositionFundingSince: vi.fn(),
|
|
8
|
-
fetchTradesSince: vi.fn()
|
|
9
|
-
}));
|
|
10
|
-
const ACCOUNT_INDEX = 87440;
|
|
11
|
-
const TRADES = [
|
|
12
|
-
{
|
|
13
|
-
// Real /trades row — account is taker via bid, integrator fee only:
|
|
14
|
-
// builderFee = 200/1e6 × 49.279769, tradeFee = 0.
|
|
15
|
-
trade_id: 23432371018,
|
|
16
|
-
trade_id_str: "23432371018",
|
|
17
|
-
type: "trade",
|
|
18
|
-
market_id: 1,
|
|
19
|
-
size: "0.00077",
|
|
20
|
-
price: "63999.7",
|
|
21
|
-
usd_amount: "49.279769",
|
|
22
|
-
ask_account_id: 281474976656674,
|
|
23
|
-
bid_account_id: ACCOUNT_INDEX,
|
|
24
|
-
is_maker_ask: true,
|
|
25
|
-
timestamp: 1782044759045,
|
|
26
|
-
taker_position_size_before: "0.00000",
|
|
27
|
-
maker_position_size_before: "0.00000",
|
|
28
|
-
integrator_taker_fee: 200
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
// Real /trades row — account is taker via ask, base taker fee only:
|
|
32
|
-
// tradeFee = 280/1e6 × 11.896090, builderFee = 0.
|
|
33
|
-
trade_id: 22207283246,
|
|
34
|
-
trade_id_str: "22207283246",
|
|
35
|
-
type: "trade",
|
|
36
|
-
market_id: 1,
|
|
37
|
-
size: "0.00019",
|
|
38
|
-
price: "62611.0",
|
|
39
|
-
usd_amount: "11.896090",
|
|
40
|
-
ask_account_id: ACCOUNT_INDEX,
|
|
41
|
-
bid_account_id: 587622,
|
|
42
|
-
is_maker_ask: false,
|
|
43
|
-
timestamp: 1781010160339,
|
|
44
|
-
taker_fee: 280,
|
|
45
|
-
taker_position_size_before: "0.00019",
|
|
46
|
-
maker_position_size_before: "0.00000"
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
// Real /trades row — no fee fields at all, so both fees resolve to 0.
|
|
50
|
-
trade_id: 23432472386,
|
|
51
|
-
trade_id_str: "23432472386",
|
|
52
|
-
type: "trade",
|
|
53
|
-
market_id: 1,
|
|
54
|
-
size: "0.00079",
|
|
55
|
-
price: "63973.6",
|
|
56
|
-
usd_amount: "50.539144",
|
|
57
|
-
ask_account_id: 726722,
|
|
58
|
-
bid_account_id: ACCOUNT_INDEX,
|
|
59
|
-
is_maker_ask: true,
|
|
60
|
-
timestamp: 1782044888766,
|
|
61
|
-
taker_position_size_before: "-0.00079",
|
|
62
|
-
maker_position_size_before: "0.00000"
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
// Synthetic row — the real payload only carries taker-side fees, so we construct a
|
|
66
|
-
// maker fill (account is the resting ask) to cover the maker_fee / integrator_maker_fee branch:
|
|
67
|
-
// tradeFee = 100/1e6 × 100, builderFee = 50/1e6 × 100.
|
|
68
|
-
trade_id: 90000000001,
|
|
69
|
-
trade_id_str: "90000000001",
|
|
70
|
-
type: "trade",
|
|
71
|
-
market_id: 1,
|
|
72
|
-
size: "0.00100",
|
|
73
|
-
price: "65000.0",
|
|
74
|
-
usd_amount: "100.000000",
|
|
75
|
-
ask_account_id: ACCOUNT_INDEX,
|
|
76
|
-
bid_account_id: 726722,
|
|
77
|
-
is_maker_ask: true,
|
|
78
|
-
timestamp: 178205e7,
|
|
79
|
-
maker_fee: 100,
|
|
80
|
-
taker_position_size_before: "0.00000",
|
|
81
|
-
maker_position_size_before: "0.00000",
|
|
82
|
-
integrator_maker_fee: 50
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
// A flip is not a reduce-only fill: Lighter explicitly reports that the
|
|
86
|
-
// account-side position sign changed after crossing through zero.
|
|
87
|
-
trade_id: 90000000002,
|
|
88
|
-
trade_id_str: "90000000002",
|
|
89
|
-
type: "trade",
|
|
90
|
-
market_id: 1,
|
|
91
|
-
size: "0.00200",
|
|
92
|
-
price: "65000.0",
|
|
93
|
-
usd_amount: "130.000000",
|
|
94
|
-
ask_account_id: ACCOUNT_INDEX,
|
|
95
|
-
bid_account_id: 726722,
|
|
96
|
-
is_maker_ask: false,
|
|
97
|
-
timestamp: 1782050001e3,
|
|
98
|
-
taker_position_size_before: "0.00100",
|
|
99
|
-
taker_position_sign_changed: true,
|
|
100
|
-
maker_position_size_before: "0.00000"
|
|
101
|
-
}
|
|
102
|
-
];
|
|
103
|
-
function createSyncer(demo = false, createAuthToken = vi.fn().mockResolvedValue({ token: "test-token" })) {
|
|
104
|
-
const sdk = {
|
|
105
|
-
core: { misc: { lighter: { createAuthToken } } }
|
|
106
|
-
};
|
|
107
|
-
const config = {
|
|
108
|
-
sdk,
|
|
109
|
-
connector: "lighter",
|
|
110
|
-
account: { exchangeIdentifier: String(ACCOUNT_INDEX) },
|
|
111
|
-
credentials: { api_key_index: 0, api_private_key: "test-key" },
|
|
112
|
-
ws: {},
|
|
113
|
-
demo
|
|
114
|
-
};
|
|
115
|
-
return new LighterAccountSyncer(config);
|
|
116
|
-
}
|
|
117
|
-
async function listFills(demo = false) {
|
|
118
|
-
const syncer = createSyncer(demo);
|
|
119
|
-
const params = {
|
|
120
|
-
since: /* @__PURE__ */ new Date(0),
|
|
121
|
-
instruments: [{ id: "1" }],
|
|
122
|
-
openPositions: [],
|
|
123
|
-
provisionalFills: []
|
|
124
|
-
};
|
|
125
|
-
return syncer.listExternalFills(params);
|
|
126
|
-
}
|
|
127
|
-
async function listFunding(demo = false) {
|
|
128
|
-
const syncer = createSyncer(demo);
|
|
129
|
-
const params = {
|
|
130
|
-
since: /* @__PURE__ */ new Date(1e3),
|
|
131
|
-
instruments: [{ id: "1" }]
|
|
132
|
-
};
|
|
133
|
-
return syncer.listExternalFunding(params);
|
|
134
|
-
}
|
|
135
|
-
async function listPositions() {
|
|
136
|
-
const syncer = createSyncer();
|
|
137
|
-
const params = {
|
|
138
|
-
instruments: [
|
|
139
|
-
{
|
|
140
|
-
id: "1",
|
|
141
|
-
precision: { quantity: { contractSize: 0.01 } }
|
|
142
|
-
}
|
|
143
|
-
]
|
|
144
|
-
};
|
|
145
|
-
return syncer.listExternalPositions(params);
|
|
146
|
-
}
|
|
147
|
-
const byId = (fills, id) => fills.find((fill) => fill.exchangeFillId === id);
|
|
148
|
-
describe("LighterAccountSyncer fee mapping", () => {
|
|
149
|
-
beforeEach(() => {
|
|
150
|
-
vi.mocked(fetchTradesSince).mockResolvedValue(TRADES);
|
|
151
|
-
});
|
|
152
|
-
it("computes the integrator (builder) fee as the taker ppm rate times the USD notional", async () => {
|
|
153
|
-
const fills = await listFills();
|
|
154
|
-
expect(byId(fills, "23432371018")).toMatchObject({
|
|
155
|
-
symbol: "1",
|
|
156
|
-
side: "BUY",
|
|
157
|
-
tradeFee: "0",
|
|
158
|
-
builderFee: "0.0098559538"
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
it("computes the trade fee as the base taker ppm rate times the USD notional", async () => {
|
|
162
|
-
const fills = await listFills();
|
|
163
|
-
expect(byId(fills, "22207283246")).toMatchObject({
|
|
164
|
-
symbol: "1",
|
|
165
|
-
side: "SELL",
|
|
166
|
-
tradeFee: "0.0033309052",
|
|
167
|
-
builderFee: "0"
|
|
168
|
-
});
|
|
169
|
-
});
|
|
170
|
-
it("emits zero fees when the trade carries no fee fields", async () => {
|
|
171
|
-
const fills = await listFills();
|
|
172
|
-
expect(byId(fills, "23432472386")).toMatchObject({
|
|
173
|
-
symbol: "1",
|
|
174
|
-
side: "BUY",
|
|
175
|
-
tradeFee: "0",
|
|
176
|
-
builderFee: "0"
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
it("reads the maker-side fee fields when the account is the maker", async () => {
|
|
180
|
-
const fills = await listFills();
|
|
181
|
-
expect(byId(fills, "90000000001")).toMatchObject({
|
|
182
|
-
symbol: "1",
|
|
183
|
-
side: "SELL",
|
|
184
|
-
tradeFee: "0.01",
|
|
185
|
-
builderFee: "0.005"
|
|
186
|
-
});
|
|
187
|
-
});
|
|
188
|
-
it("does not classify a position flip as reduce-only", async () => {
|
|
189
|
-
const fills = await listFills();
|
|
190
|
-
expect(byId(fills, "90000000002")).toMatchObject({
|
|
191
|
-
side: "SELL",
|
|
192
|
-
reduceOnly: false
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
describe("LighterAccountSyncer funding mapping", () => {
|
|
197
|
-
it("fetches paginated position funding with the generated auth token", async () => {
|
|
198
|
-
vi.mocked(fetchPositionFundingSince).mockResolvedValue([
|
|
199
|
-
{
|
|
200
|
-
timestamp: 1e3,
|
|
201
|
-
market_id: 1,
|
|
202
|
-
funding_id: 7,
|
|
203
|
-
change: "-2",
|
|
204
|
-
discount: "0",
|
|
205
|
-
rate: "0.0001",
|
|
206
|
-
position_size: "4",
|
|
207
|
-
position_side: "long"
|
|
208
|
-
}
|
|
209
|
-
]);
|
|
210
|
-
await expect(listFunding()).resolves.toEqual([
|
|
211
|
-
{
|
|
212
|
-
connector: "lighter",
|
|
213
|
-
instrumentId: "1",
|
|
214
|
-
fundingRate: "0.0001",
|
|
215
|
-
notional: "-2",
|
|
216
|
-
fundingPerUnit: "-0.5",
|
|
217
|
-
timestamp: 1e6
|
|
218
|
-
}
|
|
219
|
-
]);
|
|
220
|
-
expect(fetchPositionFundingSince).toHaveBeenCalledWith(ACCOUNT_INDEX, "test-token", /* @__PURE__ */ new Date(1e3), false);
|
|
221
|
-
});
|
|
222
|
-
});
|
|
223
|
-
describe("LighterAccountSyncer demo authentication boundary", () => {
|
|
224
|
-
it("rejects authenticated demo history reads before minting a mainnet token", async () => {
|
|
225
|
-
vi.clearAllMocks();
|
|
226
|
-
await expect(listFills(true)).rejects.toThrow("backend signing is mainnet-only");
|
|
227
|
-
await expect(listFunding(true)).rejects.toThrow("backend signing is mainnet-only");
|
|
228
|
-
expect(fetchTradesSince).not.toHaveBeenCalled();
|
|
229
|
-
expect(fetchPositionFundingSince).not.toHaveBeenCalled();
|
|
230
|
-
});
|
|
231
|
-
});
|
|
232
|
-
describe("LighterAccountSyncer position mapping", () => {
|
|
233
|
-
it("derives mark price from position value in normalized base units", async () => {
|
|
234
|
-
vi.mocked(fetchAccount).mockResolvedValue({
|
|
235
|
-
code: 200,
|
|
236
|
-
accounts: [
|
|
237
|
-
{
|
|
238
|
-
account_type: 0,
|
|
239
|
-
collateral: "1000",
|
|
240
|
-
total_asset_value: "1000",
|
|
241
|
-
available_balance: "900",
|
|
242
|
-
cross_asset_value: "1000",
|
|
243
|
-
cross_initial_margin_requirement: "100",
|
|
244
|
-
cross_maintenance_margin_requirement: "50",
|
|
245
|
-
positions: [
|
|
246
|
-
{
|
|
247
|
-
market_id: 1,
|
|
248
|
-
symbol: "BTC",
|
|
249
|
-
initial_margin_fraction: "10",
|
|
250
|
-
sign: -1,
|
|
251
|
-
position: "100",
|
|
252
|
-
avg_entry_price: "50000",
|
|
253
|
-
position_value: "60000",
|
|
254
|
-
unrealized_pnl: "-10000",
|
|
255
|
-
liquidation_price: "70000",
|
|
256
|
-
margin_mode: 0,
|
|
257
|
-
allocated_margin: "0"
|
|
258
|
-
}
|
|
259
|
-
]
|
|
260
|
-
}
|
|
261
|
-
]
|
|
262
|
-
});
|
|
263
|
-
await expect(listPositions()).resolves.toEqual([
|
|
264
|
-
{
|
|
265
|
-
symbol: "1",
|
|
266
|
-
size: "1",
|
|
267
|
-
side: "SELL",
|
|
268
|
-
entryPrice: "50000",
|
|
269
|
-
markPrice: "60000"
|
|
270
|
-
}
|
|
271
|
-
]);
|
|
272
|
-
expect(fetchAccount).toHaveBeenCalledWith(ACCOUNT_INDEX, false);
|
|
273
|
-
});
|
|
274
|
-
});
|