@pear-protocol/exchanges-sdk 0.6.1 → 0.6.3
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
CHANGED
|
@@ -101,7 +101,7 @@ const exchanges = new ExchangesSDK({
|
|
|
101
101
|
|
|
102
102
|
Hyperliquid status requires the builder address and the minimum approved fee in tenths of a basis point. Lighter status requires the integrator account and minimum maker/taker approval caps in Lighter fee ticks (parts per million). These thresholds must match the fees configured by the trading backend.
|
|
103
103
|
|
|
104
|
-
Historical Hyperliquid fill synchronization
|
|
104
|
+
Historical Hyperliquid fill synchronization identifies fills by bare `tid` and requests them aggregated by time, so partial fills of one crossing order arrive as a single fill. Consumers that persisted composite `time:coin:tid` identifiers must account for the transition overlap. Hyperliquid serves only the 10,000 most recent fills; an account with a longer history imports from that boundary forward, and the snapshot-alignment synthetics cover the residual gap. Synchronization fails closed when the exchange reports a maker rebate: the current core external-fill contract accepts only non-negative fees, so the SDK will not silently turn a rebate into a cost or advance the synchronization watermark past it.
|
|
105
105
|
|
|
106
106
|
## Account Status
|
|
107
107
|
|
|
@@ -4,7 +4,6 @@ import { BaseAccountSyncer } from '../base';
|
|
|
4
4
|
import { toFundingPaymentEntries } from './funding';
|
|
5
5
|
|
|
6
6
|
const HYPERLIQUID_FILL_PAGE_LIMIT = 2e3;
|
|
7
|
-
const HYPERLIQUID_FILL_RETENTION_LIMIT = 1e4;
|
|
8
7
|
class HyperliquidAccountSyncer extends BaseAccountSyncer {
|
|
9
8
|
address;
|
|
10
9
|
constructor(config) {
|
|
@@ -65,12 +64,10 @@ class HyperliquidAccountSyncer extends BaseAccountSyncer {
|
|
|
65
64
|
return acc;
|
|
66
65
|
}, {});
|
|
67
66
|
const allFills = [];
|
|
68
|
-
const
|
|
69
|
-
let oldestReachableFillTime = null;
|
|
67
|
+
const seenTids = /* @__PURE__ */ new Set();
|
|
70
68
|
const addFill = (fill) => {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
seenFillIds.add(fillId);
|
|
69
|
+
if (!seenTids.has(fill.tid)) {
|
|
70
|
+
seenTids.add(fill.tid);
|
|
74
71
|
allFills.push(fill);
|
|
75
72
|
}
|
|
76
73
|
};
|
|
@@ -82,22 +79,14 @@ class HyperliquidAccountSyncer extends BaseAccountSyncer {
|
|
|
82
79
|
type: "userFillsByTime",
|
|
83
80
|
user: this.address,
|
|
84
81
|
startTime: cursor,
|
|
85
|
-
aggregateByTime:
|
|
82
|
+
aggregateByTime: true
|
|
86
83
|
},
|
|
87
84
|
this.demo
|
|
88
85
|
);
|
|
89
86
|
batchSize = batch.length;
|
|
90
|
-
if (batch.length > 0 && oldestReachableFillTime === null) {
|
|
91
|
-
oldestReachableFillTime = Math.min(...batch.map((fill) => fill.time));
|
|
92
|
-
}
|
|
93
87
|
for (const rawFill of batch) {
|
|
94
88
|
addFill(rawFill);
|
|
95
89
|
}
|
|
96
|
-
if (seenFillIds.size >= HYPERLIQUID_FILL_RETENTION_LIMIT && oldestReachableFillTime !== null && since.getTime() < oldestReachableFillTime) {
|
|
97
|
-
throw new Error(
|
|
98
|
-
`Hyperliquid fill history is incomplete: requested since ${since.getTime()} predates oldest reachable fill ${oldestReachableFillTime} within the ${HYPERLIQUID_FILL_RETENTION_LIMIT}-fill retention limit.`
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
90
|
if (batchSize === HYPERLIQUID_FILL_PAGE_LIMIT) {
|
|
102
91
|
const nextCursor = batch[batch.length - 1]?.time ?? cursor;
|
|
103
92
|
if (nextCursor <= cursor) {
|
|
@@ -114,7 +103,7 @@ class HyperliquidAccountSyncer extends BaseAccountSyncer {
|
|
|
114
103
|
type: "userTwapSliceFillsByTime",
|
|
115
104
|
user: this.address,
|
|
116
105
|
startTime: cursor,
|
|
117
|
-
aggregateByTime:
|
|
106
|
+
aggregateByTime: true
|
|
118
107
|
},
|
|
119
108
|
this.demo
|
|
120
109
|
);
|
|
@@ -141,12 +130,12 @@ class HyperliquidAccountSyncer extends BaseAccountSyncer {
|
|
|
141
130
|
const tradeFee = new BigNumber(rawFill.fee).minus(rawFill.builderFee ?? "0");
|
|
142
131
|
if (tradeFee.isNegative()) {
|
|
143
132
|
throw new Error(
|
|
144
|
-
`Hyperliquid fill ${
|
|
133
|
+
`Hyperliquid fill ${rawFill.tid} has a negative net trade fee (${tradeFee.toFixed()}); the backend external-fill contract does not accept Hyperliquid rebates.`
|
|
145
134
|
);
|
|
146
135
|
}
|
|
147
136
|
fills.push({
|
|
148
137
|
connector: "hyperliquid",
|
|
149
|
-
exchangeFillId:
|
|
138
|
+
exchangeFillId: rawFill.tid.toFixed(),
|
|
150
139
|
cloid: rawFill.cloid ?? null,
|
|
151
140
|
symbol: instrumentId,
|
|
152
141
|
price: rawFill.px,
|
|
@@ -201,9 +190,6 @@ class HyperliquidAccountSyncer extends BaseAccountSyncer {
|
|
|
201
190
|
return rawFill.dir.includes("Close");
|
|
202
191
|
}
|
|
203
192
|
}
|
|
204
|
-
function hyperliquidFillId(fill) {
|
|
205
|
-
return `${fill.time}:${fill.coin}:${fill.tid}`;
|
|
206
|
-
}
|
|
207
193
|
function warnStalledPagination(stream, cursor) {
|
|
208
194
|
console.warn(`Hyperliquid ${stream} pagination stalled at timestamp ${cursor}; returned history may be incomplete.`);
|
|
209
195
|
}
|
|
@@ -77,18 +77,18 @@ describe("HyperliquidAccountSyncer fill mapping", () => {
|
|
|
77
77
|
});
|
|
78
78
|
expect(fills).toEqual([
|
|
79
79
|
expect.objectContaining({
|
|
80
|
-
exchangeFillId: "
|
|
80
|
+
exchangeFillId: "456",
|
|
81
81
|
tradeFee: "0.8",
|
|
82
82
|
builderFee: "0.2"
|
|
83
83
|
})
|
|
84
84
|
]);
|
|
85
85
|
expect(fetchInfoMock).toHaveBeenCalledWith(expect.objectContaining({ type: "userFillsByTime" }), true);
|
|
86
86
|
expect(fetchInfoMock).toHaveBeenCalledWith(
|
|
87
|
-
expect.objectContaining({ type: "userFillsByTime", aggregateByTime:
|
|
87
|
+
expect.objectContaining({ type: "userFillsByTime", aggregateByTime: true }),
|
|
88
88
|
true
|
|
89
89
|
);
|
|
90
90
|
expect(fetchInfoMock).toHaveBeenCalledWith(
|
|
91
|
-
expect.objectContaining({ type: "userTwapSliceFillsByTime", aggregateByTime:
|
|
91
|
+
expect.objectContaining({ type: "userTwapSliceFillsByTime", aggregateByTime: true }),
|
|
92
92
|
true
|
|
93
93
|
);
|
|
94
94
|
});
|
|
@@ -125,7 +125,7 @@ describe("HyperliquidAccountSyncer fill mapping", () => {
|
|
|
125
125
|
provisionalFills: []
|
|
126
126
|
})
|
|
127
127
|
).rejects.toThrow(
|
|
128
|
-
"Hyperliquid fill
|
|
128
|
+
"Hyperliquid fill 456 has a negative net trade fee (-0.1); the backend external-fill contract does not accept Hyperliquid rebates."
|
|
129
129
|
);
|
|
130
130
|
});
|
|
131
131
|
it("warns when inclusive fill pagination cannot move past the current timestamp", async () => {
|
|
@@ -164,29 +164,26 @@ describe("HyperliquidAccountSyncer fill mapping", () => {
|
|
|
164
164
|
expect(fetchInfoMock).toHaveBeenCalledTimes(2);
|
|
165
165
|
consoleWarn.mockRestore();
|
|
166
166
|
});
|
|
167
|
-
it("
|
|
167
|
+
it("imports the reachable history of a never-synced account whose older fills fell out of retention", async () => {
|
|
168
168
|
let page = 0;
|
|
169
169
|
fetchInfoMock.mockImplementation(async (body) => {
|
|
170
|
-
if (body.type
|
|
170
|
+
if (body.type === "userTwapSliceFillsByTime") return [];
|
|
171
|
+
if (page >= 5) return [];
|
|
171
172
|
const firstTime = 1e3 + page * 2e3;
|
|
172
173
|
page += 1;
|
|
173
174
|
return Array.from({ length: 2e3 }, (_, index) => rawFill(firstTime + index));
|
|
174
175
|
});
|
|
175
176
|
const syncer = createSyncer();
|
|
176
|
-
await
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
).
|
|
184
|
-
"Hyperliquid fill history is incomplete: requested since 0 predates oldest reachable fill 1000 within the 10000-fill retention limit."
|
|
185
|
-
);
|
|
186
|
-
expect(fetchInfoMock).toHaveBeenCalledTimes(5);
|
|
187
|
-
expect(fetchInfoMock).toHaveBeenCalledWith(expect.objectContaining({ aggregateByTime: false }), true);
|
|
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);
|
|
188
185
|
});
|
|
189
|
-
it("
|
|
186
|
+
it("pages through a full retention window of fills", async () => {
|
|
190
187
|
let page = 0;
|
|
191
188
|
fetchInfoMock.mockImplementation(async (body) => {
|
|
192
189
|
if (body.type === "userTwapSliceFillsByTime") return [];
|