diff --git a/apps/terminal/src/lib/tests/hl-react-exports.test.ts b/apps/terminal/src/lib/tests/hl-react-exports.test.ts new file mode 100644 index 00000000..8f2c7c51 --- /dev/null +++ b/apps/terminal/src/lib/tests/hl-react-exports.test.ts @@ -0,0 +1,60 @@ +import { existsSync, readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +type PackageJsonExportTarget = + | string + | null + | PackageJsonExportTarget[] + | { [condition: string]: PackageJsonExportTarget }; + +interface PackageJson { + exports: Record; +} + +const TEST_DIR = dirname(fileURLToPath(import.meta.url)); +const WORKSPACE_ROOT = findWorkspaceRoot(TEST_DIR); +const PACKAGE_DIR = resolve(WORKSPACE_ROOT, "packages/hl-react"); +const packageJson = JSON.parse(readFileSync(resolve(PACKAGE_DIR, "package.json"), "utf8")) as PackageJson; + +function findWorkspaceRoot(startDir: string): string { + let current = startDir; + + while (true) { + if (existsSync(resolve(current, "pnpm-workspace.yaml"))) return current; + + const parent = dirname(current); + if (parent === current) throw new Error("Could not locate pnpm-workspace.yaml"); + current = parent; + } +} + +function getExportTargetPaths(target: PackageJsonExportTarget): string[] { + if (typeof target === "string") return [target]; + if (target === null) return []; + if (Array.isArray(target)) return target.flatMap(getExportTargetPaths); + + return Object.values(target).flatMap(getExportTargetPaths); +} + +describe("@hypeterminal/hl-react package exports", () => { + it("points every subpath export at an existing source file", () => { + for (const [subpath, target] of Object.entries(packageJson.exports)) { + for (const targetPath of getExportTargetPaths(target)) { + if (targetPath.includes("*")) continue; + expect(existsSync(resolve(PACKAGE_DIR, targetPath)), `${subpath} -> ${targetPath}`).toBe(true); + } + } + }); + + it("exposes direct hook subpaths", () => { + expect(packageJson.exports).toMatchObject({ + "./hooks/useApiStatus": "./src/hooks/useApiStatus.ts", + "./hooks/useClients": "./src/hooks/useClients.ts", + "./hooks/useExchange": "./src/hooks/useExchange.ts", + "./hooks/useTradingGuard": "./src/hooks/useTradingGuard.ts", + "./hooks/useTransport": "./src/hooks/useTransport.ts", + }); + }); +}); diff --git a/apps/terminal/src/lib/tests/hyperliquid-registries.test.ts b/apps/terminal/src/lib/tests/hyperliquid-registries.test.ts index 34d28921..a58001c0 100644 --- a/apps/terminal/src/lib/tests/hyperliquid-registries.test.ts +++ b/apps/terminal/src/lib/tests/hyperliquid-registries.test.ts @@ -26,12 +26,19 @@ describe("exchange registry", () => { const EXPECTED_USER_SIGNED: ExchangeMethod[] = [ "approveAgent", "approveBuilderFee", + "cDeposit", + "cWithdraw", + "convertToMultiSigUser", + "linkStakingUser", "sendAsset", + "sendToEvmWithData", "spotSend", "tokenDelegate", "usdClassTransfer", "usdSend", "userDexAbstraction", + "userPortfolioMargin", + "userSetAbstraction", "withdraw3", ]; diff --git a/packages/hl-react/README.md b/packages/hl-react/README.md index 687eb570..17fd8c44 100644 --- a/packages/hl-react/README.md +++ b/packages/hl-react/README.md @@ -75,6 +75,18 @@ Wrap inside `WagmiProvider` and `QueryClientProvider` — the hooks depend on bo All hooks are strongly typed against the SDK's method signatures — if a method exists on the underlying SDK, a typed hook path exists for it. +Endpoint coverage is intentionally generic rather than one-hook-per-endpoint: + +| SDK client | React hook | Example | +|---|---|---| +| `InfoClient` | `useInfo(method, params, options)` | `useInfo("perpDexs", undefined)` | +| `SubscriptionClient` | `useSubscription(method, params, options)` | `useSubscription("allDexsAssetCtxs", undefined)` | +| `ExchangeClient` | `useExchange(method, options)` | `useExchange("sendToEvmWithData")` | + +As of `@nktkas/hyperliquid` `0.32.2`, newer SDK methods such as prediction-market info (`outcomeMeta`, `perpCategories`), builder/HIP-3 info (`perpDexs`, `perpDexStatus`, `userDexAbstraction`, `perpDexLimits`), richer subscriptions (`allDexsAssetCtxs`, `allDexsClearinghouseState`), and exchange actions (`sendToEvmWithData`, `setDisplayName`, `convertToMultiSigUser`, `userSetAbstraction`, `userPortfolioMargin`) are available through the same generic hooks. + +HIP-4 status: Hyperliquid's HIP-4 outcome-market API is present in the locked `0.32.2` SDK as `useInfo("outcomeMeta", undefined)`. Upstream `main` also contains additional outcome-market methods, but this package documents and tests against the pinned workspace SDK version. The peer dependency accepts newer 0.x SDK minors for install compatibility; those newer minors are untested until the workspace SDK pin is bumped. + ### `useInfo` — REST one-shot queries Thin wrapper around `InfoClient` methods (`meta`, `userFills`, `clearinghouseState`, `l2Book`, etc.), cached by TanStack Query via `infoKey` / `infoQueryOptions`. @@ -105,7 +117,28 @@ Backing store tracks `status` (`idle | subscribing | active | error`) per key, r ### `useExchange` — signed mutations -Wraps `ExchangeClient` methods (`order`, `cancel`, `modify`, `approveAgent`, `approveBuilderFee`, `withdraw`, …). Returns a TanStack Query mutation. Signing comes from either the agent wallet (default, fast-path trading) or the user wallet (approvals, withdrawals) — `useClients` decides. +Wraps `ExchangeClient` methods (`order`, `cancel`, `modify`, `approveAgent`, `approveBuilderFee`, `withdraw3`, `sendToEvmWithData`, …). Returns a TanStack Query mutation. Signing comes from either the agent wallet (default, fast-path L1 trading actions) or the connected user wallet (EIP-712/user-signed actions) — `registries/exchange.ts` decides. + +User-wallet routed methods currently include: + +```text +approveAgent +approveBuilderFee +cDeposit +cWithdraw +convertToMultiSigUser +linkStakingUser +sendAsset +sendToEvmWithData +spotSend +tokenDelegate +usdClassTransfer +usdSend +userDexAbstraction +userPortfolioMargin +userSetAbstraction +withdraw3 +``` ### `useAgent*` — agent wallet lifecycle @@ -150,7 +183,7 @@ The Zustand store (`store.ts`) drives the lifecycle: `acquireSubscription` bumps ## Peer dependencies ```json -"@nktkas/hyperliquid": "^0.31.0", +"@nktkas/hyperliquid": ">=0.32.2 <1", "@tanstack/react-query": "^5", "react": "^19", "viem": "^2", @@ -167,8 +200,9 @@ Fine-grained entry points for consumers that want only part of the surface: ```ts import { useHyperliquidStore } from "@hypeterminal/hl-react/store"; +import { useExchange } from "@hypeterminal/hl-react/hooks/useExchange"; import type { PerpMarket } from "@hypeterminal/hl-react/markets/types"; -import { subscriptionRegistry } from "@hypeterminal/hl-react/registries/subscription"; +import { getAccumulateConfig } from "@hypeterminal/hl-react/registries/subscription"; import { estimatePayloadSizeBytes } from "@hypeterminal/hl-react/internal/websocket/payload-guard"; import { RingBuffer } from "@hypeterminal/hl-react/internal/circular-buffer/ring-buffer"; ``` diff --git a/packages/hl-react/package.json b/packages/hl-react/package.json index aad23980..5ffaefff 100644 --- a/packages/hl-react/package.json +++ b/packages/hl-react/package.json @@ -11,8 +11,13 @@ "./provider": "./src/provider.tsx", "./clients": "./src/clients.ts", "./create-config": "./src/create-config.ts", + "./hooks/useApiStatus": "./src/hooks/useApiStatus.ts", + "./hooks/useClients": "./src/hooks/useClients.ts", + "./hooks/useExchange": "./src/hooks/useExchange.ts", "./hooks/useInfo": "./src/hooks/useInfo.ts", "./hooks/useSubscription": "./src/hooks/useSubscription.ts", + "./hooks/useTradingGuard": "./src/hooks/useTradingGuard.ts", + "./hooks/useTransport": "./src/hooks/useTransport.ts", "./lru": "./src/lru.ts", "./hooks/utils/useSub": "./src/hooks/utils/useSub.ts", "./markets/types": "./src/markets/types.ts", diff --git a/packages/hl-react/src/hooks/useClients.ts b/packages/hl-react/src/hooks/useClients.ts index 30c5b381..018f30de 100644 --- a/packages/hl-react/src/hooks/useClients.ts +++ b/packages/hl-react/src/hooks/useClients.ts @@ -10,7 +10,7 @@ export interface HyperliquidClients { subscription: SubscriptionClient; /** Exchange client using agent wallet - for L1 trading actions (order, cancel, etc.) */ trading: ExchangeClient | null; - /** Exchange client using user wallet - for user-signed actions (approveAgent, approveBuilderFee, withdraw, etc.) */ + /** Exchange client using user wallet - for EIP-712/user-signed actions such as approvals, transfers, and withdrawals. */ user: ExchangeClient | null; } diff --git a/packages/hl-react/src/registries/exchange.ts b/packages/hl-react/src/registries/exchange.ts index df95a861..3de9de2f 100644 --- a/packages/hl-react/src/registries/exchange.ts +++ b/packages/hl-react/src/registries/exchange.ts @@ -11,15 +11,24 @@ interface ExchangeMethodConfig { const USER_SIGNED_METHODS: ReadonlySet = new Set([ "approveAgent", "approveBuilderFee", + "cDeposit", + "cWithdraw", + "convertToMultiSigUser", + "linkStakingUser", "sendAsset", + "sendToEvmWithData", "spotSend", "tokenDelegate", "usdClassTransfer", "usdSend", "userDexAbstraction", + "userPortfolioMargin", + "userSetAbstraction", "withdraw3", ]); +// All other ExchangeClient methods are L1 actions routed to the trading client. +// They call executeL1Action in the SDK, not executeUserSignedAction. const BUILDER_INJECTED_METHODS: ReadonlySet = new Set(["order"]); const CLIENT_KEY_METHODS: ReadonlySet = new Set(["order", "cancel"]);