diff --git a/.changeset/ten-files-tell.md b/.changeset/ten-files-tell.md new file mode 100644 index 000000000..36d4c5373 --- /dev/null +++ b/.changeset/ten-files-tell.md @@ -0,0 +1,5 @@ +--- +'@relayprotocol/relay-kit-ui': patch +--- + +Support hyperliquid balance fetching for spot coins diff --git a/packages/ui/src/hooks/index.ts b/packages/ui/src/hooks/index.ts index bbe977d3b..8756cf440 100644 --- a/packages/ui/src/hooks/index.ts +++ b/packages/ui/src/hooks/index.ts @@ -21,7 +21,7 @@ import useFallbackState from './useFallbackState.js' import useMoonPayTransaction from './useMoonPayTransaction.js' import { useInternalRelayChains } from './useInternalRelayChains.js' import useGasTopUpRequired from './useGasTopUpRequired.js' -import useHyperliquidUsdcBalance from './useHyperliquidUsdcBalance.js' +import useHyperliquidBalance from './useHyperliquidBalance.js' import useEOADetection from './useEOADetection.js' import useTransactionCount from './useTransactionCount.js' import useTronBalance from './useTronBalance.js' @@ -50,7 +50,7 @@ export { useMoonPayTransaction, useInternalRelayChains, useGasTopUpRequired, - useHyperliquidUsdcBalance, + useHyperliquidBalance, useEOADetection, useTransactionCount, useTronBalance diff --git a/packages/ui/src/hooks/useCurrencyBalance.ts b/packages/ui/src/hooks/useCurrencyBalance.ts index 0587901ed..f4edc788b 100644 --- a/packages/ui/src/hooks/useCurrencyBalance.ts +++ b/packages/ui/src/hooks/useCurrencyBalance.ts @@ -17,7 +17,7 @@ import { isValidAddress } from '../utils/address.js' import useRelayClient from './useRelayClient.js' import useEclipseBalance from '../hooks/useEclipseBalance.js' import { eclipse } from '../utils/solana.js' -import useHyperliquidUsdcBalance from './useHyperliquidUsdcBalance.js' +import useHyperliquidBalance from './useHyperliquidBalance.js' import useTronBalance from '../hooks/useTronBalance.js' type UseBalanceProps = { @@ -168,18 +168,22 @@ const useCurrencyBalance = ({ ) }) - const hyperliquidUsdcBalance = useHyperliquidUsdcBalance(address, { - enabled: Boolean( - !adaptedWalletBalanceIsEnabled && - chain && - chain.vmType === 'hypevm' && - address && - _isValidAddress && - enabled - ), - gcTime: refreshInterval, - staleTime: refreshInterval - }) + const hyperliquidBalance = useHyperliquidBalance( + address, + currency as string, + { + enabled: Boolean( + !adaptedWalletBalanceIsEnabled && + chain && + chain.vmType === 'hypevm' && + address && + _isValidAddress && + enabled + ), + gcTime: refreshInterval, + staleTime: refreshInterval + } + ) const tronBalance = useTronBalance(address, currency, { enabled: Boolean( @@ -297,11 +301,11 @@ const useCurrencyBalance = ({ } } else if (chain?.vmType === 'hypevm') { return { - value: hyperliquidUsdcBalance.balance, - queryKey: hyperliquidUsdcBalance.queryKey, - isLoading: hyperliquidUsdcBalance.isLoading, - isError: hyperliquidUsdcBalance.isError, - error: hyperliquidUsdcBalance.error, + value: hyperliquidBalance.balance, + queryKey: hyperliquidBalance.queryKey, + isLoading: hyperliquidBalance.isLoading, + isError: hyperliquidBalance.isError, + error: hyperliquidBalance.error, isDuneBalance: false } } else if (chain?.vmType === 'tvm') { diff --git a/packages/ui/src/hooks/useHyperliquidBalance.ts b/packages/ui/src/hooks/useHyperliquidBalance.ts new file mode 100644 index 000000000..c20d36e59 --- /dev/null +++ b/packages/ui/src/hooks/useHyperliquidBalance.ts @@ -0,0 +1,117 @@ +import { isAddress, parseUnits } from 'viem' +import { + useQuery, + type DefaultError, + type QueryKey +} from '@tanstack/react-query' + +export type HyperliquidMarginSummary = { + accountValue?: string + totalNtlPos?: string + totalRawUsd?: string + totalMarginUsed?: string +} + +export type HyperLiquidPerpsResponse = { + marginSummary?: HyperliquidMarginSummary + crossMarginSummary?: HyperliquidMarginSummary + crossMaintenanceMarginUsed?: string + withdrawable?: string + assetPositions?: any[] + time?: number +} + +export type HyperliquidSpotBalance = { + coin: string + token: number + hold: string + total: string + entryNtl: string +} + +export type HyperliquidSpotResponse = { + balances: HyperliquidSpotBalance[] +} + +type QueryType = typeof useQuery< + string | undefined, + DefaultError, + string | undefined, + QueryKey +> +type QueryOptions = Parameters['0'] + +// Perps USDC uses zero address +const PERPS_USDC_ADDRESS = '0x00000000000000000000000000000000' + +// Map currency addresses to Hyperliquid spot coin symbols and decimals +const SPOT_TOKEN_CONFIG: Record = { + '0x2e6d84f2d7ca82e6581e03523e4389f7': { coin: 'USDe', decimals: 2 }, + '0x54e00a5988577cb0b0c9ab0cb6ef7f4b': { coin: 'USDH', decimals: 2 } +} + +export default ( + address?: string, + currency: string = PERPS_USDC_ADDRESS, + queryOptions?: Partial +) => { + const isEvmAddress = isAddress(address ?? '') + const isPerps = currency === PERPS_USDC_ADDRESS + const spotConfig = SPOT_TOKEN_CONFIG[currency.toLowerCase()] + const decimals = isPerps ? 8 : (spotConfig?.decimals ?? 2) + + const queryKey = ['useHyperliquidBalance', address, currency] + + const response = (useQuery as QueryType)({ + queryKey, + queryFn: async () => { + if (!address || !isEvmAddress) { + return undefined + } + + if (isPerps) { + // Fetch perps balance + const res = await fetch('https://api.hyperliquid.xyz/info', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + type: 'clearinghouseState', + user: address + }) + }) + const data = (await res.json()) as HyperLiquidPerpsResponse + return data?.withdrawable + } else if (spotConfig) { + // Fetch spot balances + const res = await fetch('https://api.hyperliquid.xyz/info', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + type: 'spotClearinghouseState', + user: address + }) + }) + const data = (await res.json()) as HyperliquidSpotResponse + // Find the balance matching the coin symbol + const tokenBalance = data?.balances?.find( + (b) => b.coin.toLowerCase() === spotConfig.coin.toLowerCase() + ) + return tokenBalance?.total + } + return undefined + }, + enabled: address !== undefined && isEvmAddress, + ...queryOptions + }) + + const balance = parseUnits(response.data ?? '0', decimals) + + return { + ...response, + balance, + queryKey + } as ReturnType & { + balance: bigint + queryKey: (string | undefined)[] + } +} diff --git a/packages/ui/src/hooks/useHyperliquidUsdcBalance.ts b/packages/ui/src/hooks/useHyperliquidUsdcBalance.ts deleted file mode 100644 index 047762d34..000000000 --- a/packages/ui/src/hooks/useHyperliquidUsdcBalance.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { isAddress, parseUnits } from 'viem' -import { - useQuery, - type DefaultError, - type QueryKey -} from '@tanstack/react-query' - -export type HyperliquidMarginSummary = { - accountValue?: string - totalNtlPos?: string - totalRawUsd?: string - totalMarginUsed?: string -} - -export type HyperLiquidBalanceResponse = { - marginSummary?: HyperliquidMarginSummary - crossMarginSummary?: HyperliquidMarginSummary - crossMaintenanceMarginUsed?: string - withdrawable?: string - assetPositions?: any[] - time?: number -} | null - -type QueryType = typeof useQuery< - HyperLiquidBalanceResponse, - DefaultError, - HyperLiquidBalanceResponse, - QueryKey -> -type QueryOptions = Parameters['0'] - -export default (address?: string, queryOptions?: Partial) => { - const queryKey = ['useHyperliquidBalances', address] - const isEvmAddress = isAddress(address ?? '') - - const response = (useQuery as QueryType)({ - queryKey: ['useHyperliquidBalances', address], - queryFn: async () => { - if (!address || !isEvmAddress) { - return null - } - - const response = await fetch('https://api.hyperliquid.xyz/info', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - user: address, - type: 'clearinghouseState' - }) - }) - - const data = await response.json() - - return data as HyperLiquidBalanceResponse - }, - ...queryOptions, - enabled: address !== undefined && queryOptions?.enabled && isEvmAddress - }) - - const balance = parseUnits(response.data?.withdrawable ?? '0', 8) - - return { - ...response, - balance, - queryKey - } as ReturnType & { - balance: bigint | undefined - queryKey: (string | undefined)[] - } -}