UNPKG

@nktkas/hyperliquid

Version:

Unofficial Hyperliquid API SDK for all major JS runtimes, written in TypeScript and provided with tests

193 lines (192 loc) 9.11 kB
(function (factory) { if (typeof module === "object" && typeof module.exports === "object") { var v = factory(require, exports); if (v !== undefined) module.exports = v; } else if (typeof define === "function" && define.amd) { define(["require", "exports", "../base.js"], factory); } })(function (require, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebSocketAsyncRequest = exports.WebSocketRequestError = void 0; const base_js_1 = require("../base.js"); /** * Error thrown when a WebSocket request fails: * - When the WebSocket connection is closed * - When the server responds with an error message */ class WebSocketRequestError extends base_js_1.TransportError { constructor(message) { super(message); this.name = "WebSocketRequestError"; } } exports.WebSocketRequestError = WebSocketRequestError; /** * Manages WebSocket requests to the Hyperliquid API. * Handles request creation, sending, and mapping responses to their corresponding requests. */ class WebSocketAsyncRequest { socket; lastId = 0; queue = []; lastRequestTime = 0; /** * Creates a new WebSocket async request handler. * @param socket - WebSocket connection instance for sending requests to the Hyperliquid WebSocket API * @param hlEvents - Used to recognize Hyperliquid responses and match them with sent requests */ constructor(socket, hlEvents) { this.socket = socket; // Monitor responses and match the pending request hlEvents.addEventListener("subscriptionResponse", (event) => { // Use a stringified request as an id const id = WebSocketAsyncRequest.requestToId(event.detail); this.queue.findLast((item) => item.id === id)?.resolve(event.detail); }); hlEvents.addEventListener("post", (event) => { const data = event.detail.response.type === "info" ? event.detail.response.payload.data : event.detail.response.payload; this.queue.findLast((item) => item.id === event.detail.id)?.resolve(data); }); hlEvents.addEventListener("pong", () => { this.queue.findLast((item) => item.id === "ping")?.resolve(); }); hlEvents.addEventListener("error", (event) => { try { // Error event doesn't have an id, use original request to match const request = event.detail.match(/{.*}/)?.[0]; if (!request) return; const parsedRequest = JSON.parse(request); // For `post` requests if ("id" in parsedRequest && typeof parsedRequest.id === "number") { this.queue.findLast((item) => item.id === parsedRequest.id)?.reject(new WebSocketRequestError(`Cannot complete WebSocket request: ${event.detail}`)); return; } // For `subscribe` and `unsubscribe` requests if ("subscription" in parsedRequest && typeof parsedRequest.subscription === "object" && parsedRequest.subscription !== null) { const id = WebSocketAsyncRequest.requestToId(parsedRequest); this.queue.findLast((item) => item.id === id)?.reject(new WebSocketRequestError(`Cannot complete WebSocket request: ${event.detail}`)); return; } // For already/invalid subscribed requests if (event.detail.startsWith("Already subscribed") || event.detail.startsWith("Invalid subscription")) { const id = WebSocketAsyncRequest.requestToId({ method: "subscribe", subscription: parsedRequest, }); this.queue.findLast((item) => item.id === id)?.reject(new WebSocketRequestError(`Cannot complete WebSocket request: ${event.detail}`)); return; } // For already unsubscribed requests if (event.detail.startsWith("Already unsubscribed")) { const id = WebSocketAsyncRequest.requestToId({ method: "unsubscribe", subscription: parsedRequest, }); this.queue.findLast((item) => item.id === id)?.reject(new WebSocketRequestError(`Cannot complete WebSocket request: ${event.detail}`)); return; } // For unknown requests const id = WebSocketAsyncRequest.requestToId(parsedRequest); this.queue.findLast((item) => item.id === id)?.reject(new WebSocketRequestError(`Cannot complete WebSocket request: ${event.detail}`)); } catch { // Ignore JSON parsing errors } }); // Throws all pending requests if the connection is dropped socket.addEventListener("close", () => { this.queue.forEach(({ reject }) => { reject(new WebSocketRequestError("Cannot complete WebSocket request: connection is closed")); }); this.queue = []; }); } async request(method, payload_or_signal, maybeSignal) { const payload = payload_or_signal instanceof AbortSignal ? undefined : payload_or_signal; const signal = payload_or_signal instanceof AbortSignal ? payload_or_signal : maybeSignal; // Reject the request if the signal is aborted if (signal?.aborted) return Promise.reject(signal.reason); // Create a request let id; let request; if (method === "post") { id = ++this.lastId; request = { method, id, request: payload }; } else if (method === "ping") { id = "ping"; request = { method }; } else { request = { method, subscription: payload }; id = WebSocketAsyncRequest.requestToId(request); } // Send the request this.socket.send(JSON.stringify(request), signal); this.lastRequestTime = Date.now(); // Wait for a response const { promise, resolve, reject } = Promise.withResolvers(); this.queue.push({ id, resolve, reject }); const onAbort = () => reject(signal?.reason); signal?.addEventListener("abort", onAbort, { once: true }); return await promise.finally(() => { const index = this.queue.findLastIndex((item) => item.id === id); if (index !== -1) this.queue.splice(index, 1); signal?.removeEventListener("abort", onAbort); }); } /** Normalizes an object and then converts it to a string. */ static requestToId(value) { const lowerHex = containsUppercaseHex(value) ? deepLowerHex(value) : value; const sorted = deepSortKeys(lowerHex); return JSON.stringify(sorted); // Also removes undefined } } exports.WebSocketAsyncRequest = WebSocketAsyncRequest; /** Deeply converts hexadecimal strings in an object/array to lowercase. */ function deepLowerHex(obj) { if (typeof obj === "string") { return /^(0X[0-9a-fA-F]*|0x[0-9a-fA-F]*[A-F][0-9a-fA-F]*)$/.test(obj) ? obj.toLowerCase() : obj; } if (Array.isArray(obj)) { return obj.map(deepLowerHex); } if (typeof obj === "object" && obj !== null) { const result = {}; const entries = Object.entries(obj); for (const [key, value] of entries) { result[key] = deepLowerHex(value); } return result; } return obj; } /** Check if an object contains uppercase hexadecimal strings. */ function containsUppercaseHex(obj) { const str = JSON.stringify(obj); return /0X[0-9a-fA-F]*|0x[0-9a-fA-F]*[A-F][0-9a-fA-F]*/.test(str); } /** Deeply sort the keys of an object. */ function deepSortKeys(obj) { if (typeof obj !== "object" || obj === null) { return obj; } if (Array.isArray(obj)) { return obj.map(deepSortKeys); } const result = {}; const keys = Object.keys(obj).sort(); for (const key of keys) { result[key] = deepSortKeys(obj[key]); } return result; } });