Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions apps/terminal/src/lib/tests/hl-react-exports.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, PackageJsonExportTarget>;
}

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",
});
});
});
7 changes: 7 additions & 0 deletions apps/terminal/src/lib/tests/hyperliquid-registries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
];

Expand Down
40 changes: 37 additions & 3 deletions packages/hl-react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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",
Expand All @@ -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";
```
Expand Down
5 changes: 5 additions & 0 deletions packages/hl-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/hl-react/src/hooks/useClients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
9 changes: 9 additions & 0 deletions packages/hl-react/src/registries/exchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,24 @@ interface ExchangeMethodConfig {
const USER_SIGNED_METHODS: ReadonlySet<string> = 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<string> = new Set(["order"]);

const CLIENT_KEY_METHODS: ReadonlySet<string> = new Set(["order", "cancel"]);
Expand Down
Loading