@layerzerolabs/hyperliquid-composer
Version:
LayerZero Labs reference EVM OmniChain Fungible Token (OFT) implementation for Hyperliquid
278 lines (243 loc) • 10.1 kB
text/typescript
import { HyperliquidClient, IHyperliquidSigner } from '../signer'
import {
BaseInfoRequest,
SpotMeta,
SpotInfo,
SpotDeployStates,
SpotMetaUniverse,
SpotPair,
SpotPairsWithMetadata,
SpotPairDeployAuctionStatus,
} from '../types'
import { HYPE_INDEX } from '../types/constants'
export async function getSpotMeta(
signer: IHyperliquidSigner | null,
isTestnet: boolean,
logLevel: string,
tokenIndex: string
) {
const action: BaseInfoRequest = {
type: 'spotMeta',
}
const hyperliquidClient = new HyperliquidClient(isTestnet, logLevel)
const response = await hyperliquidClient.submitHyperliquidAction('/info', signer, action)
const tokens = (response as SpotMeta).tokens
const token = tokens.find((token) => token.index === parseInt(tokenIndex))
if (!token) {
throw new Error(`Token ${tokenIndex} not found`)
}
return token
}
export async function getHipTokenInfo(
signer: IHyperliquidSigner | null,
isTestnet: boolean,
logLevel: string,
tokenAddress: string
) {
const action: BaseInfoRequest = {
type: 'tokenDetails',
tokenId: tokenAddress,
}
const hyperliquidClient = new HyperliquidClient(isTestnet, logLevel)
const response = await hyperliquidClient.submitHyperliquidAction('/info', signer, action)
const token = response as SpotInfo
return token
}
export async function getSpotDeployState(deployerAddres: string, isTestnet: boolean, logLevel: string) {
const action: BaseInfoRequest = {
type: 'spotDeployState',
user: deployerAddres,
}
const hyperliquidClient = new HyperliquidClient(isTestnet, logLevel)
const response = await hyperliquidClient.submitHyperliquidAction('/info', null, action)
const deployState = response as SpotDeployStates
return deployState
}
/**
* Gets all spot trading pairs for a given coreSpot token index.
* This function fetches the complete spotMeta universe and filters for pairs
* that include the specified token index.
*
* @param isTestnet Whether to query testnet or mainnet
* @param tokenIndex The token index to find pairs for
* @param logLevel Logging level for the client
* @returns Array of spot pairs that include the specified token
*/
export async function getSpotPairs(isTestnet: boolean, tokenIndex: number, logLevel: string): Promise<SpotPair[]> {
const action: BaseInfoRequest = {
type: 'spotMeta',
}
const hyperliquidClient = new HyperliquidClient(isTestnet, logLevel)
const response = await hyperliquidClient.submitHyperliquidAction('/info', null, action)
const spotMetaUniverse = response as SpotMetaUniverse
// Filter pairs that include the specified token index
const filteredPairs = spotMetaUniverse.universe.filter((pair) => pair.tokens.includes(tokenIndex))
return filteredPairs
}
/**
* Gets all spot trading pairs for a given coreSpot token index along with token metadata.
* This function fetches both the pairs and token metadata for name resolution.
*
* @param isTestnet Whether to query testnet or mainnet
* @param tokenIndex The token index to find pairs for
* @param logLevel Logging level for the client
* @returns Object containing filtered pairs and token metadata
*/
export async function getSpotPairsWithMetadata(
isTestnet: boolean,
tokenIndex: number,
logLevel: string
): Promise<SpotPairsWithMetadata> {
const action: BaseInfoRequest = {
type: 'spotMeta',
}
const hyperliquidClient = new HyperliquidClient(isTestnet, logLevel)
const response = await hyperliquidClient.submitHyperliquidAction('/info', null, action)
const spotMetaUniverse = response as SpotMetaUniverse
// Filter pairs that include the specified token index
const filteredPairs = spotMetaUniverse.universe.filter((pair) => pair.tokens.includes(tokenIndex))
return {
pairs: filteredPairs,
tokens: spotMetaUniverse.tokens,
}
}
/**
* Gets the current spot pair deploy auction status.
* This shows information about the ongoing auction for spot pair deployments.
*
* @param isTestnet Whether to query testnet or mainnet
* @param logLevel Logging level for the client
* @returns Current auction status information
*/
export async function getSpotPairDeployAuctionStatus(
isTestnet: boolean,
logLevel: string
): Promise<SpotPairDeployAuctionStatus> {
const action: BaseInfoRequest = {
type: 'spotPairDeployAuctionStatus',
}
const hyperliquidClient = new HyperliquidClient(isTestnet, logLevel)
const response = await hyperliquidClient.submitHyperliquidAction('/info', null, action)
return response as SpotPairDeployAuctionStatus
}
/**
* Gets the quote tokens that a given core spot token is already paired with.
* This is useful to check what trading pairs already exist before deploying new ones.
*
* @param isTestnet Whether to query testnet or mainnet
* @param tokenIndex The token index to check existing pairs for
* @param logLevel Logging level for the client
* @returns Array of quote token indices that the token is already paired with
*/
export async function getExistingQuoteTokens(
isTestnet: boolean,
tokenIndex: number,
logLevel: string
): Promise<number[]> {
const pairs = await getSpotPairs(isTestnet, tokenIndex, logLevel)
// Extract the quote tokens (other token in each pair)
const quoteTokens = pairs
.map((pair) => {
// Find the token that's NOT the input tokenIndex
return pair.tokens.find((token) => token !== tokenIndex)
})
.filter((token): token is number => token !== undefined)
return [...new Set(quoteTokens)] // Remove duplicates
}
/**
* Checks if a token is a quote asset by looking at HYPE trading pairs.
* When any core spot is promoted to a quote asset (fee token), the Hyperliquid protocol
* automatically deploys a new spot market for HYPE/QUOTE_ASSET.
*
* @param isTestnet Whether to query testnet or mainnet
* @param tokenIndex The token index to check (optional - if not provided, returns all quote assets)
* @param logLevel Logging level for the client
* @returns Object containing isQuoteAsset boolean and tokenName string
*/
/**
* Gets pending spot pairs that exist in spotMetaAndAssetCtxs but NOT in the universe.
* These are spots that have been registered via registerSpot but not yet finalized.
*
* IMPORTANT: The Hyperliquid API does not provide token composition info for pending spots,
* so this function returns ALL pending spots network-wide, not filtered by token.
* Users should use the --spot-index flag to directly specify which spot to finalize.
*
* @param isTestnet Whether to query testnet or mainnet
* @param logLevel Logging level for the client
* @returns Array of pending spot indices and their details (network-wide, not token-specific)
*/
export async function getPendingSpotPairs(
isTestnet: boolean,
logLevel: string
): Promise<Array<{ spotIndex: number; coin: string; markPx: string }>> {
const hyperliquidClient = new HyperliquidClient(isTestnet, logLevel)
// Get spotMetaAndAssetCtxs which includes pending spots
const assetCtxsAction: BaseInfoRequest = {
type: 'spotMetaAndAssetCtxs',
}
const assetCtxsResponse = (await hyperliquidClient.submitHyperliquidAction('/info', null, assetCtxsAction)) as [
SpotMetaUniverse,
Array<{ coin: string; markPx: string; circulatingSupply: string; totalSupply: string }>,
]
const universe = assetCtxsResponse[0].universe
const assetCtxs = assetCtxsResponse[1]
// Get spot indices that are in the universe (live)
const liveSpotIndices = new Set(universe.map((pair) => pair.index))
// Find spots in assetCtxs that are NOT in the universe (pending)
const pendingSpots: Array<{ spotIndex: number; coin: string; markPx: string }> = []
for (const ctx of assetCtxs) {
// Extract spot index from coin name (e.g., "@1421" -> 1421)
const match = ctx.coin.match(/^@(\d+)$/)
if (match && match[1]) {
const spotIndex = parseInt(match[1])
if (!liveSpotIndices.has(spotIndex)) {
pendingSpots.push({
spotIndex,
coin: ctx.coin,
markPx: ctx.markPx,
})
}
}
}
return pendingSpots
}
export async function isQuoteAsset(
isTestnet: boolean,
tokenIndex: number | null,
logLevel: string
): Promise<{ isQuoteAsset: boolean; tokenName: string; allQuoteAssets?: Array<{ index: number; name: string }> }> {
// Get HYPE token index based on network
const hypeTokenIndex = isTestnet ? HYPE_INDEX.TESTNET : HYPE_INDEX.MAINNET
// Get all HYPE trading pairs with metadata
const { pairs, tokens } = await getSpotPairsWithMetadata(isTestnet, hypeTokenIndex, logLevel)
// Extract all quote assets paired with HYPE
const quoteAssets = pairs
.map((pair) => {
// Find the token that's NOT HYPE
const quoteTokenIndex = pair.tokens.find((token) => token !== hypeTokenIndex)
if (quoteTokenIndex === undefined) {
return null
}
// Find the token metadata
const tokenMetadata = tokens.find((token) => token.index === quoteTokenIndex)
return {
index: quoteTokenIndex,
name: tokenMetadata?.name || `Token-${quoteTokenIndex}`,
}
})
.filter((asset): asset is { index: number; name: string } => asset !== null)
// If no tokenIndex provided, return all quote assets
if (tokenIndex === null) {
return {
isQuoteAsset: false,
tokenName: '',
allQuoteAssets: quoteAssets,
}
}
// Check if the provided tokenIndex is in the list of quote assets
const matchedAsset = quoteAssets.find((asset) => asset.index === tokenIndex)
return {
isQuoteAsset: matchedAsset !== undefined,
tokenName: matchedAsset?.name || '',
}
}