@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 uses the exchange's globally unique `time:coin:tid` identity. Existing consumers that persisted bare `tid` identifiers must account for the transition overlap. Synchronization also 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.
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 seenFillIds = /* @__PURE__ */ new Set();
69
- let oldestReachableFillTime = null;
67
+ const seenTids = /* @__PURE__ */ new Set();
70
68
  const addFill = (fill) => {
71
- const fillId = hyperliquidFillId(fill);
72
- if (!seenFillIds.has(fillId)) {
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: false
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: false
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 ${hyperliquidFillId(rawFill)} has a negative net trade fee (${tradeFee.toFixed()}); the backend external-fill contract does not accept Hyperliquid rebates.`
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: hyperliquidFillId(rawFill),
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: "1700000000000:BTC:456",
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: false }),
87
+ expect.objectContaining({ type: "userFillsByTime", aggregateByTime: true }),
88
88
  true
89
89
  );
90
90
  expect(fetchInfoMock).toHaveBeenCalledWith(
91
- expect.objectContaining({ type: "userTwapSliceFillsByTime", aggregateByTime: false }),
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 1700000000000:BTC:456 has a negative net trade fee (-0.1); the backend external-fill contract does not accept Hyperliquid rebates."
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("counts raw fills before rejecting history older than the 10,000-fill retention boundary", async () => {
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 !== "userFillsByTime") return [];
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 expect(
177
- syncer.listExternalFills({
178
- since: /* @__PURE__ */ new Date(0),
179
- instruments: [instrument],
180
- openPositions: [],
181
- provisionalFills: []
182
- })
183
- ).rejects.toThrow(
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("accepts a request exactly at the oldest reachable fill at the retention boundary", async () => {
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 [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pear-protocol/exchanges-sdk",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "Pear Protocol Exchanges SDK",
5
5
  "private": false,
6
6
  "type": "module",