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

nktkas / hyperliquid / 22525620077

28 Feb 2026 05:36PM UTC coverage: 94.608% (+2.0%) from 92.652%
22525620077

push

github

nktkas
test(exchange/topUpIsolatedOnlyMargin): update asset

606 of 778 branches covered (77.89%)

Branch coverage included in aggregate %.

7640 of 7938 relevant lines covered (96.25%)

724.64 hits per line

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

88.43
/src/api/exchange/_methods/_base/execute.ts
1
/**
2
 * Execute helpers for L1 and user-signed Exchange API actions.
3
 * @module
4
 */
5

6
import * as v from "@valibot/valibot";
157✔
7
import { parse } from "../../../../_base.ts";
157✔
8
import {
157✔
9
  type AbstractWallet,
10
  getWalletAddress,
157✔
11
  getWalletChainId,
157✔
12
  signL1Action,
157✔
13
  signMultiSigAction,
157✔
14
  signUserSignedAction,
157✔
15
} from "../../../../signing/mod.ts";
157✔
16
import type { IRequestTransport } from "../../../../transport/mod.ts";
17
import { Address, Hex, UnsignedInteger } from "../../../_schemas.ts";
157✔
18
import { globalNonceManager } from "./_nonce.ts";
157✔
19
import { withLock } from "./_semaphore.ts";
157✔
20
import type { SignatureSchema } from "./commonSchemas.ts";
21
import { assertSuccessResponse } from "./errors.ts";
157✔
22

23
// ============================================================
24
// Type Utilities
25
// ============================================================
26

27
type MaybePromise<T> = T | Promise<T>;
28

29
// deno-lint-ignore ban-types
30
type Prettify<T> = { [K in keyof T]: T[K] } & {};
31

32
/** Options for any execute functions. */
33
interface BaseOptions {
34
  /** {@link https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal | AbortSignal} to cancel a request. */
35
  signal?: AbortSignal;
36
}
37

38
/** Extract request options from a request type (excludes action, nonce, signature). */
39
export type ExtractRequestOptions<T extends { action: Record<string, unknown> }> = Prettify<
40
  & BaseOptions
41
  & Omit<T, "action" | "nonce" | "signature">
42
>;
43

44
// ============================================================
45
// Config
46
// ============================================================
47

48
/** Base configuration shared by single-wallet and multi-sig configs. */
49
interface BaseConfig<T extends IRequestTransport = IRequestTransport> {
50
  /** The transport used to connect to the Hyperliquid Exchange API. */
51
  transport: T;
52

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

56
  /**
57
   * Default vault address for vault-based operations, used when not specified in action options.
58
   * @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#subaccounts-and-vaults
59
   */
60
  defaultVaultAddress?: `0x${string}`;
61

62
  /**
63
   * Default expiration time in milliseconds, used when not specified in action options.
64
   * @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#expires-after
65
   */
66
  defaultExpiresAfter?: number | (() => MaybePromise<number>);
67

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

76
/** Configuration for single-wallet Exchange API requests. */
77
export interface ExchangeSingleWalletConfig<T extends IRequestTransport = IRequestTransport> extends BaseConfig<T> {
78
  /** The wallet used to sign requests. */
79
  wallet: AbstractWallet;
80
}
81

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

90
/** Union type for all Exchange API configurations. */
91
export type ExchangeConfig = ExchangeSingleWalletConfig | ExchangeMultiSigConfig;
92

93
// ============================================================
94
// Execute L1 Action
95
// ============================================================
96

97
/**
98
 * Execute an L1 action on the Hyperliquid Exchange.
99
 *
100
 * Handles both single-wallet and multi-sig signing.
101
 *
102
 * @param config Exchange API configuration
103
 * @param action Action payload to execute
104
 * @param options Additional options for the request
105
 * @return API response
106
 *
107
 * @throws {ApiRequestError} If the API returns an error response
108
 */
109
export async function executeL1Action<T>(
157✔
110
  config: ExchangeConfig,
157✔
111
  action: Record<string, unknown>,
157✔
112
  options?: {
157✔
113
    vaultAddress?: string;
114
    expiresAfter?: string | number;
115
    signal?: AbortSignal;
116
  },
157✔
117
): Promise<T> {
118
  const { transport } = config;
416✔
119
  const leader = getLeader(config);
416✔
120
  const walletAddress = await getWalletAddress(leader);
416✔
121

122
  // Semaphore ensures requests arrive at server in nonce order (prevents out-of-order delivery)
123
  const key = `${walletAddress}:${transport.isTestnet}`;
416✔
124
  return await withLock(key, async () => {
416✔
125
    const nonce = await (config.nonceManager?.(walletAddress) ?? globalNonceManager.getNonce(key));
×
126

127
    // Validate and resolve options
128
    const vaultAddress = parse(
675✔
129
      v.optional(Address),
675✔
130
      options?.vaultAddress ?? config.defaultVaultAddress,
675✔
131
    );
132
    const expiresAfter = parse(
×
133
      v.optional(UnsignedInteger),
×
134
      options?.expiresAfter ??
✔
135
        (typeof config.defaultExpiresAfter === "number"
×
136
          ? config.defaultExpiresAfter
×
137
          : await config.defaultExpiresAfter?.()),
×
138
    );
139
    const signal = options?.signal;
675✔
140

141
    // Sign action (multi-sig or single wallet)
142
    const [finalAction, signature] = "wallet" in config
675✔
143
      ? [
675✔
144
        action,
810✔
145
        await signL1Action({
810✔
146
          wallet: leader,
810✔
147
          action,
810✔
148
          nonce,
810✔
149
          isTestnet: transport.isTestnet,
810✔
150
          vaultAddress,
810✔
151
          expiresAfter,
810✔
152
        }),
810✔
153
      ]
675✔
154
      : await signMultiSigL1(config, action, walletAddress, nonce, vaultAddress, expiresAfter);
675✔
155

156
    // Send request and validate response
157
    const response = await transport.request("exchange", {
799✔
158
      action: finalAction,
799✔
159
      signature,
799✔
160
      nonce,
799✔
161
      vaultAddress,
799✔
162
      expiresAfter,
799✔
163
    }, signal);
799✔
164
    assertSuccessResponse(response);
675✔
165
    return response as T;
675✔
166
  });
416✔
167
}
416✔
168

169
// ============================================================
170
// Execute User-Signed Action
171
// ============================================================
172

173
/** Extract nonce field name from EIP-712 types ("nonce" or "time"). */
174
function getNonceFieldName(types: Record<string, { name: string; type: string }[]>): "nonce" | "time" {
157✔
175
  const primaryType = Object.keys(types)[0];
527✔
176
  const field = types[primaryType].find((f) => f.name === "nonce" || f.name === "time") as {
527✔
177
    name: "nonce" | "time";
178
    type: string;
179
  } | undefined;
180
  return field?.name ?? "nonce";
×
181
}
527✔
182

183
/**
184
 * Execute a user-signed action (EIP-712) on the Hyperliquid Exchange.
185
 *
186
 * Handles both single-wallet and multi-sig signing.
187
 * Automatically adds signatureChainId, hyperliquidChain, and nonce/time.
188
 *
189
 * @param config Exchange API configuration
190
 * @param action Action payload to execute
191
 * @param types EIP-712 type definitions for signing
192
 * @param options Additional options for the request
193
 * @return API response
194
 *
195
 * @throws {ApiRequestError} If the API returns an error response
196
 */
197
export async function executeUserSignedAction<T>(
157✔
198
  config: ExchangeConfig,
157✔
199
  action: Record<string, unknown>,
157✔
200
  types: Record<string, { name: string; type: string }[]>,
157✔
201
  options?: {
157✔
202
    signal?: AbortSignal;
203
  },
157✔
204
): Promise<T> {
205
  const { transport } = config;
527✔
206
  const leader = getLeader(config);
527✔
207
  const walletAddress = await getWalletAddress(leader);
527✔
208

209
  // Semaphore ensures requests arrive at server in nonce order (prevents out-of-order delivery)
210
  const key = `${walletAddress}:${transport.isTestnet}`;
527✔
211
  return withLock(key, async () => {
527✔
212
    const nonce = await (config.nonceManager?.(walletAddress) ?? globalNonceManager.getNonce(key));
×
213
    const signal = options?.signal;
×
214

215
    // Add system fields for user-signed actions
216
    const { type, ...restAction } = action;
897✔
217
    const nonceFieldName = getNonceFieldName(types);
897✔
218
    const fullAction = { // Key order is important for multi-sig
897✔
219
      type,
897✔
220
      signatureChainId: await getSignatureChainId(config),
897✔
221
      hyperliquidChain: transport.isTestnet ? "Testnet" : "Mainnet",
×
222
      ...restAction,
897✔
223
      [nonceFieldName]: nonce,
897✔
224
    };
897✔
225

226
    // Sign action (multi-sig or single wallet)
227
    const [finalAction, signature] = "wallet" in config
897✔
228
      ? [fullAction, await signUserSignedAction({ wallet: leader, action: fullAction, types })]
8,952✔
229
      : await signMultiSigUserSigned(config, fullAction, types, walletAddress, nonce);
897✔
230

231
    // Send request and validate response
232
    const response = await transport.request("exchange", {
971✔
233
      action: finalAction,
971✔
234
      signature,
971✔
235
      nonce,
971✔
236
    }, signal);
971✔
237
    assertSuccessResponse(response);
897✔
238
    return response as T;
897✔
239
  });
527✔
240
}
527✔
241

242
// ============================================================
243
// Multi-sig signing
244
// ============================================================
245

246
/** Remove leading zeros from signature components (required by Hyperliquid). */
247
function trimSignature(sig: SignatureSchema): SignatureSchema {
157✔
248
  return {
355✔
249
    r: sig.r.replace(/^0x0+/, "0x") as `0x${string}`,
355✔
250
    s: sig.s.replace(/^0x0+/, "0x") as `0x${string}`,
355✔
251
    v: sig.v,
355✔
252
  };
355✔
253
}
355✔
254

255
/** Sign an L1 action with multi-sig. */
256
async function signMultiSigL1(
157✔
257
  config: ExchangeMultiSigConfig,
157✔
258
  action: Record<string, unknown>,
157✔
259
  outerSigner: `0x${string}`,
157✔
260
  nonce: number,
157✔
261
  vaultAddress?: `0x${string}`,
157✔
262
  expiresAfter?: number,
157✔
263
): Promise<[Record<string, unknown>, SignatureSchema]> {
264
  const { transport: { isTestnet }, signers, multiSigUser } = config;
281✔
265
  const multiSigUser_ = parse(Address, multiSigUser);
281✔
266
  const outerSigner_ = parse(Address, outerSigner);
281✔
267

268
  // Collect signatures from all signers
269
  const signatures = await Promise.all(signers.map(async (signer) => {
281✔
270
    const signature = await signL1Action({
405✔
271
      wallet: signer,
405✔
272
      action: [multiSigUser_, outerSigner_, action],
2,025✔
273
      nonce,
405✔
274
      isTestnet,
405✔
275
      vaultAddress,
405✔
276
      expiresAfter,
405✔
277
    });
405✔
278
    return trimSignature(signature);
405✔
279
  }));
281✔
280

281
  // Build multi-sig action wrapper
282
  const multiSigAction = {
281✔
283
    type: "multiSig",
281✔
284
    signatureChainId: await getSignatureChainId(config),
281✔
285
    signatures,
281✔
286
    payload: {
281✔
287
      multiSigUser: multiSigUser_,
281✔
288
      outerSigner: outerSigner_,
281✔
289
      action,
281✔
290
    },
281✔
291
  };
281✔
292

293
  // Sign the wrapper with the leader
294
  const signature = await signMultiSigAction({
281✔
295
    wallet: signers[0],
281✔
296
    action: multiSigAction,
281✔
297
    nonce,
281✔
298
    isTestnet,
281✔
299
    vaultAddress,
281✔
300
    expiresAfter,
281✔
301
  });
281✔
302

303
  return [multiSigAction, signature];
1,124✔
304
}
281✔
305

306
/** Sign a user-signed action (EIP-712) with multi-sig. */
307
async function signMultiSigUserSigned(
157✔
308
  config: ExchangeMultiSigConfig,
157✔
309
  action: Record<string, unknown> & { signatureChainId: `0x${string}` },
157✔
310
  types: Record<string, { name: string; type: string }[]>,
157✔
311
  outerSigner: `0x${string}`,
157✔
312
  nonce: number,
157✔
313
): Promise<[Record<string, unknown>, SignatureSchema]> {
314
  const { signers, multiSigUser, transport: { isTestnet } } = config;
231✔
315
  const multiSigUser_ = parse(Address, multiSigUser);
231✔
316
  const outerSigner_ = parse(Address, outerSigner);
231✔
317

318
  // Collect signatures from all signers
319
  const signatures = await Promise.all(signers.map(async (signer) => {
231✔
320
    const signature = await signUserSignedAction({
305✔
321
      wallet: signer,
305✔
322
      action: {
305✔
323
        payloadMultiSigUser: multiSigUser_,
305✔
324
        outerSigner: outerSigner_,
305✔
325
        ...action,
305✔
326
      },
305✔
327
      types,
305✔
328
    });
305✔
329
    return trimSignature(signature);
305✔
330
  }));
231✔
331

332
  // Build multi-sig action wrapper
333
  const multiSigAction = {
231✔
334
    type: "multiSig",
231✔
335
    signatureChainId: await getSignatureChainId(config),
231✔
336
    signatures,
231✔
337
    payload: {
231✔
338
      multiSigUser: multiSigUser_,
231✔
339
      outerSigner: outerSigner_,
231✔
340
      action,
231✔
341
    },
231✔
342
  };
231✔
343

344
  // Sign the wrapper with the leader
345
  const signature = await signMultiSigAction({
231✔
346
    wallet: signers[0],
231✔
347
    action: multiSigAction,
231✔
348
    nonce,
231✔
349
    isTestnet,
231✔
350
  });
231✔
351

352
  return [multiSigAction, signature];
924✔
353
}
231✔
354

355
// ============================================================
356
// Helpers
357
// ============================================================
358

359
/** Get the leader wallet (first signer for the single wallet, or multi-sig). */
360
function getLeader(config: ExchangeConfig): AbstractWallet {
157✔
361
  return "wallet" in config ? config.wallet : config.signers[0];
1,354✔
362
}
1,354✔
363

364
/** Resolve signature chain ID from config or wallet. */
365
async function getSignatureChainId(config: ExchangeConfig): Promise<`0x${string}`> {
157✔
366
  if (config.signatureChainId) {
×
367
    const id = typeof config.signatureChainId === "function"
×
368
      ? await config.signatureChainId()
×
369
      : config.signatureChainId;
×
370
    return parse(Hex, id);
×
371
  }
×
372
  return getWalletChainId(getLeader(config));
725✔
373
}
725✔
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