Skip to content

Commit 92411f2

Browse files
committed
fix(perps): deduplicate funding records at chunk boundary timestamps
1 parent f6033dc commit 92411f2

File tree

1 file changed

+59
-23
lines changed

1 file changed

+59
-23
lines changed

app/controllers/perps/providers/HyperLiquidProvider.ts

Lines changed: 59 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5429,27 +5429,60 @@ export class HyperLiquidProvider implements PerpsProvider {
54295429
params?.accountId,
54305430
);
54315431

5432-
// Divide the full history range into 30-day windows and fetch all in
5433-
// parallel. The HyperLiquid API returns records in ascending order and
5434-
// caps each call at FUNDING_HISTORY_API_LIMIT (500) records. A single
5435-
// large window therefore returns only the oldest 500 items, silently
5436-
// dropping the most recent payments. Parallel small windows guarantee
5437-
// every window is under the cap so the latest records are always present.
5432+
// On-demand loading: the default window is one 30-day page so the
5433+
// initial fetch costs exactly 1 API call (~24 weight vs 312 previously).
5434+
// When loadMoreFunding in usePerpsTransactionHistory passes explicit
5435+
// startTime/endTime for an older 30-day page the while-loop below still
5436+
// produces exactly 1 chunk. The 365-day max lookback is enforced by the
5437+
// caller.
5438+
//
5439+
// Each chunk is fetched via fetchWindowWithAutoSplit: if a call returns
5440+
// FUNDING_HISTORY_API_LIMIT records the window has hit the API cap and
5441+
// the oldest records would be silently dropped. The function splits the
5442+
// window in half and recurses until every sub-window is under the cap,
5443+
// guaranteeing complete results regardless of position count or activity.
54385444
const finalEndTime = params?.endTime ?? Date.now();
5439-
const finalStartTime =
5440-
params?.startTime ??
5441-
finalEndTime -
5442-
PERPS_TRANSACTIONS_HISTORY_CONSTANTS.DEFAULT_FUNDING_HISTORY_DAYS *
5443-
24 *
5444-
60 *
5445-
60 *
5446-
1000;
54475445
const pageWindowMs =
54485446
PERPS_TRANSACTIONS_HISTORY_CONSTANTS.FUNDING_HISTORY_PAGE_WINDOW_DAYS *
54495447
24 *
54505448
60 *
54515449
60 *
54525450
1000;
5451+
const finalStartTime = params?.startTime ?? finalEndTime - pageWindowMs; // Default: most recent 30-day window only
5452+
5453+
// Minimum window size to bound recursion depth. The HyperLiquid funding
5454+
// interval is 8 h, so a 1-hour window holds at most a fraction of one
5455+
// event per position — well under the 500-record cap in any scenario.
5456+
const minSplitWindowMs = 60 * 60 * 1000;
5457+
const apiLimit =
5458+
PERPS_TRANSACTIONS_HISTORY_CONSTANTS.FUNDING_HISTORY_API_LIMIT;
5459+
5460+
// Fetches a single window. If the result hits the API cap the window is
5461+
// split in half and both halves are fetched in parallel, recursively,
5462+
// until every sub-window is under the cap.
5463+
const fetchWindowWithAutoSplit = async (
5464+
windowStart: number,
5465+
windowEnd: number,
5466+
): Promise<Awaited<ReturnType<typeof infoClient.userFunding>>> => {
5467+
const result = await infoClient.userFunding({
5468+
user: userAddress,
5469+
startTime: windowStart,
5470+
endTime: windowEnd,
5471+
});
5472+
const records = result ?? [];
5473+
if (
5474+
records.length >= apiLimit &&
5475+
windowEnd - windowStart > minSplitWindowMs
5476+
) {
5477+
const mid = windowStart + Math.floor((windowEnd - windowStart) / 2);
5478+
const [left, right] = await Promise.all([
5479+
fetchWindowWithAutoSplit(windowStart, mid),
5480+
fetchWindowWithAutoSplit(mid, windowEnd),
5481+
]);
5482+
return [...(left ?? []), ...(right ?? [])];
5483+
}
5484+
return records;
5485+
};
54535486

54545487
const chunks: { start: number; end: number }[] = [];
54555488
let chunkEnd = finalEndTime;
@@ -5460,21 +5493,24 @@ export class HyperLiquidProvider implements PerpsProvider {
54605493
}
54615494

54625495
const pages = await Promise.all(
5463-
chunks.map((chunk) =>
5464-
infoClient.userFunding({
5465-
user: userAddress,
5466-
startTime: chunk.start,
5467-
endTime: chunk.end,
5468-
}),
5469-
),
5496+
chunks.map((chunk) => fetchWindowWithAutoSplit(chunk.start, chunk.end)),
54705497
);
54715498

5472-
const allRaw = pages.flatMap((page) => page ?? []);
5499+
// Deduplicate by hash before sorting — adjacent chunk windows share their
5500+
// boundary timestamp (chunkEnd of chunk N === chunkStart of chunk N+1),
5501+
// and the HyperLiquid API is inclusive on both sides, so a record whose
5502+
// timestamp falls exactly on a boundary can appear in both adjacent calls.
5503+
const seen = new Set<string>();
5504+
const allRaw = pages.flat().filter((record) => {
5505+
if (seen.has(record.hash)) {return false;}
5506+
seen.add(record.hash);
5507+
return true;
5508+
});
54735509
allRaw.sort((a, b) => a.time - b.time);
54745510

54755511
this.#deps.debugLogger.log('User funding received:', {
54765512
count: allRaw.length,
5477-
pages: chunks.length,
5513+
chunks: chunks.length,
54785514
});
54795515

54805516
// Transform HyperLiquid funding to abstract Funding type

0 commit comments

Comments
 (0)