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

nktkas / hyperliquid / 19514753888

19 Nov 2025 08:02PM UTC coverage: 94.811% (+0.1%) from 94.685%
19514753888

push

github

nktkas
ci: remove environment for test job

364 of 583 branches covered (62.44%)

Branch coverage included in aggregate %.

12208 of 12677 relevant lines covered (96.3%)

964.38 hits per line

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

90.24
/src/api/exchange/_base/_execute.ts
1
import { getSemaphore } from "@henrygd/semaphore";
357✔
2
import { Hex, type MaybePromise, parser } from "../../_base.ts";
357✔
3
import {
357✔
4
  type AbstractWallet,
5
  getWalletAddress,
357✔
6
  getWalletChainId,
357✔
7
  signL1Action,
357✔
8
  signMultiSigAction,
357✔
9
  signUserSignedAction,
357✔
10
} from "../../../signing/mod.ts";
357✔
11
import { assertSuccessResponse } from "./_errors.ts";
357✔
12
import type { AnyResponse, AnySuccessResponse, ExchangeRequestConfig, MultiSignRequestConfig } from "./_types.ts";
13
import { getNonce } from "./_nonce.ts";
357✔
14

15
export async function executeL1Action<T extends AnySuccessResponse>(
357✔
16
  config: ExchangeRequestConfig | MultiSignRequestConfig,
357✔
17
  request: {
357✔
18
    action: Record<string, unknown>;
19
    vaultAddress?: `0x${string}`;
20
    expiresAfter?: number;
21
  },
357✔
22
  signal?: AbortSignal,
357✔
23
): Promise<T> {
24
  const { transport } = config;
543✔
25
  const { action, vaultAddress, expiresAfter } = request;
543✔
26

27
  // Sequential request execution to prevent nonce race conditions at the network layer
28
  const walletAddress = "signers" in config
543✔
29
    ? await getWalletAddress(config.signers[0])
543✔
30
    : await getWalletAddress(config.wallet);
543✔
31
  const walletKey = `@nktkas/hyperliquid:${walletAddress}:${config.transport.isTestnet}`;
656✔
32

33
  const sem = getSemaphore(walletKey);
656✔
34
  await sem.acquire();
656✔
35

36
  // Main logic
37
  try {
543✔
38
    const nonce = await getNonce(config, walletAddress);
543✔
39

40
    // Multi-signature request
41
    if ("signers" in config) {
543✔
42
      const { signers, multiSigUser } = config;
729✔
43
      const outerSigner = walletAddress;
729✔
44

45
      // Sign an L1 action for each signer
46
      const signatures = await Promise.all(signers.map(async (signer) => {
729✔
47
        const signature = await signL1Action({
802✔
48
          wallet: signer,
802✔
49
          action: [multiSigUser, outerSigner, action],
4,010✔
50
          nonce,
802✔
51
          isTestnet: transport.isTestnet,
802✔
52
          vaultAddress,
802✔
53
          expiresAfter,
802✔
54
        });
802✔
55
        signature.r = signature.r.replace(/^0x0+/, "0x") as `0x${string}`;
802✔
56
        signature.s = signature.s.replace(/^0x0+/, "0x") as `0x${string}`;
802✔
57
        return signature;
802✔
58
      }));
729✔
59

60
      // Send a request via multi-sign action
61
      return await executeMultiSigAction(
729✔
62
        { ...config, wallet: signers[0] },
2,916✔
63
        {
729✔
64
          action: {
729✔
65
            type: "multiSig",
729✔
66
            signatureChainId: await getSignatureChainId(config),
729✔
67
            signatures,
729✔
68
            payload: { multiSigUser, outerSigner, action },
3,645✔
69
          },
729✔
70
          vaultAddress,
729✔
71
          expiresAfter,
729✔
72
          nonce,
729✔
73
        },
729✔
74
        signal,
729✔
75
        false,
729✔
76
      );
77
    } else { // Single-signature request
656✔
78
      const { wallet } = config;
656✔
79

80
      // Sign an L1 action
81
      const signature = await signL1Action({
656✔
82
        wallet,
656✔
83
        action,
656✔
84
        nonce,
656✔
85
        isTestnet: transport.isTestnet,
656✔
86
        vaultAddress,
656✔
87
        expiresAfter,
656✔
88
      });
656✔
89

90
      // Send a request
91
      const response = await transport.request(
656✔
92
        "exchange",
656✔
93
        { action, signature, nonce, vaultAddress, expiresAfter },
4,592✔
94
        signal,
656✔
95
      ) as AnyResponse;
96
      assertSuccessResponse(response);
656✔
97
      return response as T;
656✔
98
    }
656✔
99
  } finally {
543✔
100
    // Release semaphore
101
    sem.release();
543✔
102
  }
543✔
103
}
543✔
104

105
export async function executeUserSignedAction<T extends AnySuccessResponse>(
357✔
106
  config: ExchangeRequestConfig | MultiSignRequestConfig,
357✔
107
  request: {
357✔
108
    action:
109
      & {
110
        signatureChainId: `0x${string}`;
111
        [key: string]: unknown;
112
      }
113
      & (
114
        | { nonce: number; time?: undefined }
115
        | { time: number; nonce?: undefined }
116
      );
117
  },
357✔
118
  types: {
357✔
119
    [key: string]: {
120
      name: string;
121
      type: string;
122
    }[];
123
  },
357✔
124
  signal?: AbortSignal,
357✔
125
): Promise<T> {
126
  const { transport } = config;
662✔
127
  const { action } = request;
662✔
128

129
  // Sequential request execution to prevent nonce race conditions at the network layer
130
  const walletAddress = "signers" in config
662✔
131
    ? await getWalletAddress(config.signers[0])
662✔
132
    : await getWalletAddress(config.wallet);
662✔
133
  const walletKey = `@nktkas/hyperliquid:${walletAddress}:${config.transport.isTestnet}`;
906✔
134

135
  const sem = getSemaphore(walletKey);
906✔
136
  await sem.acquire();
906✔
137

138
  // Main logic
139
  try {
662✔
140
    const nonce = await getNonce(config, walletAddress);
662✔
141
    if ("time" in action) request.action.time = nonce;
662✔
142
    if ("nonce" in action) request.action.nonce = nonce;
662✔
143

144
    // Multi-signature request
145
    if ("signers" in config) {
662✔
146
      const { signers, multiSigUser } = config;
723✔
147
      const outerSigner = walletAddress;
723✔
148

149
      // Sign a user-signed action for each signer
150
      const signatures = await Promise.all(signers.map(async (signer) => {
723✔
151
        const signature = await signUserSignedAction({
784✔
152
          wallet: signer,
784✔
153
          action: {
784✔
154
            payloadMultiSigUser: multiSigUser as `0x${string}`,
784✔
155
            outerSigner,
784✔
156
            ...action,
784✔
157
          },
784✔
158
          types,
784✔
159
        });
784✔
160
        signature.r = signature.r.replace(/^0x0+/, "0x") as `0x${string}`;
784✔
161
        signature.s = signature.s.replace(/^0x0+/, "0x") as `0x${string}`;
784✔
162
        return signature;
784✔
163
      }));
723✔
164

165
      // Send a request via multi-sign action
166
      return await executeMultiSigAction(
723✔
167
        { ...config, wallet: signers[0] },
2,892✔
168
        {
723✔
169
          action: {
723✔
170
            type: "multiSig",
723✔
171
            signatureChainId: action.signatureChainId,
723✔
172
            signatures,
723✔
173
            payload: { multiSigUser, outerSigner, action },
3,615✔
174
          },
723✔
175
          nonce,
723✔
176
        },
723✔
177
        signal,
723✔
178
        false,
723✔
179
      );
180
    } else { // Single-signature request
662✔
181
      const { wallet } = config;
906✔
182

183
      // Sign a user-signed action
184
      const signature = await signUserSignedAction({ wallet, action, types });
4,530✔
185

186
      // Send a request
187
      const response = await transport.request(
906✔
188
        "exchange",
906✔
189
        { action, signature, nonce },
4,530✔
190
        signal,
906✔
191
      ) as AnyResponse;
192
      assertSuccessResponse(response);
906✔
193
      return response as T;
906✔
194
    }
906✔
195
  } finally {
662✔
196
    // Release semaphore and nonce manager
197
    sem.release();
662✔
198
  }
662✔
199
}
662✔
200

201
export async function executeMultiSigAction<T extends AnySuccessResponse>(
357✔
202
  config: ExchangeRequestConfig,
357✔
203
  request: {
357✔
204
    action: {
205
      signatureChainId: `0x${string}`;
206
      [key: string]: unknown;
207
    };
208
    nonce: number;
209
    vaultAddress?: `0x${string}`;
210
    expiresAfter?: number;
211
  },
357✔
212
  signal?: AbortSignal,
357✔
213
  semaphore = true,
357✔
214
): Promise<T> {
215
  const { transport, wallet } = config;
491✔
216
  const { action, nonce, vaultAddress, expiresAfter } = request;
491✔
217

218
  // Sequential request execution to prevent nonce race conditions at the network layer
219
  let sem: ReturnType<typeof getSemaphore> | undefined;
491✔
220
  if (semaphore) {
×
221
    const walletAddress = await getWalletAddress(config.wallet);
×
222
    const walletKey = `@nktkas/hyperliquid:${walletAddress}:${config.transport.isTestnet}`;
×
223
    sem = getSemaphore(walletKey);
×
224
    await sem.acquire();
×
225
  }
×
226

227
  // Main logic
228
  try {
491✔
229
    // Sign a multi-signature action
230
    const signature = await signMultiSigAction({
491✔
231
      wallet,
491✔
232
      action,
491✔
233
      nonce,
491✔
234
      isTestnet: transport.isTestnet,
491✔
235
      vaultAddress,
491✔
236
      expiresAfter,
491✔
237
    });
491✔
238

239
    // Send a request
240
    const response = await transport.request(
491✔
241
      "exchange",
491✔
242
      { action, signature, nonce, vaultAddress, expiresAfter },
3,437✔
243
      signal,
491✔
244
    ) as AnyResponse;
245
    assertSuccessResponse(response);
491✔
246
    return response as T;
491✔
247
  } finally {
491✔
248
    // Release semaphore if used
249
    sem?.release();
×
250
  }
491✔
251
}
491✔
252

253
/** Get the signature chain ID from the config value / function or from the wallet. */
254
export async function getSignatureChainId(
357✔
255
  config:
357✔
256
    | {
257
      wallet: AbstractWallet;
258
      signatureChainId?: string | (() => MaybePromise<string>);
259
    }
260
    | {
261
      signers: readonly AbstractWallet[];
262
      signatureChainId?: string | (() => MaybePromise<string>);
263
    },
357✔
264
): Promise<`0x${string}`> {
265
  if ("signatureChainId" in config && config.signatureChainId) {
×
266
    const signatureChainId = typeof config.signatureChainId === "string"
×
267
      ? config.signatureChainId
×
268
      : await config.signatureChainId();
×
269
    return parser(Hex)(signatureChainId);
×
270
  } else if ("wallet" in config) {
×
271
    return await getWalletChainId(config.wallet);
735✔
272
  } else {
×
273
    return await getWalletChainId(config.signers[0]);
×
274
  }
×
275
}
735✔
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