hyperliquid-sdk
Version:
<<< Typescript SDK for the Hyperliquid API >>>
1,627 lines (1,611 loc) • 52.8 kB
JavaScript
// 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
);