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
12 changes: 10 additions & 2 deletions apps/docs/content/docs/clients.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,12 +341,20 @@ await client.order({
});

await client.cancel({ cancels: [{ coin: "BTC", o: 12345 }] });
await client.trailingStop({
coin: "BTC",
isBuy: false,
sz: "0.1",
reduceOnly: true,
retracement: { pct: "1.5%" }, // or { px: "30000" } for an absolute retracement
activationPx: null,
});
await client.updateLeverage({ coin: "BTC", isCross: true, leverage: 5 });
```

This works on `order`, `modify`, `batchModify`, `cancel`, `cancelByCloid`, `twapOrder`, `twapCancel`,
`updateLeverage`, `updateIsolatedMargin`, and `topUpIsolatedOnlyMargin`. Entries without a `coin` are dispatched
unchanged — no metadata is fetched unless a call actually uses a symbol.
`trailingStop`, `updateLeverage`, `updateIsolatedMargin`, and `topUpIsolatedOnlyMargin`. Entries without a `coin`
are dispatched unchanged — no metadata is fetched unless a call actually uses a symbol.

The symbol metadata comes from the client's `symbolConverter` config. When omitted, the client lazily creates and
loads a `SymbolConverter` over the same transport on the first symbol-based call; pass a preloaded instance to enable
Expand Down
2 changes: 2 additions & 0 deletions packages/hyperliquid/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@
"./api/exchange/submitPrepared": "./src/api/exchange/_methods/submitPrepared.ts",
"./api/exchange/tokenDelegate": "./src/api/exchange/_methods/tokenDelegate.ts",
"./api/exchange/topUpIsolatedOnlyMargin": "./src/api/exchange/_methods/topUpIsolatedOnlyMargin.ts",
"./api/exchange/trailingStop": "./src/api/exchange/_methods/trailingStop.ts",
"./api/exchange/twapCancel": "./src/api/exchange/_methods/twapCancel.ts",
"./api/exchange/twapOrder": "./src/api/exchange/_methods/twapOrder.ts",
"./api/exchange/updateIsolatedMargin": "./src/api/exchange/_methods/updateIsolatedMargin.ts",
Expand Down Expand Up @@ -289,6 +290,7 @@
"./actions/subAccountTransfer": "./src/actions/subAccountTransfer.ts",
"./actions/tokenDelegate": "./src/actions/tokenDelegate.ts",
"./actions/topUpIsolatedOnlyMargin": "./src/actions/topUpIsolatedOnlyMargin.ts",
"./actions/trailingStop": "./src/actions/trailingStop.ts",
"./actions/twapCancel": "./src/actions/twapCancel.ts",
"./actions/twapOrder": "./src/actions/twapOrder.ts",
"./actions/updateIsolatedMargin": "./src/actions/updateIsolatedMargin.ts",
Expand Down
1 change: 1 addition & 0 deletions packages/hyperliquid/src/actions/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export * from "./subAccountSpotTransfer.ts";
export * from "./subAccountTransfer.ts";
export * from "./tokenDelegate.ts";
export * from "./topUpIsolatedOnlyMargin.ts";
export * from "./trailingStop.ts";
export * from "./twapCancel.ts";
export * from "./twapOrder.ts";
export * from "./updateIsolatedMargin.ts";
Expand Down
7 changes: 7 additions & 0 deletions packages/hyperliquid/src/actions/trailingStop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/** Canonical trailingStop action builder. @module */
export { buildTrailingStop } from "../api/exchange/_methods/trailingStop.ts";
export type {
TrailingStopOptions,
TrailingStopSuccessResponse,
TrailingStopParameters,
} from "../api/exchange/_methods/trailingStop.ts";
10 changes: 10 additions & 0 deletions packages/hyperliquid/src/api/_errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ export interface ApiSingleErrorResponse {
};
}

/** Data-level error response where `response.data` itself carries the error (e.g. `trailingStop`). */
export interface ApiDataErrorResponse {
/** Response details. */
response: {
/** Specific data carrying the error message. */
data: { error: string };
};
}

/** Explorer endpoint error envelope (`{ type: "error", message }`). */
export interface ApiExplorerErrorResponse {
/** Envelope type. */
Expand All @@ -62,6 +71,7 @@ export type ApiErrorResponse =
| ApiTopLevelErrorResponse
| ApiBulkErrorResponse
| ApiSingleErrorResponse
| ApiDataErrorResponse
| ApiExplorerErrorResponse;

// ============================================================
Expand Down
33 changes: 28 additions & 5 deletions packages/hyperliquid/src/api/exchange/_methods/_base/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import {
type ApiBulkErrorResponse,
type ApiDataErrorResponse,
ApiRequestError,
type ApiSingleErrorResponse,
type ApiTopLevelErrorResponse,
Expand All @@ -13,6 +14,7 @@ import {
export { ApiRequestError };
export type {
ApiBulkErrorResponse,
ApiDataErrorResponse,
ApiErrorResponse,
ApiExplorerErrorResponse,
ApiSingleErrorResponse,
Expand Down Expand Up @@ -49,11 +51,18 @@ function isSingleError(r: unknown): r is ApiSingleErrorResponse {
return hasErrorField(status);
}

/** True if `r` matches any of the three Hyperliquid error response shapes. */
/** True if `r` matches `{ response: { data: { error } } }` (e.g. `trailingStop`). */
function isDataError(r: unknown): r is ApiDataErrorResponse {
if (typeof r !== "object" || r === null) return false;
const data = (r as { response?: { data?: unknown } }).response?.data;
return hasErrorField(data);
}

/** True if `r` matches any of the four Hyperliquid error response shapes. */
export function isErrorResponse(
r: unknown,
): r is ApiTopLevelErrorResponse | ApiBulkErrorResponse | ApiSingleErrorResponse {
return isTopLevelError(r) || isBulkError(r) || isSingleError(r);
): r is ApiTopLevelErrorResponse | ApiBulkErrorResponse | ApiSingleErrorResponse | ApiDataErrorResponse {
return isTopLevelError(r) || isBulkError(r) || isSingleError(r) || isDataError(r);
}

// ============================================================
Expand All @@ -73,6 +82,9 @@ function getErrorMessage(r: unknown): string | undefined {
if (isSingleError(r)) {
return r.response.data.status.error;
}
if (isDataError(r)) {
return r.response.data.error;
}
return undefined;
}

Expand Down Expand Up @@ -128,5 +140,16 @@ type ExcludeSingleError<T> = T extends { response: { data: { status: infer S } }
>
: T;

/** Exclude all three Hyperliquid error response shapes from `T`. */
export type ExcludeErrorResponse<T> = ExcludeTopLevelError<ExcludeBulkError<ExcludeSingleError<T>>>;
/** Filter out error variant from `response.data` itself (e.g. `trailingStop`). */
type ExcludeDataError<T> = T extends { response: { data: infer D } }
? Exclude<D, { error: unknown }> extends never
? never
: Prettify<
Omit<T, "response"> & {
response: Prettify<Omit<T["response"], "data"> & { data: Exclude<D, { error: unknown }> }>;
}
>
: T;

/** Exclude all four Hyperliquid error response shapes from `T`. */
export type ExcludeErrorResponse<T> = ExcludeTopLevelError<ExcludeBulkError<ExcludeSingleError<ExcludeDataError<T>>>>;
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type {
export { buildAction } from "./_action.ts";
export {
type ApiBulkErrorResponse,
type ApiDataErrorResponse,
type ApiErrorResponse,
type ApiExplorerErrorResponse,
ApiRequestError,
Expand Down
168 changes: 168 additions & 0 deletions packages/hyperliquid/src/api/exchange/_methods/trailingStop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts";
import * as v from "valibot";

// ============================================================
// API Schemas
// ============================================================

import { Address, Hex, Percent, UnsignedDecimal, UnsignedInteger } from "../../_schemas.ts";

/**
* Place a trailing stop order.
* @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#place-a-trailing-stop-order
*/
export const TrailingStopRequest = /* @__PURE__ */ (() => {
return v.object({
/** Action to perform. */
action: v.object({
/** Type of action. */
type: v.literal("trailingStop"),
/** Asset ID. */
asset: UnsignedInteger,
/** Position side (`true` for long, `false` for short). */
isBuy: v.boolean(),
/** Size (in base currency units). */
sz: UnsignedDecimal,
/** Whether the order is reduce-only. */
reduceOnly: v.boolean(),
/** Retracement distance from the extreme price at which the stop triggers. */
retracement: v.union([
v.object({
/** Retracement as a percentage of the price (e.g., "1.234%"). */
pct: Percent,
}),
v.object({
/** Retracement as an absolute price distance. */
px: UnsignedDecimal,
}),
]),
/** Price at which the trailing stop activates (`null` to activate immediately). */
activationPx: v.nullable(UnsignedDecimal),
}),
/** Nonce (timestamp in ms) used to prevent replay attacks. */
nonce: UnsignedInteger,
/** ECDSA signature components. */
signature: v.object({
/** First 32-byte component. */
r: v.pipe(Hex, v.length(66)),
/** Second 32-byte component. */
s: v.pipe(Hex, v.length(66)),
/** Recovery identifier. */
v: v.picklist([27, 28]),
}),
/** Vault address (for vault trading). */
vaultAddress: v.optional(Address),
/** Expiration time of the action. */
expiresAfter: v.optional(UnsignedInteger),
});
})();
export type TrailingStopRequest = v.InferOutput<typeof TrailingStopRequest>;

/**
* Response for placing a trailing stop order.
* @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#place-a-trailing-stop-order
*/
export type TrailingStopResponse = {
/** Successful status. */
status: "ok";
/** Response details. */
response: {
/** Type of response. */
type: "trailingStop";
/** Specific data. */
data:
| {
/** Order ID of the placed trailing stop order. */
oid: number;
}
| {
/** Error message. */
error: string;
};
};
};

// ============================================================
// Execution Logic
// ============================================================

import {
type ExchangeConfig,
type ExcludeErrorResponse,
buildAction,
executeL1Action,
type ExtractRequestOptions,
} from "./_base/mod.ts";

/** Schema for action fields (excludes request-level system fields). */
const TrailingStopActionSchema = /* @__PURE__ */ (() => {
return v.object(TrailingStopRequest.entries.action.entries);
})();

/** Action parameters for the {@linkcode trailingStop} function. */
export type TrailingStopParameters = Omit<v.InferInput<typeof TrailingStopActionSchema>, "type">;

/** Request options for the {@linkcode trailingStop} function. */
export type TrailingStopOptions = ExtractRequestOptions<v.InferInput<typeof TrailingStopRequest>>;

/** Successful variant of {@linkcode TrailingStopResponse} without errors. */
export type TrailingStopSuccessResponse = ExcludeErrorResponse<TrailingStopResponse>;

/**
* Place a trailing stop order.
*
* Signing: L1 Action.
*
* @param config General configuration for Exchange API requests.
* @param params Parameters specific to the API request.
* @param opts Request execution options.
* @return Successful variant of {@link TrailingStopResponse} without error status.
*
* @throws {ValidationError} When the request parameters fail validation (before sending).
* @throws {TransportError} When the transport layer throws an error.
* @throws {ApiRequestError} When the API returns an unsuccessful response.
*
* @example
* ```ts
* import { HttpTransport } from "@bloxwap/hyperliquid";
* import { trailingStop } from "@bloxwap/hyperliquid/api/exchange";
* import { privateKeyToAccount } from "viem/accounts";
*
* const wallet = privateKeyToAccount("0x...");
* const transport = new HttpTransport(); // or `WebSocketTransport`
*
* const data = await trailingStop({ transport, wallet }, {
* asset: 0,
* isBuy: true,
* sz: "0.1",
* reduceOnly: false,
* retracement: { pct: "1.234%" },
* activationPx: "30000",
* });
* ```
*
* @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#place-a-trailing-stop-order
*/
export function trailingStop(
config: ExchangeConfig,
params: TrailingStopParameters,
opts?: TrailingStopOptions,
): Promise<TrailingStopSuccessResponse> {
const action = buildAction(TrailingStopActionSchema, { type: "trailingStop", ...params }, opts);
return executeL1Action(config, action, opts);
}

/**
* Build a canonical {@linkcode trailingStop} action: validate, normalize, fill defaults, and copy and freeze
* the result. Allocates no nonce and makes no wallet or transport call, so the action can be signed
* and submitted later, or reused while its fields stay valid.
*
* @param params Parameters specific to the API request.
* @return Immutable action typed with {@link TrailingStopSuccessResponse}.
*
* @throws {ValidationError} When the request parameters fail validation.
*/
export function buildTrailingStop(params: TrailingStopParameters): CanonicalAction<TrailingStopSuccessResponse> {
const action = buildAction(TrailingStopActionSchema, { type: "trailingStop", ...params });
return canonicalAction(action, { kind: "l1" });
}
Loading
Loading