UNPKG

@nktkas/hyperliquid

Version:

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

136 lines (135 loc) 5.46 kB
import { TransportError } from "../base.js"; /** * Error thrown when an HTTP response is deemed invalid: * - Non-200 status code * - Unexpected content type */ export class HttpRequestError extends TransportError { response; responseBody; /** * Creates a new HTTP request error. * @param response - The failed HTTP response. * @param responseBody - The raw response body content, if available. */ constructor(response, responseBody) { let message = `HTTP request failed: status ${response.status}`; if (responseBody) message += `, body "${responseBody}"`; super(message); this.response = response; this.responseBody = responseBody; this.name = "HttpRequestError"; } } /** HTTP implementation of the REST transport interface. */ export class HttpTransport { isTestnet; timeout; server; fetchOptions; onRequest; onResponse; /** * Creates a new HTTP transport instance. * @param options - Configuration options for the HTTP transport layer. */ constructor(options) { this.isTestnet = options?.isTestnet ?? false; this.timeout = options?.timeout === undefined ? 10_000 : options.timeout; this.server = { mainnet: { api: options?.server?.mainnet?.api ?? "https://api.hyperliquid.xyz", rpc: options?.server?.mainnet?.rpc ?? "https://rpc.hyperliquid.xyz", }, testnet: { api: options?.server?.testnet?.api ?? "https://api.hyperliquid-testnet.xyz", rpc: options?.server?.testnet?.rpc ?? "https://rpc.hyperliquid-testnet.xyz", }, }; this.fetchOptions = options?.fetchOptions ?? {}; this.onRequest = options?.onRequest; this.onResponse = options?.onResponse; } /** * Sends a request to the Hyperliquid API via fetch. * @param endpoint - The API endpoint to send the request to. * @param payload - The payload to send with the request. * @param signal - An optional abort signal. * @returns A promise that resolves with parsed JSON response body. * @throws {HttpRequestError} - Thrown when an HTTP response is deemed invalid. * @throws May throw {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch#exceptions | fetch errors}. */ async request(endpoint, payload, signal) { // Construct a Request const url = new URL(endpoint, this.server[this.isTestnet ? "testnet" : "mainnet"][endpoint === "explorer" ? "rpc" : "api"]); const init = mergeRequestInit({ body: JSON.stringify(payload), headers: { "Accept-Encoding": "gzip, deflate, br, zstd", "Content-Type": "application/json", }, keepalive: true, method: "POST", signal: this.timeout ? AbortSignal.timeout(this.timeout) : undefined, }, this.fetchOptions, { signal }); let request = new Request(url, init); // Call the onRequest callback, if provided if (this.onRequest) { const customRequest = await this.onRequest(request); if (customRequest instanceof Request) request = customRequest; } // Send the Request and wait for a Response let response = await fetch(request); // Call the onResponse callback, if provided if (this.onResponse) { const customResponse = await this.onResponse(response); if (customResponse instanceof Response) response = customResponse; } // Validate the Response if (!response.ok || !response.headers.get("Content-Type")?.includes("application/json")) { // Unload the response body to prevent memory leaks const body = await response.text().catch(() => undefined); throw new HttpRequestError(response, body); } // Parse the response body const body = await response.json(); // Check if the response is an error if (body?.type === "error") { throw new HttpRequestError(response, body?.message); } // Return the response body return body; } } /** Merges multiple {@linkcode HeadersInit} into one {@linkcode Headers}. */ function mergeHeadersInit(...inits) { if (inits.length === 0 || inits.length === 1) { return new Headers(inits[0]); } const merged = new Headers(); for (const headers of inits) { const iterator = Symbol.iterator in headers ? headers : Object.entries(headers); for (const [key, value] of iterator) { merged.set(key, value); } } return merged; } /** Merges multiple {@linkcode RequestInit} into one {@linkcode RequestInit}. */ function mergeRequestInit(...inits) { const merged = inits.reduce((acc, init) => ({ ...acc, ...init }), {}); const headersList = inits.map((init) => init.headers) .filter((headers) => typeof headers === "object"); if (headersList.length > 0) { merged.headers = mergeHeadersInit(...headersList); } const signals = inits.map((init) => init.signal) .filter((signal) => signal instanceof AbortSignal); if (signals.length > 0) { merged.signal = signals.length > 1 ? AbortSignal.any(signals) : signals[0]; } return merged; }