UNPKG

hyperliquid-sdk

Version:

<<< Typescript SDK for the Hyperliquid API >>>

1,627 lines (1,611 loc) 52.8 kB
// src/utils/helpers.ts import axios from "axios"; // src/utils/errors.ts var HyperliquidAPIError = class extends Error { constructor(message) { super(message); this.name = "HyperliquidAPIError"; } }; var AuthenticationError = class extends Error { constructor(message) { super(message); this.name = "AuthenticationError"; } }; function handleApiError(error) { if (error.message) { throw new HyperliquidAPIError(error.message); } if (error.response?.status && error.response.statusText) { throw new HyperliquidAPIError( `API request has failed with status: ${error.response.status} and text: ${error.response.statusText}` ); } if (error.request) { throw new HyperliquidAPIError("No response received from the server"); } throw error; } // src/utils/helpers.ts var HttpApi = class { client; endpoint; // TODO: Rework rate limiter rateLimiter; constructor(baseUrl, endpoint = "/", rateLimiter) { this.endpoint = endpoint; this.client = axios.create({ baseURL: baseUrl, headers: { "Content-Type": "application/json" } }); this.rateLimiter = rateLimiter; } async makeRequest(payload, weight = 2) { try { await this.rateLimiter.waitForToken(weight); return (await this.client.post(this.endpoint, payload)).data; } catch (error) { if (axios.isAxiosError(error)) { handleApiError(error); } else if (error instanceof Error) { throw new HyperliquidAPIError(`Unknown error: ${error.message}`); } console.error(`Unhandled error type: `, error); throw error; } } }; var validatePublicKey = (publicKey) => { if (!publicKey) { throw new Error("Public Key is required!"); } }; // src/types/constants.ts var BASE_URLS = { PRODUCTION: "https://api.hyperliquid.xyz", TESTNET: "https://api.hyperliquid-testnet.xyz" }; var WSS_URLS = { PRODUCTION: "wss://api.hyperliquid.xyz/ws", TESTNET: "wss://api.hyperliquid-testnet.xyz/ws" }; var ENDPOINTS = { INFO: "/info", EXCHANGE: "/exchange" }; var ARBITRUM_CHAIN_ID_DECIMAL = { MAINNET: 42161, TESTNET: 421614 }; var ARBITRUM_CHAIN_ID_HEX = { MAINNET: "0xa4b1", TESTNET: "0x66eee" }; var HYPERLIQUID_CHAIN_NAME = { MAINNET: "Mainnet", TESTNET: "Testnet" }; // src/rest/info/general.ts var GeneralInfoAPI = class { httpApi; symbolConversion; constructor(httpApi, symbolConversion) { this.httpApi = httpApi; this.symbolConversion = symbolConversion; } async getAllMids() { const response = await this.httpApi.makeRequest({ type: "allMids" /* ALL_MIDS */ }); const convertedResponse = {}; for (const [key, value] of Object.entries(response)) { const convertedKey = await this.symbolConversion.convertSymbol(key); convertedResponse[convertedKey] = parseFloat(value); } return convertedResponse; } async getUserOpenOrders(userPublicKey, rawResponse = false) { validatePublicKey(userPublicKey); const response = await this.httpApi.makeRequest({ type: "openOrders" /* OPEN_ORDERS */, user: userPublicKey }); return rawResponse ? response : await this.symbolConversion.convertResponse(response); } async getReferralState(userPublicKey) { validatePublicKey(userPublicKey); return this.httpApi.makeRequest({ type: "referral" /* REFERRAL */, user: userPublicKey }); } async getFrontendOpenOrders(userPublicKey, rawResponse = false) { validatePublicKey(userPublicKey); const response = await this.httpApi.makeRequest( { type: "frontendOpenOrders" /* FRONTEND_OPEN_ORDERS */, user: userPublicKey }, 20 ); return rawResponse ? response : await this.symbolConversion.convertResponse(response); } async getUserFills(userPublicKey, rawResponse = false) { validatePublicKey(userPublicKey); const response = await this.httpApi.makeRequest( { type: "userFills" /* USER_FILLS */, user: userPublicKey }, 20 ); return rawResponse ? response : await this.symbolConversion.convertResponse(response); } async getUserFillsByTime(userPublicKey, startTime, endTime, rawResponse = false) { validatePublicKey(userPublicKey); let params = { user: userPublicKey, startTime: Math.round(startTime), type: "userFillsByTime" /* USER_FILLS_BY_TIME */ }; if (endTime) { params.endTime = Math.round(endTime); } const response = await this.httpApi.makeRequest(params, 20); return rawResponse ? response : await this.symbolConversion.convertResponse(response); } async getUserRateLimit(userPublicKey, rawResponse = false) { validatePublicKey(userPublicKey); const response = await this.httpApi.makeRequest( { type: "userRateLimit" /* USER_RATE_LIMIT */, user: userPublicKey }, 20 ); return rawResponse ? response : await this.symbolConversion.convertResponse(response); } async getOrderStatus(userPublicKey, oid, rawResponse = false) { validatePublicKey(userPublicKey); const response = await this.httpApi.makeRequest({ type: "orderStatus" /* ORDER_STATUS */, user: userPublicKey, oid }); return rawResponse ? response : await this.symbolConversion.convertResponse(response); } async getUserFees(userPublicKey) { validatePublicKey(userPublicKey); return this.httpApi.makeRequest({ type: "userFees" /* USER_FEES */, user: userPublicKey }); } async getUserPortfolio(userPublicKey) { validatePublicKey(userPublicKey); return this.httpApi.makeRequest({ type: "portfolio" /* PORTFOLIO */, user: userPublicKey }); } async getL2Book(coin, rawResponse = false) { const response = await this.httpApi.makeRequest({ type: "l2Book" /* L2_BOOK */, coin: await this.symbolConversion.convertSymbol(coin, "reverse") }); return rawResponse ? response : await this.symbolConversion.convertResponse(response); } async getCandleSnapshot(coin, interval, startTime, endTime) { return this.httpApi.makeRequest({ type: "candleSnapshot" /* CANDLE_SNAPSHOT */, req: { coin: await this.symbolConversion.convertSymbol(coin, "reverse"), interval, startTime, endTime } }); } }; // src/rest/info/spot.ts var SpotInfoAPI = class { httpApi; symbolConversion; constructor(httpApi, symbolConversion) { this.httpApi = httpApi; this.symbolConversion = symbolConversion; } async getSpotMeta(rawResponse = false) { const response = await this.httpApi.makeRequest({ type: "spotMeta" /* SPOT_META */ }); return rawResponse ? response : await this.symbolConversion.convertResponse( response, ["name", "coin", "symbol"], "SPOT" ); } async getSpotClearinghouseState(userPublicKey, rawResponse = false) { validatePublicKey(userPublicKey); const response = await this.httpApi.makeRequest({ type: "spotClearinghouseState" /* SPOT_CLEARINGHOUSE_STATE */, user: userPublicKey }); return rawResponse ? response : await this.symbolConversion.convertResponse( response, ["name", "coin", "symbol"], "SPOT" ); } async getSpotMetaAndAssetCtxs(rawResponse = false) { const response = await this.httpApi.makeRequest({ type: "spotMetaAndAssetCtxs" /* SPOT_META_AND_ASSET_CTXS */ }); return rawResponse ? response : await this.symbolConversion.convertResponse(response); } }; // src/rest/info/perpetuals.ts var PerpetualsInfoAPI = class { httpApi; symbolConversion; constructor(httpApi, symbolConversion) { this.httpApi = httpApi; this.symbolConversion = symbolConversion; } async getMeta(rawResponse = false) { const response = await this.httpApi.makeRequest({ type: "meta" /* META */ }); return rawResponse ? response : await this.symbolConversion.convertResponse( response, ["name", "coin", "symbol"], "PERP" ); } async getMetaAndAssetCtxs(rawResponse = false) { const response = await this.httpApi.makeRequest({ type: "metaAndAssetCtxs" /* PERPS_META_AND_ASSET_CTXS */ }); return rawResponse ? response : await this.symbolConversion.convertResponse( response, ["name", "coin", "symbol"], "PERP" ); } async getClearinghouseState(userPublicKey, rawResponse = false) { validatePublicKey(userPublicKey); const response = await this.httpApi.makeRequest({ type: "clearinghouseState" /* PERPS_CLEARINGHOUSE_STATE */, user: userPublicKey }); return rawResponse ? response : await this.symbolConversion.convertResponse(response); } async getUserFunding(userPublicKey, startTime, endTime, rawResponse = false) { validatePublicKey(userPublicKey); const response = await this.httpApi.makeRequest( { type: "userFunding" /* USER_FUNDING */, user: userPublicKey, startTime, endTime }, 20 ); return rawResponse ? response : await this.symbolConversion.convertResponse(response); } async getUserNonFundingLedgerUpdates(userPublicKey, startTime, endTime, rawResponse = false) { validatePublicKey(userPublicKey); const response = await this.httpApi.makeRequest( { type: "userNonFundingLedgerUpdates" /* USER_NON_FUNDING_LEDGER_UPDATES */, user: userPublicKey, startTime, endTime }, 20 ); return rawResponse ? response : await this.symbolConversion.convertResponse(response); } async getFundingHistory(coin, startTime, endTime, rawResponse = false) { const response = await this.httpApi.makeRequest( { type: "fundingHistory" /* FUNDING_HISTORY */, coin: await this.symbolConversion.convertSymbol(coin, "reverse"), startTime, endTime }, 20 ); return rawResponse ? response : await this.symbolConversion.convertResponse(response); } }; // src/rest/info.ts var InfoAPI = class { spot; perpetuals; httpApi; generalAPI; symbolConversion; constructor(baseURL, rateLimiter, symbolConversion) { this.httpApi = new HttpApi(baseURL, ENDPOINTS.INFO, rateLimiter); this.symbolConversion = symbolConversion; this.generalAPI = new GeneralInfoAPI(this.httpApi, this.symbolConversion); this.spot = new SpotInfoAPI(this.httpApi, this.symbolConversion); this.perpetuals = new PerpetualsInfoAPI( this.httpApi, this.symbolConversion ); } async getAssetIndex(assetName) { return await this.symbolConversion.getAssetIndex(assetName); } async getInternalName(exchangeName) { return await this.symbolConversion.convertSymbol(exchangeName); } async getAllAssets() { return await this.symbolConversion.getAllAssets(); } async getAllMids() { return this.generalAPI.getAllMids(); } async getReferralState(userPublicKey) { return this.generalAPI.getReferralState(userPublicKey); } async getUserOpenOrders(user, rawResponse = false) { return this.generalAPI.getUserOpenOrders(user, rawResponse); } async getFrontendOpenOrders(user, rawResponse = false) { return this.generalAPI.getFrontendOpenOrders(user, rawResponse); } async getUserFills(user, rawResponse = false) { return this.generalAPI.getUserFills(user, rawResponse); } async getUserFillsByTime(user, startTime, endTime, rawResponse = false) { return this.generalAPI.getUserFillsByTime( user, startTime, endTime, rawResponse ); } async getUserRateLimit(user, rawResponse = false) { return this.generalAPI.getUserRateLimit(user, rawResponse); } async getOrderStatus(user, oid, rawResponse = false) { return this.generalAPI.getOrderStatus(user, oid, rawResponse); } async getUserFees(userPublicKey) { return this.generalAPI.getUserFees(userPublicKey); } async getUserPortfolio(userPublicKey) { return this.generalAPI.getUserPortfolio(userPublicKey); } async getL2Book(coin, rawResponse = false) { return this.generalAPI.getL2Book(coin, rawResponse); } async getCandleSnapshot(coin, interval, startTime, endTime) { return this.generalAPI.getCandleSnapshot( coin, interval, startTime, endTime ); } }; // src/utils/signing.ts import { encode } from "@msgpack/msgpack"; import { ethers, getBytes, keccak256 } from "ethers"; var phantomDomain = { name: "Exchange", version: "1", chainId: 1337, verifyingContract: "0x0000000000000000000000000000000000000000" }; var agentTypes = { Agent: [ { name: "source", type: "string" }, { name: "connectionId", type: "bytes32" } ] }; function orderTypeToWire(orderType) { if (orderType.limit) { return { limit: orderType.limit }; } else if (orderType.trigger) { return { trigger: { isMarket: orderType.trigger.isMarket, triggerPx: floatToWire(Number(orderType.trigger.triggerPx)), tpsl: orderType.trigger.tpsl } }; } throw new Error("Invalid order type"); } function addressToBytes(address) { return getBytes(address); } function actionHash(action, vaultAddress, nonce) { const msgPackBytes = encode(action); const additionalBytesLength = vaultAddress === null ? 9 : 29; const data = new Uint8Array(msgPackBytes.length + additionalBytesLength); data.set(msgPackBytes); const view = new DataView(data.buffer); view.setBigUint64(msgPackBytes.length, BigInt(nonce), false); if (vaultAddress === null) { view.setUint8(msgPackBytes.length + 8, 0); } else { view.setUint8(msgPackBytes.length + 8, 1); data.set(addressToBytes(vaultAddress), msgPackBytes.length + 9); } return keccak256(data); } function constructPhantomAgent(hash, isMainnet) { return { source: isMainnet ? "a" : "b", connectionId: hash }; } async function signL1Action(wallet, action, activePool, nonce, isMainnet) { const hash = actionHash(action, activePool, nonce); const phantomAgent = constructPhantomAgent(hash, isMainnet); const data = { domain: phantomDomain, types: agentTypes, primaryType: "Agent", message: phantomAgent }; return signInner(wallet, data); } async function signUserSignedAction(wallet, action, payloadTypes, primaryType, isMainnet) { action.signatureChainId = isMainnet ? ARBITRUM_CHAIN_ID_HEX.MAINNET : ARBITRUM_CHAIN_ID_HEX.TESTNET; action.hyperliquidChain = isMainnet ? HYPERLIQUID_CHAIN_NAME.MAINNET : HYPERLIQUID_CHAIN_NAME.TESTNET; const data = { domain: { name: "HyperliquidSignTransaction", version: "1", chainId: isMainnet ? ARBITRUM_CHAIN_ID_DECIMAL.MAINNET : ARBITRUM_CHAIN_ID_DECIMAL.TESTNET, verifyingContract: "0x0000000000000000000000000000000000000000" }, types: { [primaryType]: payloadTypes }, primaryType, message: action }; return signInner(wallet, data); } async function signUsdTransferAction(wallet, action, isMainnet) { return signUserSignedAction( wallet, action, [ { name: "hyperliquidChain", type: "string" }, { name: "destination", type: "string" }, { name: "amount", type: "string" }, { name: "time", type: "uint64" } ], "HyperliquidTransaction:UsdSend", isMainnet ); } async function signWithdrawFromBridgeAction(wallet, action, isMainnet) { return signUserSignedAction( wallet, action, [ { name: "hyperliquidChain", type: "string" }, { name: "destination", type: "string" }, { name: "amount", type: "string" }, { name: "time", type: "uint64" } ], "HyperliquidTransaction:Withdraw", isMainnet ); } async function signAgent(wallet, action, isMainnet) { return signUserSignedAction( wallet, action, [ { name: "hyperliquidChain", type: "string" }, { name: "agentAddress", type: "address" }, { name: "agentName", type: "string" }, { name: "nonce", type: "uint64" } ], "HyperliquidTransaction:ApproveAgent", isMainnet ); } async function signInner(wallet, data) { const signature = await wallet.signTypedData( data.domain, data.types, data.message ); return splitSig(signature); } function splitSig(sig) { const { r, s, v } = ethers.Signature.from(sig); return { r, s, v }; } function floatToWire(x) { const rounded = x.toFixed(8); if (Math.abs(parseFloat(rounded) - x) >= 1e-12) { throw new Error(`floatToWire causes rounding: ${x}`); } let normalized = rounded.replace(/\.?0+$/, ""); if (normalized === "-0") normalized = "0"; return normalized; } function floatToIntForHashing(x) { return floatToInt(x, 8); } function floatToUsdInt(x) { return floatToInt(x, 6); } function floatToInt(x, power) { const withDecimals = x * Math.pow(10, power); if (Math.abs(Math.round(withDecimals) - withDecimals) >= 1e-3) { throw new Error(`floatToInt causes rounding: ${x}`); } return Math.round(withDecimals); } function getTimestampMs() { return Date.now(); } function orderToWire(order, asset) { const orderWire = { a: asset, b: order.is_buy, p: floatToWire(order.limit_px), s: floatToWire(order.sz), r: order.reduce_only, t: orderTypeToWire(order.order_type) }; if (order.cloid !== void 0) { orderWire.c = order.cloid; } return orderWire; } function orderWireToAction(orders, grouping = "na", builder) { return { type: "order", orders, grouping, ...builder !== void 0 ? { builder } : {} }; } function cancelOrderToAction(cancelRequest) { return { type: "cancel", cancels: [cancelRequest] }; } // src/types/index.ts var LeverageModeEnum = /* @__PURE__ */ ((LeverageModeEnum2) => { LeverageModeEnum2["CROSS"] = "cross"; LeverageModeEnum2["ISOLATED"] = "isolated"; return LeverageModeEnum2; })(LeverageModeEnum || {}); // src/rest/exchange.ts var ExchangeAPI = class { constructor(testnet, wallet, info, rateLimiter, symbolConversion) { this.info = info; const baseURL = testnet ? BASE_URLS.TESTNET : BASE_URLS.PRODUCTION; this.isMainnet = !testnet; this.httpApi = new HttpApi(baseURL, ENDPOINTS.EXCHANGE, rateLimiter); this.wallet = wallet; this.symbolConversion = symbolConversion; } wallet; httpApi; symbolConversion; isMainnet = true; async getAssetIndex(symbol) { const index = await this.symbolConversion.getAssetIndex(symbol); if (index === void 0) { throw new Error(`Unknown asset: ${symbol}`); } return index; } async placeOrder(orderRequest) { const { orders, vaultAddress = null, grouping = "na", builder } = orderRequest; const ordersArray = orders ?? [orderRequest]; try { const assetIndexCache = /* @__PURE__ */ new Map(); const orderWires = await Promise.all( ordersArray.map(async (o) => { let assetIndex = assetIndexCache.get(o.coin); if (assetIndex === void 0) { assetIndex = await this.getAssetIndex(o.coin); assetIndexCache.set(o.coin, assetIndex); } return orderToWire(o, assetIndex); }) ); const actions = orderWireToAction(orderWires, grouping, builder); const nonce = Date.now(); const signature = await signL1Action( this.wallet, actions, vaultAddress, nonce, this.isMainnet ); const payload = { action: actions, nonce, signature, vaultAddress }; const result = await this.httpApi.makeRequest(payload, 1); return this.validateErrorResult(result); } catch (error) { throw error; } } validateErrorResult(result) { if (typeof result.response !== "string") { const status = result.response.data.statuses.find( (status2) => !!status2.error ); if (status) { throw new Error(status.error); } } if (result.status !== "ok" && typeof result.response === "string") { throw new Error(result.response); } return result; } //Cancel using order id (oid) async cancelOrder(cancelRequests) { try { const cancels = Array.isArray(cancelRequests) ? cancelRequests : [cancelRequests]; const cancelsWithIndices = await Promise.all( cancels.map(async (req) => ({ ...req, a: await this.getAssetIndex(req.coin) })) ); const action = { type: "cancel" /* CANCEL */, cancels: cancelsWithIndices.map(({ a, o }) => ({ a, o })) }; const nonce = Date.now(); const signature = await signL1Action( this.wallet, action, null, nonce, this.isMainnet ); const payload = { action, nonce, signature }; return this.httpApi.makeRequest(payload, 1); } catch (error) { throw error; } } //Cancel using a CLOID async cancelOrderByCloid(symbol, cloid) { try { const assetIndex = await this.getAssetIndex(symbol); const action = { type: "cancelByCloid" /* CANCEL_BY_CLOID */, cancels: [{ asset: assetIndex, cloid }] }; const nonce = Date.now(); const signature = await signL1Action( this.wallet, action, null, nonce, this.isMainnet ); const payload = { action, nonce, signature }; return this.httpApi.makeRequest(payload, 1); } catch (error) { throw error; } } //Modify a single order async modifyOrder(oid, orderRequest) { try { const assetIndex = await this.getAssetIndex(orderRequest.coin); const orderWire = orderToWire(orderRequest, assetIndex); const action = { type: "modify" /* MODIFY */, oid, order: orderWire }; const nonce = Date.now(); const signature = await signL1Action( this.wallet, action, null, nonce, this.isMainnet ); const payload = { action, nonce, signature }; return this.httpApi.makeRequest(payload, 1); } catch (error) { throw error; } } //Modify multiple orders at once async batchModifyOrders(modifies) { try { const assetIndices = await Promise.all( modifies.map((m) => this.getAssetIndex(m.order.coin)) ); const action = { type: "batchModify" /* BATCH_MODIFY */, modifies: modifies.map((m, index) => { return { oid: m.oid, order: orderToWire(m.order, assetIndices[index]) }; }) }; const nonce = Date.now(); const signature = await signL1Action( this.wallet, action, null, nonce, this.isMainnet ); const payload = { action, nonce, signature }; return this.httpApi.makeRequest(payload, 1); } catch (error) { throw error; } } async updateLeverage(symbol, leverageMode, leverage) { try { const assetIndex = await this.getAssetIndex(symbol); const action = { type: "updateLeverage" /* UPDATE_LEVERAGE */, asset: assetIndex, isCross: leverageMode === "cross" /* CROSS */, leverage }; const nonce = Date.now(); const signature = await signL1Action( this.wallet, action, null, nonce, this.isMainnet ); const payload = { action, nonce, signature }; return this.httpApi.makeRequest(payload, 1); } catch (error) { throw error; } } //Update how much margin there is on a perps position async updateIsolatedMargin(symbol, isBuy, ntli) { try { const assetIndex = await this.getAssetIndex(symbol); const action = { type: "updateIsolatedMargin" /* UPDATE_ISOLATED_MARGIN */, asset: assetIndex, isBuy, ntli }; const nonce = Date.now(); const signature = await signL1Action( this.wallet, action, null, nonce, this.isMainnet ); const payload = { action, nonce, signature }; return this.httpApi.makeRequest(payload, 1); } catch (error) { throw error; } } getChainIdHex() { return this.isMainnet ? ARBITRUM_CHAIN_ID_HEX.MAINNET : ARBITRUM_CHAIN_ID_HEX.TESTNET; } //Takes from the perps wallet and sends to another wallet without the $1 fee (doesn't touch bridge, so no fees) async usdTransfer(destination, amount) { try { const action = { type: "usdSend" /* USD_SEND */, hyperliquidChain: this.getHyperliquidChainName(), signatureChainId: this.getChainIdHex(), destination, amount: amount.toString(), time: Date.now() }; const signature = await signUsdTransferAction( this.wallet, action, this.isMainnet ); const payload = { action, nonce: action.time, signature }; return this.httpApi.makeRequest(payload, 1); } catch (error) { throw error; } } //Transfer SPOT assets i.e PURR to another wallet (doesn't touch bridge, so no fees) async spotTransfer(destination, token, amount) { try { const action = { type: "spotSend" /* SPOT_SEND */, hyperliquidChain: this.getHyperliquidChainName(), signatureChainId: this.getChainIdHex(), destination, token, amount, time: Date.now() }; const signature = await signUserSignedAction( this.wallet, action, [ { name: "hyperliquidChain", type: "string" }, { name: "destination", type: "string" }, { name: "token", type: "string" }, { name: "amount", type: "string" }, { name: "time", type: "uint64" } ], "HyperliquidTransaction:SpotSend", this.isMainnet ); const payload = { action, nonce: action.time, signature }; return this.httpApi.makeRequest(payload, 1); } catch (error) { throw error; } } getHyperliquidChainName() { return this.isMainnet ? HYPERLIQUID_CHAIN_NAME.MAINNET : HYPERLIQUID_CHAIN_NAME.TESTNET; } //Withdraw USDC, this txn goes across the bridge and costs $1 in fees as of writing this async initiateWithdrawal(destination, amount) { try { const action = { type: "withdraw3" /* WITHDRAW */, hyperliquidChain: this.getHyperliquidChainName(), signatureChainId: this.getChainIdHex(), destination, amount: amount.toString(), time: Date.now() }; const signature = await signWithdrawFromBridgeAction( this.wallet, action, this.isMainnet ); const payload = { action, nonce: action.time, signature }; const result = await this.httpApi.makeRequest(payload, 1); return this.validateErrorResult(result); } catch (error) { throw error; } } //Transfer between spot and perpetual wallets (intra-account transfer) async transferBetweenSpotAndPerp(usdc, toPerp) { try { const action = { type: "usdClassTransfer" /* USD_CLASS_TRANSFER */, hyperliquidChain: this.getHyperliquidChainName(), signatureChainId: this.getChainIdHex(), amount: usdc.toString(), toPerp, nonce: Date.now() }; const signature = await signUserSignedAction( this.wallet, action, [ { name: "hyperliquidChain", type: "string" }, { name: "amount", type: "string" }, { name: "toPerp", type: "bool" }, { name: "nonce", type: "uint64" } ], "HyperliquidTransaction:UsdClassTransfer", this.isMainnet ); const payload = { action, nonce: action.nonce, signature }; const result = await this.httpApi.makeRequest(payload, 1); return this.validateErrorResult(result); } catch (error) { throw error; } } //Schedule a cancel for a given time (in ms) //Note: Only available once you've traded $1 000 000 in volume async scheduleCancel(time) { try { const action = { type: "scheduleCancel" /* SCHEDULE_CANCEL */, time }; const nonce = Date.now(); const signature = await signL1Action( this.wallet, action, null, nonce, this.isMainnet ); const payload = { action, nonce, signature }; return this.httpApi.makeRequest(payload, 1); } catch (error) { throw error; } } //Transfer between vault and perpetual wallets (intra-account transfer) async vaultTransfer(vaultAddress, isDeposit, usd) { try { const action = { type: "vaultTransfer" /* VAULT_TRANSFER */, vaultAddress, isDeposit, usd }; const nonce = Date.now(); const signature = await signL1Action( this.wallet, action, null, nonce, this.isMainnet ); const payload = { action, nonce, signature }; return this.httpApi.makeRequest(payload, 1); } catch (error) { throw error; } } async setReferrer(code) { try { const action = { type: "setReferrer" /* SET_REFERRER */, code }; const nonce = Date.now(); const signature = await signL1Action( this.wallet, action, null, nonce, this.isMainnet ); const payload = { action, nonce, signature }; return this.httpApi.makeRequest(payload, 1); } catch (error) { throw error; } } }; // src/websocket/connection.ts import { EventEmitter } from "events"; var WebSocketClient = class extends EventEmitter { ws = null; url; pingInterval = null; reconnectAttempts = 0; maxReconnectAttempts = 5; reconnectDelay = 5e3; initialReconnectDelay = 1e3; maxReconnectDelay = 3e4; constructor(testnet = false) { super(); this.url = testnet ? WSS_URLS.TESTNET : WSS_URLS.PRODUCTION; } connect() { return new Promise((resolve, reject) => { this.ws = new WebSocket(this.url); this.ws.onopen = () => { console.log("WebSocket connected"); this.reconnectAttempts = 0; this.startPingInterval(); resolve(); }; this.ws.onmessage = (event) => { const message = JSON.parse(event.data.toString()); this.emit("message", message); }; this.ws.onerror = (ev) => { console.error("WebSocket error:", ev); reject("Something went wrong!"); }; this.ws.onclose = () => { console.log("WebSocket disconnected"); this.stopPingInterval(); this.reconnect(); }; }); } reconnect() { if (this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnectAttempts++; const delay = Math.min( this.initialReconnectDelay * Math.pow(2, this.reconnectAttempts - 1), this.maxReconnectDelay ); console.log( `Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts}) in ${delay}ms...` ); setTimeout(() => this.connect(), delay); } else { console.error( "Max reconnection attempts reached. Please reconnect manually." ); this.emit("maxReconnectAttemptsReached"); } } startPingInterval() { this.pingInterval = setInterval(() => { this.sendMessage({ method: "ping" }); }, 15e3); } stopPingInterval() { if (this.pingInterval) { clearInterval(this.pingInterval); this.pingInterval = null; } } sendMessage(message) { if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { throw new Error("WebSocket is not connected"); } this.ws.send(JSON.stringify(message)); } close() { if (this.ws) { this.ws.close(); } this.stopPingInterval(); } }; // src/websocket/subscriptions.ts var WebSocketSubscriptions = class { ws; symbolConversion; constructor(ws, symbolConversion) { this.ws = ws; this.symbolConversion = symbolConversion; } async subscribe(subscription) { this.ws.sendMessage({ method: "subscribe", subscription }); } async unsubscribe(subscription) { const convertedSubscription = await this.symbolConversion.convertSymbolsInObject(subscription); this.ws.sendMessage({ method: "unsubscribe", subscription: convertedSubscription }); } handleMessage(message, callback, channel, additionalChecks = () => true) { if (typeof message !== "object" || message === null) { console.warn("Received invalid message format:", message); return; } let data = message.data || message; if (data.channel === channel && additionalChecks(data)) { const convertedData = this.symbolConversion.convertSymbolsInObject(data); callback(convertedData); } } async subscribeToAllMids(callback) { if (typeof callback !== "function") { throw new Error("Callback must be a function"); } await this.subscribe({ type: "allMids" }); this.ws.on("message", async (message) => { if (message.channel === "allMids") { if (message.data.mids) { const convertedData = {}; for (const [key, value] of Object.entries(message.data.mids)) { const convertedKey = await this.symbolConversion.convertSymbol(key); const convertedValue = this.symbolConversion.convertToNumber(value); convertedData[convertedKey] = convertedValue; } callback(convertedData); } } }); } async subscribeToNotification(user, callback) { await this.subscribe({ type: "notification", user }); this.ws.on("message", async (message) => { if (message.channel === "notification") { message = await this.symbolConversion.convertSymbolsInObject(message); callback(message.data); } }); } async subscribeToWebData2(user, callback) { await this.subscribe({ type: "webData2", user }); this.ws.on("message", async (message) => { if (message.channel === "webData2") { message = await this.symbolConversion.convertSymbolsInObject(message); callback(message.data); } }); } async subscribeToCandle(coin, interval, callback) { const convertedCoin = await this.symbolConversion.convertSymbol( coin, "reverse" ); await this.subscribe({ type: "candle", coin: convertedCoin, interval }); this.ws.on("message", async (message) => { if (message.channel === "candle" && message.data.s === convertedCoin && message.data.i === interval) { callback(message.data); } }); } async subscribeToL2Book(coin, callback) { const convertedCoin = await this.symbolConversion.convertSymbol( coin, "reverse" ); await this.subscribe({ type: "l2Book", coin: convertedCoin }); this.ws.on("message", async (message) => { if (message.channel === "l2Book" && message.data.coin === convertedCoin) { message = await this.symbolConversion.convertSymbolsInObject(message, [ "coin" ]); callback(message.data); } }); } async subscribeToTrades(coin, callback) { const convertedCoin = await this.symbolConversion.convertSymbol( coin, "reverse" ); await this.subscribe({ type: "trades", coin: convertedCoin }); this.ws.on("message", async (message) => { if (message.channel === "trades" && message.data[0].coin === convertedCoin) { message = await this.symbolConversion.convertSymbolsInObject(message, [ "coin" ]); callback(message.data); } }); } async subscribeToOrderUpdates(user, callback) { await this.subscribe({ type: "orderUpdates", user }); this.ws.on("message", async (message) => { if (message.channel === "orderUpdates") { message = await this.symbolConversion.convertSymbolsInObject(message); callback(message.data); } }); } async subscribeToUserEvents(user, callback) { await this.subscribe({ type: "userEvents", user }); this.ws.on("message", async (message) => { if (message.channel === "userEvents") { message = await this.symbolConversion.convertSymbolsInObject(message); callback(message.data); } }); } async subscribeToUserFills(user, callback) { await this.subscribe({ type: "userFills", user }); this.ws.on("message", async (message) => { if (message.channel === "userFills") { message = await this.symbolConversion.convertSymbolsInObject(message); callback(message.data); } }); } async subscribeToUserFundings(user, callback) { await this.subscribe({ type: "userFundings", user }); this.ws.on("message", async (message) => { if (message.channel === "userFundings") { message = await this.symbolConversion.convertSymbolsInObject(message); callback(message.data); } }); } async subscribeToUserNonFundingLedgerUpdates(user, callback) { await this.subscribe({ type: "userNonFundingLedgerUpdates", user }); this.ws.on("message", async (message) => { if (message.channel === "userNonFundingLedgerUpdates") { message = await this.symbolConversion.convertSymbolsInObject(message); callback(message.data); } }); } async subscribeToUserActiveAssetData(user, coin, callback) { await this.subscribe({ type: "activeAssetData", user, coin }); this.ws.on("message", async (message) => { if (message.channel === "activeAssetData") { message = await this.symbolConversion.convertSymbolsInObject(message); callback(message.data); } }); } async postRequest(requestType, payload) { const id = Date.now(); const convertedPayload = await this.symbolConversion.convertSymbolsInObject(payload); this.ws.sendMessage({ method: "post", id, request: { type: requestType, payload: convertedPayload } }); return new Promise((resolve, reject) => { const responseHandler = (message) => { if (typeof message === "object" && message !== null) { const data = message.data || message; if (data.channel === "post" && data.id === id) { this.ws.removeListener("message", responseHandler); if (data.response && data.response.type === "error") { reject(new Error(data.response.payload)); } else { const convertedResponse = this.symbolConversion.convertSymbolsInObject( data.response ? data.response.payload : data ); resolve(convertedResponse); } } } }; this.ws.on("message", responseHandler); setTimeout(() => { this.ws.removeListener("message", responseHandler); reject(new Error("Request timeout")); }, 3e4); }); } async unsubscribeFromAllMids() { this.unsubscribe({ type: "allMids" }); } async unsubscribeFromNotification(user) { this.unsubscribe({ type: "notification", user }); } async unsubscribeFromWebData2(user) { this.unsubscribe({ type: "webData2", user }); } async unsubscribeFromCandle(coin, interval) { this.unsubscribe({ type: "candle", coin, interval }); } async unsubscribeFromL2Book(coin) { this.unsubscribe({ type: "l2Book", coin }); } async unsubscribeFromTrades(coin) { this.unsubscribe({ type: "trades", coin }); } async unsubscribeFromOrderUpdates(user) { this.unsubscribe({ type: "orderUpdates", user }); } async unsubscribeFromUserEvents(user) { this.unsubscribe({ type: "userEvents", user }); } async unsubscribeFromUserFills(user) { this.unsubscribe({ type: "userFills", user }); } async unsubscribeFromUserFundings(user) { this.unsubscribe({ type: "userFundings", user }); } async unsubscribeFromUserNonFundingLedgerUpdates(user) { this.unsubscribe({ type: "userNonFundingLedgerUpdates", user }); } async unsubscribeFromUserActiveAssetData(user, coin) { this.unsubscribe({ type: "activeAssetData", user, coin }); } }; // src/utils/rateLimiter.ts var RateLimiter = class { tokens; lastRefill; capacity; constructor() { this.capacity = 1200; this.tokens = this.capacity; this.lastRefill = Date.now(); } refillTokens() { const now = Date.now(); const elapsedMinutes = (now - this.lastRefill) / (1e3 * 60); if (elapsedMinutes >= 1) { this.tokens = this.capacity; this.lastRefill = now; } } async waitForToken(weight = 1) { this.refillTokens(); if (this.tokens >= weight) { this.tokens -= weight; return; } const waitTime = (60 - (Date.now() - this.lastRefill) / 1e3) * 1e3; return new Promise((resolve) => setTimeout(resolve, waitTime)).then(() => { this.refillTokens(); return this.waitForToken(weight); }); } }; // src/rest/custom.ts var CustomOperations = class { exchange; infoApi; wallet; symbolConversion; constructor(exchange, infoApi, wallet, symbolConversion) { this.exchange = exchange; this.infoApi = infoApi; this.wallet = wallet; this.symbolConversion = symbolConversion; } async cancelAllOrders(symbol) { try { const address = await this.wallet.getAddress(); const openOrders = await this.infoApi.getUserOpenOrders(address); let ordersToCancel; for (let order of openOrders) { order.coin = await this.symbolConversion.convertSymbol(order.coin); } if (symbol) { ordersToCancel = openOrders.filter((order) => order.coin === symbol); } else { ordersToCancel = openOrders; } if (ordersToCancel.length === 0) { throw new Error("No orders to cancel"); } const cancelRequests = ordersToCancel.map( (order) => ({ coin: order.coin, o: order.oid }) ); return this.exchange.cancelOrder(cancelRequests); } catch (error) { throw error; } } async getAllAssets() { return await this.symbolConversion.getAllAssets(); } DEFAULT_SLIPPAGE = 0.05; async getSlippagePrice(symbol, isBuy, slippage, px) { const convertedSymbol = await this.symbolConversion.convertSymbol(symbol); if (!px) { const allMids = await this.infoApi.getAllMids(); px = Number(allMids[convertedSymbol]); } const isSpot = symbol.includes("-SPOT"); const decimals = px.toString().split(".")[1]?.length || 0; console.log(decimals); px *= isBuy ? 1 + slippage : 1 - slippage; const spotDecimals = px < 1 ? 5 : 8; return Number(px.toFixed(isSpot ? spotDecimals : decimals - 1)); } async marketOpen(symbol, isBuy, size, px, slippage = this.DEFAULT_SLIPPAGE, cloid) { const convertedSymbol = await this.symbolConversion.convertSymbol(symbol); const slippagePrice = await this.getSlippagePrice( convertedSymbol, isBuy, slippage, px ); const orderRequest = { coin: convertedSymbol, is_buy: isBuy, sz: size, limit_px: slippagePrice, order_type: { limit: { tif: "Ioc" } }, reduce_only: false }; if (cloid) { orderRequest.cloid = cloid; } console.debug("Order Request payload: ", orderRequest); return this.exchange.placeOrder(orderRequest); } async marketClose(symbol, size, px, slippage = this.DEFAULT_SLIPPAGE, cloid) { const convertedSymbol = await this.symbolConversion.convertSymbol(symbol); const address = await this.wallet.getAddress(); const positions = await this.infoApi.perpetuals.getClearinghouseState(address); for (const position of positions.assetPositions) { const item = position.position; if (convertedSymbol !== item.coin) { continue; } const szi = parseFloat(item.szi); const closeSize = size || Math.abs(szi); const isBuy = szi < 0; const slippagePrice = await this.getSlippagePrice( convertedSymbol, isBuy, slippage, px ); const orderRequest = { coin: convertedSymbol, is_buy: isBuy, sz: closeSize, limit_px: slippagePrice, order_type: { limit: { tif: "Ioc" } }, reduce_only: true }; if (cloid) { orderRequest.cloid = cloid; } return this.exchange.placeOrder(orderRequest); } throw new Error(`No position found for ${convertedSymbol}`); } async closeAllPositions(slippage = this.DEFAULT_SLIPPAGE) { try { const address = await this.wallet.getAddress(); const positions = await this.infoApi.perpetuals.getClearinghouseState(address); const closeOrders = []; console.log(positions); for (const position of positions.assetPositions) { const item = position.position; if (parseFloat(item.szi) !== 0) { const symbol = await this.symbolConversion.convertSymbol( item.coin, "forward" ); closeOrders.push( this.marketClose(symbol, void 0, void 0, slippage) ); } } return await Promise.all(closeOrders); } catch (error) { throw error; } } }; // src/utils/symbolConversion.ts var SymbolConversion = class { assetToIndexMap = /* @__PURE__ */ new Map(); exchangeToInternalNameMap = /* @__PURE__ */ new Map(); httpApi; refreshIntervalMs = 6e4; refreshInterval = null; initializationPromise; constructor(baseURL, rateLimiter) { this.httpApi = new HttpApi(baseURL, ENDPOINTS.INFO, rateLimiter); this.initializationPromise = this.initialize(); } async initialize() { await this.refreshAssetMaps(); this.startPeriodicRefresh(); } async refreshAssetMaps() { try { const [perpMeta, spotMeta] = await Promise.all([ // TODO: Fix any this.httpApi.makeRequest({ type: "metaAndAssetCtxs" /* PERPS_META_AND_ASSET_CTXS */ }), // TODO: Fix any this.httpApi.makeRequest({ type: "spotMetaAndAssetCtxs" /* SPOT_META_AND_ASSET_CTXS */ }) ]); this.assetToIndexMap.clear(); this.exchangeToInternalNameMap.clear(); perpMeta[0].universe.forEach((asset, index) => { const internalName = `${asset.name}-PERP`; this.assetToIndexMap.set(internalName, index); this.exchangeToInternalNameMap.set(asset.name, internalName); }); spotMeta[0].tokens.forEach((token) => { const universeItem = spotMeta[0].universe.find( (item) => item.tokens[0] === token.index ); if (universeItem) { const internalName = `${token.name}-SPOT`; const exchangeName = universeItem.name; const index = universeItem.index; this.assetToIndexMap.set(internalName, 1e4 + index); this.exchangeToInternalNameMap.set(exchangeName, internalName); } }); } catch (error) { console.error("Failed to refresh asset maps:", error); } } startPeriodicRefresh() { this.refreshInterval = setInterval(() => { this.refreshAssetMaps(); }, this.refreshIntervalMs); } stopPeriodicRefresh() { if (this.refreshInterval) { clearInterval(this.refreshInterval); this.refreshInterval = null; } } async ensureInitialized() { await this.initializationPromise; } async getInternalName(exchangeName) { await this.ensureInitialized(); return this.exchangeToInternalNameMap.get(exchangeName); } async getExchangeName(internalName) { await this.ensureInitialized(); for (const [ exchangeName, name ] of this.exchangeToInternalNameMap.entries()) { if (name === internalName) { return exchangeName; } } return void 0; } async getAssetIndex(assetSymbol) { await this.ensureInitialized(); return this.assetToIndexMap.get(assetSymbol); } async getAllAssets() { await this.ensureInitialized(); const perp = []; const spot = []; for (const [asset, index] of this.assetToIndexMap.entries()) { if (asset.endsWith("-PERP")) { perp.push(asset); } else if (asset.endsWith("-SPOT")) { spot.push(asset); } } return { perp, spot }; } async convertSymbol(symbol, mode = "", symbolMode = "") { await this.ensureInitialized(); let rSymbol; if (mode === "reverse") { for (const [key, value] of this.exchangeToInternalNameMap.entries()) { if (value === symbol) { return key; } } rSymbol = symbol; } else { rSymbol = this.exchangeToInternalNameMap.get(symbol) || symbol; } if (symbolMode === "SPOT") { if (!rSymbol.endsWith("-SPOT")) { rSymbol = symbol + "-SPOT"; } } else if (symbolMode === "PERP") { if (!rSymbol.endsWith("-PERP")) { rSymbol = symbol + "-PERP"; } } return rSymbol; } async convertSymbolsInObject(obj, symbolsFields = ["coin", "symbol"], symbolMode = "") { await this.ensureInitialized(); if (typeof obj !== "object" || obj === null) { return this.convertToNumber(obj); } if (Array.isArray(obj)) { return Promise.all( obj.map( (item) => this.convertSymbolsInObject(item, symbolsFields, symbolMode) ) ); } const convertedObj = {}; for (const [key, value] of Object.entries(obj)) { if (symbolsFields.includes(key)) { convertedObj[key] = await this.convertSymbol( value, "", symbolMode ); } else if (key === "side") { convertedObj[key] = value === "A" ? "sell" : value === "B" ? "buy" : value; } else { convertedObj[key] = await this.convertSymbolsInObject( value, symbolsFields, symbolMode );