UNPKG

@nktkas/hyperliquid

Version:

Hyperliquid API SDK for all major JS runtimes, written in TypeScript.

199 lines 8.42 kB
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