diff --git a/package.json b/package.json index 8dd908de..c431f7f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bloxwap/hyperliquid", - "version": "0.1.5", + "version": "0.1.6", "description": "Blazing fast TypeScript Hyperliquid SDK.", "license": "MIT", "type": "module", diff --git a/src/transport/websocket/_id.ts b/src/transport/websocket/_id.ts index a7abf63c..9db6fc5c 100644 --- a/src/transport/websocket/_id.ts +++ b/src/transport/websocket/_id.ts @@ -85,6 +85,13 @@ export function normalize(value: unknown): unknown { const result: Record = {}; for (const key of Object.keys(value).sort()) { const item = normalize((value as Record)[key]); + // Drop `undefined`-valued keys, matching what `JSON.stringify` does to this object on its + // way to the wire. Keeping them would leave the id and the echo carrying a field the server + // never receives and so can never echo back — and `isSubset` requires every key of the + // pending request to be present in the response, so such a subscription could never be + // matched to its own confirmation. A payload built as `{ dex: params.dex || undefined }` + // is the shape that hits this. + if (item === undefined) continue; // `__proto__` is the one key with a setter on `Object.prototype`: plain assignment would // invoke it and silently drop an own `__proto__` key a `JSON.parse`d payload can carry, // collapsing two logically distinct payloads onto the same id. Define it as a data diff --git a/tests/transport/websocket/_id.test.ts b/tests/transport/websocket/_id.test.ts index 1abd5ea5..fa306890 100644 --- a/tests/transport/websocket/_id.test.ts +++ b/tests/transport/websocket/_id.test.ts @@ -25,6 +25,17 @@ describe("requestToId", () => { assert(requestToId([1, 2]) !== requestToId([2, 1])); }); + test("drops undefined-valued keys, matching what reaches the wire", () => { + // `JSON.stringify` omits an `undefined` value, so a payload built as + // `{ dex: params.dex || undefined }` goes out as `{"type":"allMids"}`. Keeping the key in the + // normalized form left the pending request demanding a field the server can never echo back, + // and `isSubset` then refused to match the subscription to its own confirmation — `allMids()` + // never resolved. The normalized form has to describe what was actually sent. + assertEquals(requestToId({ type: "allMids", dex: undefined }), requestToId({ type: "allMids" })); + assertEquals(requestToId({ type: "allMids", dex: undefined }), '{"type":"allMids"}'); + assert(isSubset(normalize({ type: "allMids", dex: undefined }), { type: "allMids" })); + }); + test("keeps an own __proto__ key from a parsed payload", () => { const payload = JSON.parse('{"type":"l2Book","__proto__":{"coin":"BTC"}}') as Record; assertEquals(requestToId(payload), '{"__proto__":{"coin":"BTC"},"type":"l2Book"}');