• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nktkas / hyperliquid / 20260168111

16 Dec 2025 07:33AM UTC coverage: 95.34% (-1.3%) from 96.688%
20260168111

push

github

nktkas
docs: improve code documentation

661 of 881 branches covered (75.03%)

Branch coverage included in aggregate %.

5 of 5 new or added lines in 2 files covered. (100.0%)

151 existing lines in 17 files now uncovered.

12843 of 13283 relevant lines covered (96.69%)

1103.6 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.35
/src/api/exchange/_methods/_base/execute.ts
1
import * as v from "@valibot/valibot";
364✔
2
import type { IRequestTransport } from "../../../../transport/mod.ts";
3
import { Address, Hex, UnsignedInteger } from "../../../_schemas.ts";
364✔
4
import {
364✔
5
  type AbstractWallet,
6
  getWalletAddress,
364✔
7
  getWalletChainId,
364✔
8
  signL1Action,
364✔
9
  signMultiSigAction,
364✔
10
  signUserSignedAction,
364✔
11
} from "../../../../signing/mod.ts";
364✔
12
import { assertSuccessResponse } from "./errors.ts";
364✔
13
import { defaultNonceManager } from "./_nonce.ts";
364✔
14
import { withLock } from "./_semaphore.ts";
364✔
15
import type { SignatureSchema } from "./commonSchemas.ts";
16

17
// =============================================================
18
// Type Utilities
19
// =============================================================
20

21
type MaybePromise<T> = T | Promise<T>;
22

23
// deno-lint-ignore ban-types
24
type Prettify<T> = { [K in keyof T]: T[K] } & {};
25

26
/** Options for any execute functions. */
27
interface BaseOptions {
28
  /** {@link https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal | AbortSignal} to cancel a request. */
29
  signal?: AbortSignal;
30
}
31

32
/** Extract request options from a request type (excludes action, nonce, signature). */
33
export type ExtractRequestOptions<T extends { action: Record<string, unknown> }> = Prettify<
34
  & BaseOptions
35
  & Omit<T, "action" | "nonce" | "signature">
36
>;
37

38
// =============================================================
39
// Config
40
// =============================================================
41

42
/** Base configuration shared by single-wallet and multi-sig configs. */
43
interface BaseConfig<T extends IRequestTransport = IRequestTransport> {
44
  /** The transport used to connect to the Hyperliquid Exchange API. */
45
  transport: T;
46

47
  /** Signature chain ID for EIP-712 signing, defaults to wallet's chain ID. */
48
  signatureChainId?: `0x${string}` | (() => MaybePromise<`0x${string}`>);
49

50
  /**
51
   * Default vault address for vault-based operations, used when not specified in action options.
52
   * @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#subaccounts-and-vaults
53
   */
54
  defaultVaultAddress?: `0x${string}`;
55

56
  /**
57
   * Default expiration time in milliseconds, used when not specified in action options.
58
   * @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#expires-after
59
   */
60
  defaultExpiresAfter?: number | (() => MaybePromise<number>);
61

62
  /**
63
   * Custom nonce generator function.
64
   * Defaults to a global manager using timestamp with auto-increment.
65
   * @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/nonces-and-api-wallets#hyperliquid-nonces
66
   */
67
  nonceManager?: (address: string) => MaybePromise<number>;
68
}
69

70
/** Configuration for single-wallet Exchange API requests. */
71
export interface ExchangeSingleWalletConfig<T extends IRequestTransport = IRequestTransport> extends BaseConfig<T> {
72
  /** The wallet used to sign requests. */
73
  wallet: AbstractWallet;
74
}
75

76
/** Configuration for multi-signature Exchange API requests. */
77
export interface ExchangeMultiSigConfig<T extends IRequestTransport = IRequestTransport> extends BaseConfig<T> {
78
  /** Array of wallets for multi-sig. First wallet is the leader. */
79
  signers: readonly [AbstractWallet, ...AbstractWallet[]];
80
  /** The multi-signature account address. */
81
  multiSigUser: `0x${string}`;
82
}
83

84
/** Union type for all Exchange API configurations. */
85
export type ExchangeConfig = ExchangeSingleWalletConfig | ExchangeMultiSigConfig;
86

87
// =============================================================
88
// Execute L1 Action
89
// =============================================================
90

91
/**
92
 * Execute an L1 action on the Hyperliquid Exchange.
93
 * Handles both single-wallet and multi-sig signing.
94
 */
95
export async function executeL1Action<T>(
364✔
96
  config: ExchangeConfig,
364✔
97
  action: Record<string, unknown>,
364✔
98
  options?: {
364✔
99
    vaultAddress?: string;
100
    expiresAfter?: string | number;
101
    signal?: AbortSignal;
102
  },
364✔
103
): Promise<T> {
104
  const { transport } = config;
561✔
105
  const leader = getLeader(config);
561✔
106
  const walletAddress = await getWalletAddress(leader);
561✔
107

108
  // Semaphore ensures requests arrive at server in nonce order (prevents out-of-order delivery)
109
  const key = `${walletAddress}:${transport.isTestnet}`;
561✔
110
  return await withLock(key, async () => {
561✔
UNCOV
111
    const nonce = await (config.nonceManager?.(walletAddress) ?? defaultNonceManager.getNonce(key));
×
112

113
    // Validate and resolve options
114
    const vaultAddress = v.parse(
758✔
115
      v.optional(Address),
758✔
116
      options?.vaultAddress ?? config.defaultVaultAddress,
758✔
117
    );
118
    const expiresAfter = v.parse(
×
119
      v.optional(UnsignedInteger),
×
120
      options?.expiresAfter ??
✔
121
        (typeof config.defaultExpiresAfter === "number"
×
122
          ? config.defaultExpiresAfter
×
UNCOV
123
          : await config.defaultExpiresAfter?.()),
×
124
    );
125
    const signal = options?.signal;
758✔
126

127
    // Sign action (multi-sig or single wallet)
128
    const [finalAction, signature] = "wallet" in config
758✔
129
      ? [
758✔
130
        action,
878✔
131
        await signL1Action({
878✔
132
          wallet: leader,
878✔
133
          action,
878✔
134
          nonce,
878✔
135
          isTestnet: transport.isTestnet,
878✔
136
          vaultAddress,
878✔
137
          expiresAfter,
878✔
138
        }),
878✔
139
      ]
758✔
140
      : await signMultiSigL1(config, action, walletAddress, nonce, vaultAddress, expiresAfter);
758✔
141

142
    // Send request and validate response
143
    const response = await transport.request("exchange", {
835✔
144
      action: finalAction,
835✔
145
      signature,
835✔
146
      nonce,
835✔
147
      vaultAddress,
835✔
148
      expiresAfter,
835✔
149
    }, signal);
835✔
150
    assertSuccessResponse(response);
758✔
151
    return response as T;
758✔
152
  });
561✔
153
}
561✔
154

155
// =============================================================
156
// Execute User-Signed Action
157
// =============================================================
158

159
/** Extract nonce field name from EIP-712 types ("nonce" or "time"). */
160
function getNonceFieldName(types: Record<string, { name: string; type: string }[]>): "nonce" | "time" {
364✔
161
  const primaryType = Object.keys(types)[0];
672✔
162
  const field = types[primaryType].find((f) => f.name === "nonce" || f.name === "time") as {
672✔
163
    name: "nonce" | "time";
164
    type: string;
165
  } | undefined;
UNCOV
166
  return field?.name ?? "nonce";
×
167
}
672✔
168

169
/**
170
 * Execute a user-signed action (EIP-712) on the Hyperliquid Exchange.
171
 * Handles both single-wallet and multi-sig signing.
172
 * Automatically adds signatureChainId, hyperliquidChain, and nonce/time.
173
 */
174
export async function executeUserSignedAction<T>(
364✔
175
  config: ExchangeConfig,
364✔
176
  action: Record<string, unknown>,
364✔
177
  types: Record<string, { name: string; type: string }[]>,
364✔
178
  options?: {
364✔
179
    signal?: AbortSignal;
180
  },
364✔
181
): Promise<T> {
182
  const { transport } = config;
672✔
183
  const leader = getLeader(config);
672✔
184
  const walletAddress = await getWalletAddress(leader);
672✔
185

186
  // Semaphore ensures requests arrive at server in nonce order (prevents out-of-order delivery)
187
  const key = `${walletAddress}:${transport.isTestnet}`;
672✔
188
  return withLock(key, async () => {
672✔
189
    const nonce = await (config.nonceManager?.(walletAddress) ?? defaultNonceManager.getNonce(key));
×
UNCOV
190
    const signal = options?.signal;
×
191

192
    // Add system fields for user-signed actions
193
    const { type, ...restAction } = action;
980✔
194
    const nonceFieldName = getNonceFieldName(types);
980✔
195
    const fullAction = { // key order is important for multi-sig
980✔
196
      type,
980✔
197
      signatureChainId: await getSignatureChainId(config),
980✔
198
      hyperliquidChain: transport.isTestnet ? "Testnet" : "Mainnet",
980✔
199
      ...restAction,
980✔
200
      [nonceFieldName]: nonce,
980✔
201
    };
980✔
202

203
    // Sign action (multi-sig or single wallet)
204
    const [finalAction, signature] = "wallet" in config
980✔
205
      ? [fullAction, await signUserSignedAction({ wallet: leader, action: fullAction, types })]
9,322✔
206
      : await signMultiSigUserSigned(config, fullAction, types, walletAddress, nonce);
980✔
207

208
    // Send request and validate response
209
    const response = await transport.request("exchange", {
1,041✔
210
      action: finalAction,
1,041✔
211
      signature,
1,041✔
212
      nonce,
1,041✔
213
    }, signal);
1,041✔
214
    assertSuccessResponse(response);
980✔
215
    return response as T;
980✔
216
  });
672✔
217
}
672✔
218

219
// =============================================================
220
// Multi-sig signing
221
// =============================================================
222

223
/** Remove leading zeros from signature components (required by Hyperliquid). */
224
function trimSignature(sig: SignatureSchema): SignatureSchema {
364✔
225
  return {
502✔
226
    r: sig.r.replace(/^0x0+/, "0x") as `0x${string}`,
502✔
227
    s: sig.s.replace(/^0x0+/, "0x") as `0x${string}`,
502✔
228
    v: sig.v,
502✔
229
  };
502✔
230
}
502✔
231

232
/** Sign an L1 action with multi-sig. */
233
async function signMultiSigL1(
364✔
234
  config: ExchangeMultiSigConfig,
364✔
235
  action: Record<string, unknown>,
364✔
236
  outerSigner: `0x${string}`,
364✔
237
  nonce: number,
364✔
238
  vaultAddress?: `0x${string}`,
364✔
239
  expiresAfter?: number,
364✔
240
): Promise<[Record<string, unknown>, SignatureSchema]> {
241
  const { transport: { isTestnet }, signers, multiSigUser } = config;
441✔
242
  const multiSigUser_ = v.parse(Address, multiSigUser);
441✔
243
  const outerSigner_ = v.parse(Address, outerSigner);
441✔
244

245
  // Collect signatures from all signers
246
  const signatures = await Promise.all(signers.map(async (signer) => {
441✔
247
    const signature = await signL1Action({
518✔
248
      wallet: signer,
518✔
249
      action: [multiSigUser_, outerSigner_, action],
2,590✔
250
      nonce,
518✔
251
      isTestnet,
518✔
252
      vaultAddress,
518✔
253
      expiresAfter,
518✔
254
    });
518✔
255
    return trimSignature(signature);
518✔
256
  }));
441✔
257

258
  // Build multi-sig action wrapper
259
  const multiSigAction = {
441✔
260
    type: "multiSig",
441✔
261
    signatureChainId: await getSignatureChainId(config),
441✔
262
    signatures,
441✔
263
    payload: {
441✔
264
      multiSigUser: multiSigUser_,
441✔
265
      outerSigner: outerSigner_,
441✔
266
      action,
441✔
267
    },
441✔
268
  };
441✔
269

270
  // Sign the wrapper with the leader
271
  const signature = await signMultiSigAction({
441✔
272
    wallet: signers[0],
441✔
273
    action: multiSigAction,
441✔
274
    nonce,
441✔
275
    isTestnet,
441✔
276
    vaultAddress,
441✔
277
    expiresAfter,
441✔
278
  });
441✔
279

280
  return [multiSigAction, signature];
1,764✔
281
}
441✔
282

283
/** Sign a user-signed action (EIP-712) with multi-sig. */
284
async function signMultiSigUserSigned(
364✔
285
  config: ExchangeMultiSigConfig,
364✔
286
  action: Record<string, unknown> & { signatureChainId: `0x${string}` },
364✔
287
  types: Record<string, { name: string; type: string }[]>,
364✔
288
  outerSigner: `0x${string}`,
364✔
289
  nonce: number,
364✔
290
): Promise<[Record<string, unknown>, SignatureSchema]> {
291
  const { signers, multiSigUser, transport: { isTestnet } } = config;
425✔
292
  const multiSigUser_ = v.parse(Address, multiSigUser);
425✔
293
  const outerSigner_ = v.parse(Address, outerSigner);
425✔
294

295
  // Collect signatures from all signers
296
  const signatures = await Promise.all(signers.map(async (signer) => {
425✔
297
    const signature = await signUserSignedAction({
486✔
298
      wallet: signer,
486✔
299
      action: {
486✔
300
        payloadMultiSigUser: multiSigUser_,
486✔
301
        outerSigner: outerSigner_,
486✔
302
        ...action,
486✔
303
      },
486✔
304
      types,
486✔
305
    });
486✔
306
    return trimSignature(signature);
486✔
307
  }));
425✔
308

309
  // Build multi-sig action wrapper
310
  const multiSigAction = {
425✔
311
    type: "multiSig",
425✔
312
    signatureChainId: await getSignatureChainId(config),
425✔
313
    signatures,
425✔
314
    payload: {
425✔
315
      multiSigUser: multiSigUser_,
425✔
316
      outerSigner: outerSigner_,
425✔
317
      action,
425✔
318
    },
425✔
319
  };
425✔
320

321
  // Sign the wrapper with the leader
322
  const signature = await signMultiSigAction({
425✔
323
    wallet: signers[0],
425✔
324
    action: multiSigAction,
425✔
325
    nonce,
425✔
326
    isTestnet,
425✔
327
  });
425✔
328

329
  return [multiSigAction, signature];
1,700✔
330
}
425✔
331

332
// =============================================================
333
// Helpers
334
// =============================================================
335

336
/** Get the leader wallet (first signer for the single wallet, or multi-sig). */
337
function getLeader(config: ExchangeConfig): AbstractWallet {
364✔
338
  return "wallet" in config ? config.wallet : config.signers[0];
1,315✔
339
}
1,315✔
340

341
/** Resolve signature chain ID from config or wallet. */
342
async function getSignatureChainId(config: ExchangeConfig): Promise<`0x${string}`> {
364✔
UNCOV
343
  if (config.signatureChainId) {
×
UNCOV
344
    const id = typeof config.signatureChainId === "function"
×
UNCOV
345
      ? await config.signatureChainId()
×
UNCOV
346
      : config.signatureChainId;
×
UNCOV
347
    return v.parse(Hex, id);
×
UNCOV
348
  }
×
349
  return getWalletChainId(getLeader(config));
810✔
350
}
810✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc