@nktkas/hyperliquid
Version:
Hyperliquid API SDK for all major JS runtimes, written in TypeScript.
289 lines (288 loc) • 9.05 kB
JavaScript
/**
* Multi-sig wrapper construction and outer signing.
* @module
*/ import { getWalletAddress, signTypedData } from "./_abstractWallet.js";
import { createL1ActionHash, signL1Inner } from "./_l1.js";
import { signUserSignedInner } from "./_userSigned.js";
/** EIP-712 types for the multi-sig outer wrapper. */ const MULTI_SIG_TYPES = {
"HyperliquidTransaction:SendMultiSig": [
{
name: "hyperliquidChain",
type: "string"
},
{
name: "multiSigActionHash",
type: "bytes32"
},
{
name: "nonce",
type: "uint64"
}
]
};
/**
* Signs the multi-sig outer wrapper with the leader's wallet.
*
* @param args The leader wallet, the wrapper, and signing parameters.
* @return The leader's ECDSA signature.
*
* @throws {AbstractWalletError} If signing fails.
*/ async function signMultiSigOuter(args) {
const { leader, wrapper, nonce, isTestnet = false, vaultAddress, expiresAfter } = args;
const { type: _, ...wrapperWithoutType } = wrapper;
const multiSigActionHash = createL1ActionHash({
action: wrapperWithoutType,
nonce,
vaultAddress,
expiresAfter
});
return await signTypedData({
wallet: leader,
domain: {
name: "HyperliquidSignTransaction",
version: "1",
chainId: parseInt(wrapper.signatureChainId),
verifyingContract: "0x0000000000000000000000000000000000000000"
},
types: MULTI_SIG_TYPES,
primaryType: "HyperliquidTransaction:SendMultiSig",
message: {
hyperliquidChain: isTestnet ? "Testnet" : "Mainnet",
multiSigActionHash,
nonce
}
});
}
/**
* Signs an L1 action with multi-sig orchestration.
*
* Collects inner signatures from each signer over `[multiSigUser, outerSigner, action]`,
* builds the multi-sig wrapper, and signs the wrapper with the leader (first signer).
*
* @param args The signers, action, and signing parameters.
* @return The wrapper action (modified) and the leader's signature.
*
* @throws {AbstractWalletError} If signing fails.
*
* @example
* ```ts
* import { signMultiSigL1 } from "@nktkas/hyperliquid/signing";
* import { privateKeyToAccount } from "npm:viem/accounts";
*
* const signers = [
* privateKeyToAccount("0x..."),
* privateKeyToAccount("0x..."),
* // ...more signers if needed
* ] as const;
*
* const action = { type: "cancel", cancels: [{ a: 0, o: 12345 }] };
* const nonce = Date.now();
*
* const { action: wrapper, signature } = await signMultiSigL1({
* signers,
* multiSigUser: "0x...",
* signatureChainId: "0x66eee",
* action,
* nonce,
* });
* ```
*
* @example
* \- Full cycle of signing and sending a multi-sig L1 action to the Hyperliquid API
* ```ts
* import { canonicalize, signMultiSigL1 } from "@nktkas/hyperliquid/signing";
* import { CancelRequest } from "@nktkas/hyperliquid/api/exchange";
* import { privateKeyToAccount } from "npm:viem/accounts";
*
* const signers = [
* privateKeyToAccount("0x..."),
* privateKeyToAccount("0x..."),
* // ...more signers if needed
* ] as const;
*
* // For correct hashing, keys in the L1 action must be in
* // the same order as in the schema definition
* // ⌄⌄⌄⌄⌄⌄⌄⌄⌄
* const action = canonicalize(CancelRequest.entries.action, {
* type: "cancel",
* cancels: [{ a: 0, o: 12345 }],
* });
* const nonce = Date.now();
*
* const { action: wrapper, signature } = await signMultiSigL1({
* signers,
* multiSigUser: "0x...",
* signatureChainId: "0x66eee",
* action,
* nonce,
* });
*
* // Send the multi-sig wrapper to the Hyperliquid API
* const response = await fetch("https://api.hyperliquid.xyz/exchange", {
* method: "POST",
* headers: { "Content-Type": "application/json" },
* body: JSON.stringify({ action: wrapper, signature, nonce }),
* });
* const body = await response.json();
* ```
*/ export async function signMultiSigL1(args) {
// --- Resolve leader (outer signer) address ---------------
const outerSigner = await getWalletAddress(args.signers[0]);
// --- Collect inner signatures from all signers -----------
const innerSignatures = await Promise.all(args.signers.map((signer)=>signL1Inner({
signer,
action: args.action,
multiSigUser: args.multiSigUser,
outerSigner,
nonce: args.nonce,
isTestnet: args.isTestnet,
vaultAddress: args.vaultAddress,
expiresAfter: args.expiresAfter
})));
// --- Build wrapper ---------------------------------------
const wrapper = {
type: "multiSig",
signatureChainId: args.signatureChainId,
signatures: innerSignatures,
payload: {
multiSigUser: args.multiSigUser.toLowerCase(),
outerSigner,
action: args.action
}
};
// --- Sign wrapper with leader ----------------------------
const signature = await signMultiSigOuter({
leader: args.signers[0],
wrapper,
nonce: args.nonce,
isTestnet: args.isTestnet,
vaultAddress: args.vaultAddress,
expiresAfter: args.expiresAfter
});
return {
action: wrapper,
signature
};
}
/**
* Signs a user-signed action with multi-sig orchestration.
*
* Collects inner signatures from each signer over the action with multi-sig
* fields injected into its [EIP-712](https://eips.ethereum.org/EIPS/eip-712) type, builds the wrapper, and signs the
* wrapper with the leader.
*
* @param args The signers, action, types, and signing parameters.
* @return The wrapper action and the leader's signature.
*
* @throws {AbstractWalletError} If signing fails.
*
* @example
* ```ts
* import { signMultiSigUserSigned } from "@nktkas/hyperliquid/signing";
* import { ApproveAgentTypes } from "@nktkas/hyperliquid/api/exchange";
* import { privateKeyToAccount } from "npm:viem/accounts";
*
* const signers = [
* privateKeyToAccount("0x..."),
* privateKeyToAccount("0x..."),
* // ...more signers if needed
* ] as const;
*
* const types = ApproveAgentTypes; // or custom EIP-712 types matching the action
* const action = {
* type: "approveAgent",
* signatureChainId: "0x66eee" as const,
* hyperliquidChain: "Mainnet" as const,
* agentAddress: "0x...",
* agentName: "Agent",
* nonce: Date.now(),
* };
*
* const { action: wrapper, signature } = await signMultiSigUserSigned({
* signers,
* multiSigUser: "0x...",
* action,
* types,
* });
* ```
*
* @example
* \- Full cycle of signing and sending a multi-sig user-signed action to the Hyperliquid API
* ```ts
* import { signMultiSigUserSigned } from "@nktkas/hyperliquid/signing";
* import { ApproveAgentTypes } from "@nktkas/hyperliquid/api/exchange";
* import { privateKeyToAccount } from "npm:viem/accounts";
*
* const signers = [
* privateKeyToAccount("0x..."),
* privateKeyToAccount("0x..."),
* // ...more signers if needed
* ] as const;
*
* const types = ApproveAgentTypes; // or custom EIP-712 types matching the action
* const action = {
* type: "approveAgent",
* signatureChainId: "0x66eee" as const,
* hyperliquidChain: "Mainnet" as const,
* agentAddress: "0x...",
* agentName: "Agent",
* nonce: Date.now(),
* };
*
* const { action: wrapper, signature } = await signMultiSigUserSigned({
* signers,
* multiSigUser: "0x...",
* action,
* types,
* });
*
* // Send the multi-sig wrapper to the Hyperliquid API
* const response = await fetch("https://api.hyperliquid.xyz/exchange", {
* method: "POST",
* headers: { "Content-Type": "application/json" },
* body: JSON.stringify({ action: wrapper, signature, nonce: action.nonce }),
* });
* const body = await response.json();
* ```
*/ export async function signMultiSigUserSigned(args) {
// --- Resolve leader (outer signer) address ---------------
const outerSigner = await getWalletAddress(args.signers[0]);
// --- Collect inner signatures from all signers -----------
const innerSignatures = await Promise.all(args.signers.map((signer)=>signUserSignedInner({
signer,
action: args.action,
types: args.types,
multiSigUser: args.multiSigUser,
outerSigner
})));
// --- Build wrapper ---------------------------------------
const wrapper = {
type: "multiSig",
signatureChainId: args.action.signatureChainId,
signatures: innerSignatures,
payload: {
multiSigUser: args.multiSigUser.toLowerCase(),
outerSigner,
action: args.action
}
};
// --- Sign wrapper with leader ----------------------------
const signature = await signMultiSigOuter({
leader: args.signers[0],
wrapper,
nonce: args.action.nonce ?? args.action.time,
isTestnet: args.action.hyperliquidChain === "Testnet"
});
return {
action: wrapper,
signature
};
}
/** Removes leading zeros from signature `r` and `s` (required for multi-sig signatures). */ export function trimSignature(sig) {
return {
r: sig.r.replace(/^0x0+/, "0x"),
s: sig.s.replace(/^0x0+/, "0x"),
v: sig.v
};
}
//# sourceMappingURL=_multiSig.js.map