UNPKG

@nktkas/hyperliquid

Version:

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

352 lines 12.7 kB
/** * Low-level utilities for signing Hyperliquid transactions. * * @example Signing an L1 action * ```ts * import { signL1Action } from "@nktkas/hyperliquid/signing"; * import { CancelRequest } from "@nktkas/hyperliquid/api/exchange"; * import * as v from "npm:valibot"; * import { privateKeyToAccount } from "npm:viem/accounts"; * * const wallet = privateKeyToAccount("0x..."); // viem or ethers * * const action = v.parse(CancelRequest.entries.action, { // not required, but for correct generation * type: "cancel", * cancels: [ * { a: 0, o: 12345 }, * ], * }); * const nonce = Date.now(); * * const signature = await signL1Action({ wallet, action, nonce }); * * // Send the signed action to the Hyperliquid API * const response = await fetch("https://api.hyperliquid.xyz/exchange", { * method: "POST", * headers: { "Content-Type": "application/json" }, * body: JSON.stringify({ action, signature, nonce }), * }); * const body = await response.json(); * ``` * * @example Signing a user-signed action * ```ts * import { signUserSignedAction } from "@nktkas/hyperliquid/signing"; * import { ApproveAgentRequest, ApproveAgentTypes } from "@nktkas/hyperliquid/api/exchange"; * import * as v from "npm:valibot"; * import { privateKeyToAccount } from "npm:viem/accounts"; * * const wallet = privateKeyToAccount("0x..."); // viem or ethers * * const action = v.parse(ApproveAgentRequest.entries.action, { // not required, but for correct generation * type: "approveAgent", * signatureChainId: "0x66eee", * hyperliquidChain: "Mainnet", * agentAddress: "0x...", * agentName: "Agent", * nonce: Date.now(), * }); * * const signature = await signUserSignedAction({ wallet, action, types: ApproveAgentTypes }); * * // Send the signed action to the Hyperliquid API * const response = await fetch("https://api.hyperliquid.xyz/exchange", { * method: "POST", * headers: { "Content-Type": "application/json" }, * body: JSON.stringify({ action, signature, nonce: action.nonce }), * }); * const body = await response.json(); * ``` * * @module */ import { keccak_256 } from "@noble/hashes/sha3.js"; import { bytesToHex, concatBytes, hexToBytes } from "@noble/hashes/utils.js"; import { encode as encodeMsgpack } from "../../deps/jsr.io/@std/msgpack/1.0.3/mod.js"; import { signTypedData } from "./_abstractWallet.js"; export { AbstractWalletError, getWalletAddress, getWalletChainId, } from "./_abstractWallet.js"; export { PrivateKeySigner } from "./_privateKeySigner.js"; /** * Create a hash of the L1 action. * @example * ```ts * import { createL1ActionHash } from "@nktkas/hyperliquid/signing"; * import { CancelRequest } from "@nktkas/hyperliquid/api/exchange"; * import * as v from "npm:valibot"; * * const action = v.parse(CancelRequest.entries.action, { // not required, but for correct generation * type: "cancel", * cancels: [ * { a: 0, o: 12345 }, * ], * }); * const nonce = Date.now(); * * const actionHash = createL1ActionHash({ action, nonce }); * ``` */ export function createL1ActionHash(args) { const { action, nonce, vaultAddress, expiresAfter } = args; // 1. Action const actionBytes = encodeMsgpack(largeIntToBigInt(removeUndefinedKeys(action))); // 2. Nonce const nonceBytes = toUint64Bytes(nonce); // 3. Vault address const vaultMarker = vaultAddress ? new Uint8Array([1]) : new Uint8Array([0]); const vaultBytes = vaultAddress ? hexToBytes(vaultAddress.slice(2)) : new Uint8Array(); // 4. Expires after const expiresMarker = expiresAfter !== undefined ? new Uint8Array([0]) : new Uint8Array(); const expiresBytes = expiresAfter !== undefined ? toUint64Bytes(expiresAfter) : new Uint8Array(); // Create a hash const bytes = concatBytes(actionBytes, nonceBytes, vaultMarker, vaultBytes, expiresMarker, expiresBytes); const hash = keccak_256(bytes); return `0x${bytesToHex(hash)}`; } function toUint64Bytes(n) { const bytes = new Uint8Array(8); new DataView(bytes.buffer).setBigUint64(0, BigInt(n)); return bytes; } function largeIntToBigInt(obj) { if (typeof obj === "number" && Number.isInteger(obj) && (obj >= 0x100000000 || obj < -0x80000000)) { return BigInt(obj); } if (Array.isArray(obj)) return obj.map(largeIntToBigInt); if (typeof obj === "object" && obj !== null) { const result = {}; for (const key in obj) result[key] = largeIntToBigInt(obj[key]); return result; } return obj; } function removeUndefinedKeys(obj) { if (Array.isArray(obj)) return obj.map(removeUndefinedKeys); if (typeof obj === "object" && obj !== null) { const result = {}; for (const key in obj) { if (obj[key] !== undefined) { result[key] = removeUndefinedKeys(obj[key]); } } return result; } return obj; } /** * Sign an L1 action. * @example * ```ts * import { signL1Action } from "@nktkas/hyperliquid/signing"; * import { CancelRequest } from "@nktkas/hyperliquid/api/exchange"; * import * as v from "npm:valibot"; * import { privateKeyToAccount } from "npm:viem/accounts"; * * const wallet = privateKeyToAccount("0x..."); // viem or ethers * * const action = v.parse(CancelRequest.entries.action, { // not required, but for correct generation * type: "cancel", * cancels: [ * { a: 0, o: 12345 }, * ], * }); * const nonce = Date.now(); * * const signature = await signL1Action({ wallet, action, nonce }); * * // Send the signed action to the Hyperliquid API * const response = await fetch("https://api.hyperliquid.xyz/exchange", { * method: "POST", * headers: { "Content-Type": "application/json" }, * body: JSON.stringify({ action, signature, nonce }), * }); * const body = await response.json(); * ``` */ export async function signL1Action(args) { const { wallet, action, nonce, isTestnet = false, vaultAddress, expiresAfter } = args; return await signTypedData({ wallet, domain: { name: "Exchange", version: "1", chainId: 1337, // hyperliquid requires chainId to be 1337 verifyingContract: "0x0000000000000000000000000000000000000000", }, types: { Agent: [ { name: "source", type: "string" }, { name: "connectionId", type: "bytes32" }, ], }, primaryType: "Agent", message: { source: isTestnet ? "b" : "a", connectionId: createL1ActionHash({ action, nonce, vaultAddress, expiresAfter }), }, }); } /** * Sign a user-signed action. * @example * ```ts * import { signUserSignedAction } from "@nktkas/hyperliquid/signing"; * import { ApproveAgentRequest, ApproveAgentTypes } from "@nktkas/hyperliquid/api/exchange"; * import * as v from "npm:valibot"; * import { privateKeyToAccount } from "npm:viem/accounts"; * * const wallet = privateKeyToAccount("0x..."); // viem or ethers * * const action = v.parse(ApproveAgentRequest.entries.action, { // not required, but for correct generation * type: "approveAgent", * signatureChainId: "0x66eee", * hyperliquidChain: "Mainnet", * agentAddress: "0x...", * agentName: "Agent", * nonce: Date.now(), * }); * * const signature = await signUserSignedAction({ wallet, action, types: ApproveAgentTypes }); * * // Send the signed action to the Hyperliquid API * const response = await fetch("https://api.hyperliquid.xyz/exchange", { * method: "POST", * headers: { "Content-Type": "application/json" }, * body: JSON.stringify({ action, signature, nonce: action.nonce }), * }); * const body = await response.json(); * ``` */ export async function signUserSignedAction(args) { let { wallet, action, types } = args; const primaryType = Object.keys(types)[0]; // Special case: for `approveAgent` // If `agentName` is null or undefined, set it to an empty string if (action.type === "approveAgent" && !action.agentName) { action = { ...action, agentName: "" }; } // Special case: for multi-sign payload // If the action contains `payloadMultiSigUser` and `outerSigner`, add them to the types if ("payloadMultiSigUser" in action && "outerSigner" in action) { const primaryTypeArray = types[primaryType]; types = { ...types, [primaryType]: [ primaryTypeArray[0], // after `hyperliquidChain` { name: "payloadMultiSigUser", type: "address" }, { name: "outerSigner", type: "address" }, ...primaryTypeArray.slice(1), ], }; } // Special case: for some wallets // If the action has extra keys not in the types, filter them out const knownKeys = new Set(types[primaryType].map((f) => f.name)); const message = Object.fromEntries(Object.entries(action).filter(([k]) => knownKeys.has(k))); return await signTypedData({ wallet, domain: { name: "HyperliquidSignTransaction", version: "1", chainId: parseInt(action.signatureChainId), verifyingContract: "0x0000000000000000000000000000000000000000", }, types, primaryType, message, }); } /** * Sign a multi-signature action. * @example * ```ts * import { signL1Action, signMultiSigAction } from "@nktkas/hyperliquid/signing"; * import { ScheduleCancelRequest } from "@nktkas/hyperliquid/api/exchange"; * import * as v from "npm:valibot"; * import { privateKeyToAccount } from "npm:viem/accounts"; * * const wallet = privateKeyToAccount("0x..."); // viem or ethers * const multiSigUser = "0x..."; * * const nonce = Date.now(); * const action = v.parse(ScheduleCancelRequest.entries.action, { // not required, but for correct generation * type: "scheduleCancel", * time: Date.now() + 10000, * }); * * // Create the required number of signatures * const signatures = await Promise.all(["0x...", "0x..."].map(async (signerPrivKey) => { * return await signL1Action({ * wallet: privateKeyToAccount(signerPrivKey as `0x${string}`), // viem or ethers * action: [multiSigUser.toLowerCase(), wallet.address.toLowerCase(), action], * nonce, * }); * })); * * // // or user-signed action * // const signatures = await Promise.all(["0x...", "0x..."].map(async (signerPrivKey) => { * // return await signUserSignedAction({ * // wallet: privateKeyToAccount(signerPrivKey as `0x${string}`), // viem or ethers * // action: { * // ...action, * // payloadMultiSigUser: multiSigUser, * // outerSigner: wallet.address, * // }, * // types: SomeTypes, * // }); * // })); * * // Then use signatures in the multi-sig action * const multiSigAction = { * type: "multiSig", * signatureChainId: "0x66eee" as const, * signatures, * payload: { * multiSigUser, * outerSigner: wallet.address, * action, * }, * }; * const multiSigSignature = await signMultiSigAction({ wallet, action: multiSigAction, nonce }); * * // Send the multi-sig action to the Hyperliquid API * const response = await fetch("https://api.hyperliquid.xyz/exchange", { * method: "POST", * headers: { "Content-Type": "application/json" }, * body: JSON.stringify({ action: multiSigAction, signature: multiSigSignature, nonce }), * }); * const body = await response.json(); * ``` */ export async function signMultiSigAction(args) { let { wallet, action, nonce, isTestnet = false, vaultAddress, expiresAfter } = args; if ("type" in action) { const { type: _, ...actionWithoutType } = action; action = actionWithoutType; } return await signTypedData({ wallet, domain: { name: "HyperliquidSignTransaction", version: "1", chainId: parseInt(action.signatureChainId), verifyingContract: "0x0000000000000000000000000000000000000000", }, types: { "HyperliquidTransaction:SendMultiSig": [ { name: "hyperliquidChain", type: "string" }, { name: "multiSigActionHash", type: "bytes32" }, { name: "nonce", type: "uint64" }, ], }, primaryType: "HyperliquidTransaction:SendMultiSig", message: { hyperliquidChain: isTestnet ? "Testnet" : "Mainnet", multiSigActionHash: createL1ActionHash({ action, nonce, vaultAddress, expiresAfter }), nonce, }, }); } //# sourceMappingURL=mod.js.map