@nktkas/hyperliquid
Version:
Hyperliquid API SDK for all major JS runtimes, written in TypeScript.
199 lines • 8.42 kB
JavaScript
import * as v from "valibot";
import { Address, Hex, UnsignedInteger } from "../../../_schemas.js";
import { getWalletAddress, getWalletChainId, signL1Action, signMultiSigAction, signUserSignedAction, } from "../../../../signing/mod.js";
import { assertSuccessResponse } from "./errors.js";
import { defaultNonceManager } from "./_nonce.js";
import { withLock } from "./_semaphore.js";
// =============================================================
// Execute L1 Action
// =============================================================
/**
* Execute an L1 action on the Hyperliquid Exchange.
* Handles both single-wallet and multi-sig signing.
*/
export async function executeL1Action(config, action, options) {
const { transport } = config;
const leader = getLeader(config);
const walletAddress = await getWalletAddress(leader);
// Semaphore ensures requests arrive at server in nonce order (prevents out-of-order delivery)
const key = `${walletAddress}:${transport.isTestnet}`;
return await withLock(key, async () => {
const nonce = await (config.nonceManager?.(walletAddress) ?? defaultNonceManager.getNonce(key));
// Validate and resolve options
const vaultAddress = v.parse(v.optional(Address), options?.vaultAddress ?? config.defaultVaultAddress);
const expiresAfter = v.parse(v.optional(UnsignedInteger), options?.expiresAfter ??
(typeof config.defaultExpiresAfter === "number"
? config.defaultExpiresAfter
: await config.defaultExpiresAfter?.()));
const signal = options?.signal;
// Sign action (multi-sig or single wallet)
const [finalAction, signature] = isMultiSig(config)
? await signMultiSigL1(config, action, walletAddress, nonce, vaultAddress, expiresAfter)
: [
action,
await signL1Action({
wallet: leader,
action,
nonce,
isTestnet: transport.isTestnet,
vaultAddress,
expiresAfter,
}),
];
// Send request and validate response
const response = await transport.request("exchange", {
action: finalAction,
signature,
nonce,
vaultAddress,
expiresAfter,
}, signal);
assertSuccessResponse(response);
return response;
});
}
// =============================================================
// Execute User-Signed Action
// =============================================================
/** Extract nonce field name from EIP-712 types ("nonce" or "time"). */
function getNonceFieldName(types) {
const primaryType = Object.keys(types)[0];
const field = types[primaryType].find((f) => f.name === "nonce" || f.name === "time");
return field?.name ?? "nonce";
}
/**
* Execute a user-signed action (EIP-712) on the Hyperliquid Exchange.
* Handles both single-wallet and multi-sig signing.
* Automatically adds signatureChainId, hyperliquidChain, and nonce/time.
*/
export async function executeUserSignedAction(config, action, types, options) {
const { transport } = config;
const leader = getLeader(config);
const walletAddress = await getWalletAddress(leader);
// Semaphore ensures requests arrive at server in nonce order (prevents out-of-order delivery)
const key = `${walletAddress}:${transport.isTestnet}`;
return withLock(key, async () => {
const nonce = await (config.nonceManager?.(walletAddress) ?? defaultNonceManager.getNonce(key));
const signal = options?.signal;
// Add system fields for user-signed actions
const { type, ...restAction } = action;
const nonceFieldName = getNonceFieldName(types);
const fullAction = {
type,
signatureChainId: await getSignatureChainId(config),
hyperliquidChain: transport.isTestnet ? "Testnet" : "Mainnet",
...restAction,
[nonceFieldName]: nonce,
};
// Sign action (multi-sig or single wallet)
const [finalAction, signature] = isMultiSig(config)
? await signMultiSigUserSigned(config, fullAction, types, walletAddress, nonce)
: [fullAction, await signUserSignedAction({ wallet: leader, action: fullAction, types })];
// Send request and validate response
const response = await transport.request("exchange", {
action: finalAction,
signature,
nonce,
}, signal);
assertSuccessResponse(response);
return response;
});
}
// =============================================================
// Multi-sig signing (private)
// =============================================================
/** Remove leading zeros from signature components (required by Hyperliquid). */
function trimSignature(sig) {
return {
r: sig.r.replace(/^0x0+/, "0x"),
s: sig.s.replace(/^0x0+/, "0x"),
v: sig.v,
};
}
/** Sign an L1 action with multi-sig. */
async function signMultiSigL1(config, action, outerSigner, nonce, vaultAddress, expiresAfter) {
const { transport: { isTestnet }, wallet: signers, multiSigUser } = config;
const multiSigUser_ = v.parse(Address, multiSigUser);
const outerSigner_ = v.parse(Address, outerSigner);
// Collect signatures from all signers
const signatures = await Promise.all(signers.map(async (signer) => {
const signature = await signL1Action({
wallet: signer,
action: [multiSigUser_, outerSigner_, action],
nonce,
isTestnet,
vaultAddress,
expiresAfter,
});
return trimSignature(signature);
}));
// Build multi-sig action wrapper
const multiSigAction = {
type: "multiSig",
signatureChainId: await getSignatureChainId(config),
signatures,
payload: { multiSigUser: multiSigUser_, outerSigner: outerSigner_, action },
};
// Sign the wrapper with the leader
const signature = await signMultiSigAction({
wallet: signers[0],
action: multiSigAction,
nonce,
isTestnet,
vaultAddress,
expiresAfter,
});
return [multiSigAction, signature];
}
/** Sign a user-signed action (EIP-712) with multi-sig. */
async function signMultiSigUserSigned(config, action, types, outerSigner, nonce) {
const { wallet: signers, multiSigUser, transport: { isTestnet } } = config;
const multiSigUser_ = v.parse(Address, multiSigUser);
const outerSigner_ = v.parse(Address, outerSigner);
// Collect signatures from all signers
const signatures = await Promise.all(signers.map(async (signer) => {
const signature = await signUserSignedAction({
wallet: signer,
action: { payloadMultiSigUser: multiSigUser_, outerSigner: outerSigner_, ...action },
types,
});
return trimSignature(signature);
}));
// Build multi-sig action wrapper
const multiSigAction = {
type: "multiSig",
signatureChainId: await getSignatureChainId(config),
signatures,
payload: { multiSigUser: multiSigUser_, outerSigner: outerSigner_, action },
};
// Sign the wrapper with the leader
const signature = await signMultiSigAction({
wallet: signers[0],
action: multiSigAction,
nonce,
isTestnet,
});
return [multiSigAction, signature];
}
// =============================================================
// Helpers (private)
// =============================================================
/** Type guard for multi-sig configuration. */
function isMultiSig(config) {
return Array.isArray(config.wallet);
}
/** Get the leader wallet (first signer for multi-sig, or the single wallet). */
function getLeader(config) {
return isMultiSig(config) ? config.wallet[0] : config.wallet;
}
/** Resolve signature chain ID from config or wallet. */
async function getSignatureChainId(config) {
if (config.signatureChainId) {
const id = typeof config.signatureChainId === "function"
? await config.signatureChainId()
: config.signatureChainId;
return v.parse(Hex, id);
}
return getWalletChainId(getLeader(config));
}
//# sourceMappingURL=execute.js.map