Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
d6c6ef3
feat: use fast l2 order book subscriptions
Minnzen Jul 14, 2026
6dcd042
fix: preserve order book identity during transitions
Minnzen Jul 14, 2026
2f60fb7
fix: refresh order book when precision changes
Minnzen Jul 14, 2026
3126d14
Merge branch 'release/v6.5.0' into codex/fix-hyperliquid-fast-l2-v6.5.0
Minnzen Jul 14, 2026
f045393
Merge branch 'release/v6.5.0' into codex/fix-hyperliquid-fast-l2-v6.5.0
Minnzen Jul 15, 2026
1958aa9
fix: prevent stale spot order book sync
Minnzen Jul 15, 2026
77b591e
Merge branch 'release/v6.5.0' into codex/fix-hyperliquid-fast-l2-v6.5.0
Minnzen Jul 15, 2026
eaf0ce2
fix: stabilize spot order book transitions
Minnzen Jul 15, 2026
fb04409
fix: keep order book precision stable during reloads
Minnzen Jul 15, 2026
7af92cb
chore: trace order book precision transitions
Minnzen Jul 15, 2026
8836ca6
chore: trace mobile open order filtering
Minnzen Jul 15, 2026
51a58e1
fix: keep funding stable during order book reloads
Minnzen Jul 15, 2026
9b56cc3
fix: keep order book precision stable before l2 loads
Minnzen Jul 15, 2026
d6becb4
fix: harden order book subscription recovery
Minnzen Jul 15, 2026
7b3a756
chore: trace Hyperliquid market data flow
Minnzen Jul 15, 2026
a836b09
chore: remove temporary Hyperliquid diagnostics
Minnzen Jul 15, 2026
b7a3849
fix: prevent stale mobile open order views OK-57833
Minnzen Jul 15, 2026
7b95ff6
fix: cancel stale fast l2 recovery OK-57953
Minnzen Jul 15, 2026
f38ebbb
Merge remote-tracking branch 'origin/release/v6.5.0' into codex/fix-h…
Minnzen Jul 15, 2026
cb90bc9
fix: prevent invalid Hyperliquid spot depth options
Minnzen Jul 16, 2026
af71b4e
perf: improve Hyperliquid order book startup
Minnzen Jul 16, 2026
a7c0de8
perf: reuse L2 cache across instrument switches
Minnzen Jul 16, 2026
787caa8
fix: allow cached L2 hydration from empty state
Minnzen Jul 16, 2026
2398a02
fix: keep fast l2 transitions on the current target
Minnzen Jul 16, 2026
c9b65fa
fix: stabilize desktop order book loading
Minnzen Jul 16, 2026
6901539
fix: keep order book tick parameters synchronized
Minnzen Jul 17, 2026
dacaeba
Merge branch 'release/v6.5.0' into codex/fix-hyperliquid-fast-l2-v6.5.0
Minnzen Jul 17, 2026
bc3d230
fix: subscribe new order book coin before unsubscribing the old one
Minnzen Jul 17, 2026
ae6eccf
fix: keep order book size precision stable
Minnzen Jul 20, 2026
3544fad
Merge branch 'release/v6.5.0' into codex/fix-hyperliquid-fast-l2-v6.5.0
Minnzen Jul 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/kit-bg/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"main": "src/index.tsx",
"dependencies": {
"eth-url-parser": "^1.0.4",
"fflate": "0.8.2",
"idb": "^7.1.1",
"ipaddr.js": "^2.3.0",
"miscreant": "^0.3.2"
Expand Down
40 changes: 28 additions & 12 deletions packages/kit-bg/src/dbs/simple/entity/SimpleDbEntityPerp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,24 +417,40 @@ export class SimpleDbEntityPerp extends SimpleDbEntityBase<ISimpleDbPerpData> {
mantissa?: number | null;
data: IBook;
}) {
if (!coin || !data) {
await this.setL2BookSnapshotCaches([{ coin, nSigFigs, mantissa, data }]);
}

@backgroundMethod()
async setL2BookSnapshotCaches(
snapshots: Array<{
coin: string;
nSigFigs?: number | null;
mantissa?: number | null;
data: IBook;
}>,
) {
const validSnapshots = snapshots.filter(
(snapshot) => snapshot.coin && snapshot.data,
);
if (validSnapshots.length === 0) {
return;
}
await this.setPerpData((prev): ISimpleDbPerpData => {
const key = this._getL2BookSnapshotCacheKey({
coin,
nSigFigs,
mantissa,
});
const nextCache = {
...prev?.l2BookSnapshotCache,
[key]: {
const updatedAt = Date.now();
const nextCache = { ...prev?.l2BookSnapshotCache };
validSnapshots.forEach(({ coin, nSigFigs, mantissa, data }) => {
const key = this._getL2BookSnapshotCacheKey({
coin,
nSigFigs,
mantissa,
});
nextCache[key] = {
data,
updatedAt: Date.now(),
updatedAt,
nSigFigs,
mantissa,
},
};
};
});
return {
...prev,
l2BookSnapshotCache: this._limitSnapshotCacheEntries(nextCache),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import {
swrCacheUtils,
swrKeys,
} from '@onekeyhq/shared/src/utils/swrCacheUtils';
import type { IBook } from '@onekeyhq/shared/types/hyperliquid/sdk';

import {
import ServiceHyperliquidCache, {
buildL2BookSnapshotCachePayload,
getL2BookSnapshotCacheEntryLevelCount,
getL2BookSnapshotSwrCache,
selectL2BookSnapshotCacheEntry,
shouldWritePerpsAccountDisplayCache,
} from './ServiceHyperliquidCache';
Expand Down Expand Up @@ -51,6 +56,11 @@ function buildEntry({
}

describe('ServiceHyperliquidCache L2 book helpers', () => {
afterEach(() => {
swrCacheUtils.clearAll();
swrCacheUtils.flushNow();
});

it('counts the shallower side of the cached L2 book', () => {
expect(
getL2BookSnapshotCacheEntryLevelCount(
Expand All @@ -59,13 +69,13 @@ describe('ServiceHyperliquidCache L2 book helpers', () => {
).toBe(12);
});

it('selects the newest complete L2 book snapshot from cache candidates', () => {
it('selects the newest two-sided L2 book even when the market is shallow', () => {
const simpleDbEntry = buildEntry({
updatedAt: 100,
bidLevels: 18,
askLevels: 18,
});
const newerIncompleteSwrEntry = buildEntry({
const newerShallowSwrEntry = buildEntry({
updatedAt: 200,
bidLevels: 25,
askLevels: 4,
Expand All @@ -74,9 +84,29 @@ describe('ServiceHyperliquidCache L2 book helpers', () => {
expect(
selectL2BookSnapshotCacheEntry({
simpleDbEntry,
swrEntry: newerIncompleteSwrEntry,
swrEntry: newerShallowSwrEntry,
}),
).toBe(newerShallowSwrEntry);
});

it('rejects snapshots with an empty side', () => {
const validEntry = buildEntry({
updatedAt: 100,
bidLevels: 1,
askLevels: 1,
});
const newerOneSidedEntry = buildEntry({
updatedAt: 200,
bidLevels: 25,
askLevels: 0,
});

expect(
selectL2BookSnapshotCacheEntry({
simpleDbEntry: validEntry,
swrEntry: newerOneSidedEntry,
}),
).toBe(simpleDbEntry);
).toBe(validEntry);
});

it('builds option-specific payload only for the active book', () => {
Expand Down Expand Up @@ -114,6 +144,157 @@ describe('ServiceHyperliquidCache L2 book helpers', () => {
mantissa: null,
});
});

it('uses the source precision instead of current UI options', () => {
const data = Object.assign(
buildBook({ coin: 'ETH', bidLevels: 20, askLevels: 20 }),
{ nSigFigs: 5, mantissa: 5 },
);

expect(
buildL2BookSnapshotCachePayload({
data,
activeBookCoin: 'ETH',
activeOptions: { nSigFigs: 5, mantissa: 2 },
}),
).toMatchObject({ nSigFigs: 5, mantissa: 5 });
});

it('rejects a latest snapshot cached for another precision', () => {
const data = Object.assign(
buildBook({ coin: 'ETH', bidLevels: 20, askLevels: 20 }),
{ nSigFigs: 5, mantissa: 5 },
);
swrCacheUtils.set(swrKeys.perpsL2BookSnapshotLatest({ coin: 'ETH' }), data);

expect(
getL2BookSnapshotSwrCache({
coin: 'ETH',
nSigFigs: 5,
mantissa: 2,
maxAgeMs: 60_000,
}),
).toBeUndefined();
});

it('preserves source precision when using an untargeted latest snapshot', () => {
const data = Object.assign(
buildBook({ coin: 'ETH', bidLevels: 20, askLevels: 20 }),
{ nSigFigs: 5, mantissa: 5 },
);
swrCacheUtils.set(swrKeys.perpsL2BookSnapshotLatest({ coin: 'ETH' }), data);

expect(
getL2BookSnapshotSwrCache({
coin: 'ETH',
maxAgeMs: 60_000,
}),
).toMatchObject({
data,
nSigFigs: 5,
mantissa: 5,
});
});
});

describe('ServiceHyperliquidCache L2 book runtime cache', () => {
afterEach(() => {
delete (
globalThis as typeof globalThis & {
$onekeyIsInBackground?: boolean;
}
).$onekeyIsInBackground;
jest.useRealTimers();
swrCacheUtils.clearAll();
swrCacheUtils.flushNow();
});

function createService() {
(
globalThis as typeof globalThis & {
$onekeyIsInBackground?: boolean;
}
).$onekeyIsInBackground = true;
const getL2BookSnapshotCache = jest.fn().mockResolvedValue(undefined);
const setL2BookSnapshotCaches = jest.fn().mockResolvedValue(undefined);
const service = new ServiceHyperliquidCache({
backgroundApi: {
simpleDb: {
perp: {
getL2BookSnapshotCache,
setL2BookSnapshotCaches,
},
},
},
});
return {
service,
getL2BookSnapshotCache,
setL2BookSnapshotCaches,
};
}

it('serves the exact hot target without waiting for persisted storage', async () => {
const { service, getL2BookSnapshotCache } = createService();
const data = Object.assign(
buildBook({ coin: 'ETH', bidLevels: 4, askLevels: 4 }),
{ nSigFigs: 5, mantissa: 2 },
);

service.cacheL2BookSnapshot({
data,
activeBookCoin: 'ETH',
activeOptions: { nSigFigs: 5, mantissa: 2 },
});

await expect(
service.getL2BookSnapshotCache({
coin: 'ETH',
nSigFigs: 5,
mantissa: 2,
}),
).resolves.toMatchObject({
coin: 'ETH',
nSigFigs: 5,
mantissa: 2,
isCachedSnapshot: true,
});
expect(getL2BookSnapshotCache).not.toHaveBeenCalled();
});

it('keeps one pending snapshot per target during rapid switches', () => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2026-07-16T00:00:00.000Z'));
const { service, setL2BookSnapshotCaches } = createService();
const cache = (coin: string, bidPrice: string) => {
const data = buildBook({ coin, bidLevels: 4, askLevels: 4 });
data.levels[0][0] = { px: bidPrice, sz: '1', n: 1 };
service.cacheL2BookSnapshot({
data,
activeBookCoin: coin,
activeOptions: { nSigFigs: 5, mantissa: null },
});
};

cache('BTC', '100');
cache('ETH', '200');
cache('SOL', '300');
cache('ETH', '201');
service.flushPendingL2BookSnapshotCache();

expect(setL2BookSnapshotCaches).toHaveBeenCalledTimes(2);
expect(setL2BookSnapshotCaches.mock.calls[1]?.[0]).toEqual([
expect.objectContaining({
coin: 'ETH',
data: expect.objectContaining({
levels: expect.arrayContaining([
expect.arrayContaining([expect.objectContaining({ px: '201' })]),
]),
}),
}),
expect.objectContaining({ coin: 'SOL' }),
]);
});
});

describe('ServiceHyperliquidCache account display write throttle', () => {
Expand Down
Loading
Loading