@nktkas/hyperliquid
Version:
Hyperliquid API SDK for all major JS runtimes, written in TypeScript.
344 lines (343 loc) • 14 kB
JavaScript
import { meta, outcomeMeta, perpDexs, spotMeta } from "../api/info/mod.js";
/**
* Utility class for converting asset symbols to their corresponding IDs and size decimals.
* Supports perpetuals, spots, optional builder dexs and outcome markets.
*
* @example
* ```ts
* import { HttpTransport } from "@nktkas/hyperliquid";
* import { SymbolConverter } from "@nktkas/hyperliquid/utils";
*
* const transport = new HttpTransport(); // or `WebSocketTransport`
* const converter = await SymbolConverter.create({ transport });
*
* // By default, dexs are not loaded; specify them when creating an instance
* // const converter = await SymbolConverter.create({ transport, dexs: ["test"] });
*
* const btcId = converter.getAssetId("BTC"); // perpetual → 0
* const hypeUsdcId = converter.getAssetId("HYPE/USDC"); // spot → 10107
* const dexAbcId = converter.getAssetId("test:ABC"); // builder dex (if enabled) → 110000
* const outcomeId = converter.getAssetId("btc-above-61720-yes-jun-08-0600"); // outcome market → 100002200
*
* const btcSzDecimals = converter.getSzDecimals("BTC"); // perpetual → 5
* const hypeUsdcSzDecimals = converter.getSzDecimals("HYPE/USDC"); // spot → 2
* const dexAbcSzDecimals = converter.getSzDecimals("test:ABC"); // builder dex (if enabled) → 0
*
* const spotPairId = converter.getSpotPairId("HFUN/USDC"); // → "@2"
*
* const symbol = converter.getSymbolBySpotPairId("@107"); // → "HYPE/USDC"
* ```
*
* @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/asset-ids
*/ export class SymbolConverter {
_transport;
_dexOption;
_nameToAssetId = new Map();
_nameToSzDecimals = new Map();
_nameToSpotPairId = new Map();
_spotPairIdToName = new Map();
_reloadPromise;
/**
* Creates a new SymbolConverter instance, but does not initialize it.
* Run {@link reload} to load asset data or use {@link create} to create and initialize in one step.
*
* @param options Configuration options including transport and optional dex support.
*/ constructor(options){
this._transport = options.transport;
this._dexOption = options.dexs ?? false;
}
/**
* Create and initialize a SymbolConverter instance.
*
* @param options Configuration options including transport and optional dex support.
* @return Initialized SymbolConverter instance.
*
* @example
* ```ts
* import { HttpTransport } from "@nktkas/hyperliquid";
* import { SymbolConverter } from "@nktkas/hyperliquid/utils";
*
* const transport = new HttpTransport(); // or `WebSocketTransport`
* const converter = await SymbolConverter.create({ transport });
* ```
*/ static async create(options) {
const instance = new SymbolConverter(options);
await instance.reload();
return instance;
}
/**
* Reload asset mappings from the API.
*
* Useful for refreshing data when new assets are added.
*/ reload() {
// Prevent concurrent reloads: if a reload is already in progress, return the existing promise.
if (this._reloadPromise) return this._reloadPromise;
this._reloadPromise = this._reload().finally(()=>{
this._reloadPromise = undefined;
});
return this._reloadPromise;
}
async _reload() {
const config = {
transport: this._transport
};
const needDexs = this._dexOption === true || Array.isArray(this._dexOption) && this._dexOption.length > 0;
const [perpMetaData, spotMetaData, perpDexsData, outcomeMetaData] = await Promise.all([
meta(config),
spotMeta(config),
needDexs ? perpDexs(config) : undefined,
outcomeMeta(config)
]);
this._nameToAssetId.clear();
this._nameToSzDecimals.clear();
this._nameToSpotPairId.clear();
this._spotPairIdToName.clear();
this._processPerps(perpMetaData);
this._processSpot(spotMetaData);
this._processOutcomeMarkets(outcomeMetaData);
if (perpDexsData) await this._processBuilderDexs(perpDexsData);
}
// ============================================================
// Perpetuals
// ============================================================
_processPerps(perpMetaData) {
perpMetaData.universe.forEach((asset, index)=>{
this._nameToAssetId.set(asset.name, index);
this._nameToSzDecimals.set(asset.name, asset.szDecimals);
});
}
// ============================================================
// Spot
// ============================================================
_processSpot(spotMetaData) {
const tokenMap = new Map();
spotMetaData.tokens.forEach((token)=>{
tokenMap.set(token.index, {
name: token.name,
szDecimals: token.szDecimals
});
});
spotMetaData.universe.forEach((market)=>{
const baseToken = tokenMap.get(market.tokens[0]);
const quoteToken = tokenMap.get(market.tokens[1]);
if (!baseToken || !quoteToken) return;
const assetId = 10000 + market.index;
const baseQuoteKey = `${baseToken.name}/${quoteToken.name}`;
this._nameToAssetId.set(baseQuoteKey, assetId);
this._nameToSzDecimals.set(baseQuoteKey, baseToken.szDecimals);
this._nameToSpotPairId.set(baseQuoteKey, market.name);
this._spotPairIdToName.set(market.name, baseQuoteKey);
});
}
// ============================================================
// Builder dexs
// ============================================================
async _processBuilderDexs(perpDexsData) {
const builderDexs = perpDexsData.map((dex, index)=>({
dex,
index
})).filter((item)=>{
return item.index > 0 && item.dex !== null && item.dex.name.length > 0;
});
if (builderDexs.length === 0) return;
const dexsToProcess = Array.isArray(this._dexOption) ? builderDexs.filter((item)=>this._dexOption.includes(item.dex.name)) : builderDexs;
if (dexsToProcess.length === 0) return;
const config = {
transport: this._transport
};
const results = await Promise.allSettled(dexsToProcess.map((item)=>meta(config, {
dex: item.dex.name
})));
results.forEach((result, idx)=>{
if (result.status !== "fulfilled") return;
const dexIndex = dexsToProcess[idx].index;
const offset = 100000 + dexIndex * 10000;
result.value.universe.forEach((asset, index)=>{
const assetId = offset + index;
this._nameToAssetId.set(asset.name, assetId);
this._nameToSzDecimals.set(asset.name, asset.szDecimals);
});
});
}
// ============================================================
// Outcome markets
// ============================================================
_processOutcomeMarkets(outcomeMetaData) {
// Map each named outcome to its owning question, which holds the price spec and bucket order
const questionByOutcome = new Map();
outcomeMetaData.questions.forEach((question)=>{
question.namedOutcomes.forEach((outcomeId)=>questionByOutcome.set(outcomeId, question));
});
// Fallback outcomes are not tradable on their own and have no public slug
const fallbackOutcomes = new Set(outcomeMetaData.questions.map((question)=>question.fallbackOutcome));
outcomeMetaData.outcomes.forEach((outcome)=>{
if (fallbackOutcomes.has(outcome.outcome)) return;
const question = questionByOutcome.get(outcome.outcome);
outcome.sideSpecs.forEach((sideSpec, sideIdx)=>{
const slug = this._outcomeSlug(outcome, sideSpec.name, question);
if (!slug) return;
this._nameToAssetId.set(slug, 100000000 + 10 * outcome.outcome + sideIdx);
// Outcome markets are absent from szDecimals metadata; they are all 5
this._nameToSzDecimals.set(slug, 5);
});
});
}
/**
* Builds the URL slug for an outcome side.
*
* Recurring price markets encode their slug in a `class:price*` description; every other market
* derives it from the question, outcome, and side names (e.g. "nba-finals-game-3-san-antonio").
*/ _outcomeSlug(outcome, side, question) {
if (outcome.description.includes("class:priceBinary")) {
return this._recurringPriceSlug(outcome.description, side, 0);
}
if (question?.description.includes("class:priceBucket")) {
return this._recurringPriceSlug(question.description, side, question.namedOutcomes.indexOf(outcome.outcome));
}
return [
question?.name,
outcome.name,
side
].filter((part)=>part !== undefined).map((part)=>this._slugify(part)).join("-");
}
/**
* Builds the slug for a recurring price market from its `class:price*` description.
*
* @param spec Pipe-delimited recurring market spec (e.g. "class:priceBinary|underlying:BTC|expiry:...").
* @param side Side name (e.g. "Yes").
* @param bucketIndex Price range position for `priceBucket` markets (0 = below, 1 = between, 2 = above).
* @return The generated slug, or `null` if the spec is incomplete or of an unknown class.
*
* @see https://hyperliquid.gitbook.io/hyperliquid-docs/trading/contract-specifications
*/ _recurringPriceSlug(spec, side, bucketIndex) {
const fields = {};
for (const part of spec.split("|")){
const [key, ...value] = part.split(":");
fields[key] = value.join(":");
}
const { class: assetClass, underlying, expiry } = fields;
if (!assetClass || !underlying || !expiry) return null;
// Expiry "YYYYMMDD-HHMM" becomes the slug date "mon-dd-HHMM"
const months = [
"jan",
"feb",
"mar",
"apr",
"may",
"jun",
"jul",
"aug",
"sep",
"oct",
"nov",
"dec"
];
const date = `${months[Number(expiry.slice(4, 6)) - 1]}-${expiry.slice(6, 8)}-${expiry.slice(9, 13)}`;
if (assetClass === "priceBinary") {
const { targetPrice } = fields;
if (!targetPrice) return null;
return `${underlying}-above-${targetPrice}-${side}-${date}`.toLowerCase();
}
if (assetClass === "priceBucket") {
const prices = fields.priceThresholds?.split(",");
if (prices?.length !== 2) return null;
let range;
if (bucketIndex === 0) {
range = `below-${prices[0]}`;
} else if (bucketIndex === 1) {
range = `${prices[0]}-to-${prices[1]}`;
} else {
range = `above-${prices[1]}`;
}
return `${underlying}-price-range-${date}-${range}-${side}`.toLowerCase();
}
return null;
}
/** Converts a market or side name to a URL slug. */ _slugify(name) {
return name.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/[\s-]+/g, "-").replace(/^-+|-+$/g, "");
}
// ============================================================
// Public API
// ============================================================
/**
* Get asset ID for a coin.
* - For Perpetuals, use the coin name (e.g., "BTC").
* - For Spots, use the "BASE/QUOTE" format (e.g., "HYPE/USDC").
* - For Builder Dexs, use the "DEX_NAME:ASSET_NAME" format (e.g., "test:ABC").
* - For Outcome Markets, use the slug as in the URL (e.g., "btc-above-61720-yes-jun-08-0600").
*
* @example
* ```ts
* import { HttpTransport } from "@nktkas/hyperliquid";
* import { SymbolConverter } from "@nktkas/hyperliquid/utils";
*
* const transport = new HttpTransport(); // or `WebSocketTransport`
* const converter = await SymbolConverter.create({ transport });
*
* converter.getAssetId("BTC"); // → 0
* converter.getAssetId("HYPE/USDC"); // → 10107
* converter.getAssetId("test:ABC"); // → 110000
* converter.getAssetId("btc-above-61720-yes-jun-08-0600"); // → 100002200
* ```
*/ getAssetId(name) {
return this._nameToAssetId.get(name);
}
/**
* Get size decimals for a coin.
* - For Perpetuals, use the coin name (e.g., "BTC").
* - For Spots, use the "BASE/QUOTE" format (e.g., "HYPE/USDC").
* - For Builder Dexs, use the "DEX_NAME:ASSET_NAME" format (e.g., "test:ABC").
* - For Outcome Markets, use the slug as in the URL (e.g., "btc-above-61720-yes-jun-08-0600").
*
* @example
* ```ts
* import { HttpTransport } from "@nktkas/hyperliquid";
* import { SymbolConverter } from "@nktkas/hyperliquid/utils";
*
* const transport = new HttpTransport(); // or `WebSocketTransport`
* const converter = await SymbolConverter.create({ transport });
*
* converter.getSzDecimals("BTC"); // → 5
* converter.getSzDecimals("HYPE/USDC"); // → 2
* converter.getSzDecimals("test:ABC"); // → 0
* converter.getSzDecimals("btc-above-61720-yes-jun-08-0600"); // → 5
* ```
*/ getSzDecimals(name) {
return this._nameToSzDecimals.get(name);
}
/**
* Get spot pair ID (e.g., @2) for info endpoints and subscriptions (e.g., l2book, trades).
*
* @example
* ```ts
* import { HttpTransport } from "@nktkas/hyperliquid";
* import { SymbolConverter } from "@nktkas/hyperliquid/utils";
*
* const transport = new HttpTransport(); // or `WebSocketTransport`
* const converter = await SymbolConverter.create({ transport });
*
* converter.getSpotPairId("HFUN/USDC"); // → "@2"
* converter.getSpotPairId("PURR/USDC"); // → "PURR/USDC" (exceptions exist for some pairs)
* ```
*/ getSpotPairId(name) {
return this._nameToSpotPairId.get(name);
}
/**
* Get the symbol ("BASE/QUOTE") from a spot pair ID (e.g., @107).
*
* @example
* ```ts
* import { HttpTransport } from "@nktkas/hyperliquid";
* import { SymbolConverter } from "@nktkas/hyperliquid/utils";
*
* const transport = new HttpTransport(); // or `WebSocketTransport`
* const converter = await SymbolConverter.create({ transport });
*
* converter.getSymbolBySpotPairId("@107"); // → "HYPE/USDC"
* converter.getSymbolBySpotPairId("PURR/USDC"); // → "PURR/USDC" (exceptions exist for some pairs)
* ```
*/ getSymbolBySpotPairId(pairId) {
return this._spotPairIdToName.get(pairId);
}
}
//# sourceMappingURL=_symbolConverter.js.map