UNPKG

@nktkas/hyperliquid

Version:

Unofficial Hyperliquid API SDK for all major JS runtimes, written in TypeScript and provided with tests

388 lines (387 loc) 15.4 kB
/** * This module contains functions for generating Hyperliquid transaction signatures * and interfaces to various wallet implementations. * * @example * ```ts * import { signL1Action } from "@nktkas/hyperliquid/signing"; * * const action = { * type: "cancel", * cancels: [{ a: 0, o: 12345 }], * }; * const nonce = Date.now(); * * const signature = await signL1Action({ * wallet, * action, * nonce, * isTestnet: true, // Change to false for mainnet * }); * ``` * @example * ```ts * import { signUserSignedAction } from "@nktkas/hyperliquid/signing"; * * const action = { * type: "approveAgent", * hyperliquidChain: "Testnet", // "Mainnet" or "Testnet" * signatureChainId: "0x66eee", * nonce: Date.now(), * agentAddress: "0x...", * agentName: "Agent", * }; * * const signature = await signUserSignedAction({ * wallet, * action, * types: { * "HyperliquidTransaction:ApproveAgent": [ * { name: "hyperliquidChain", type: "string" }, * { name: "agentAddress", type: "address" }, * { name: "agentName", type: "string" }, * { name: "nonce", type: "uint64" }, * ], * }, * chainId: parseInt(action.signatureChainId, 16), * }); * ``` * * @module */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __exportStar = (this && this.__exportStar) || function(m, exports) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); }; (function (factory) { if (typeof module === "object" && typeof module.exports === "object") { var v = factory(require, exports); if (v !== undefined) module.exports = v; } else if (typeof define === "function" && define.amd) { define(["require", "exports", "@noble/hashes/sha3", "../../deps/jsr.io/@std/msgpack/1.0.3/encode.js", "../../deps/jsr.io/@std/encoding/1.0.10/hex.js", "../../deps/jsr.io/@std/bytes/1.0.6/concat.js", "./_ethers.js", "./_private_key.js", "./_viem.js", "./_window.js", "./_sorter.js"], factory); } })(function (require, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.isValidPrivateKey = exports.isAbstractWindowEthereum = exports.isAbstractViemWalletClient = exports.isAbstractEthersV5Signer = exports.isAbstractEthersSigner = void 0; exports.createL1ActionHash = createL1ActionHash; exports.signL1Action = signL1Action; exports.signUserSignedAction = signUserSignedAction; exports.signMultiSigAction = signMultiSigAction; const sha3_1 = require("@noble/hashes/sha3"); const encode_js_1 = require("../../deps/jsr.io/@std/msgpack/1.0.3/encode.js"); const hex_js_1 = require("../../deps/jsr.io/@std/encoding/1.0.10/hex.js"); const concat_js_1 = require("../../deps/jsr.io/@std/bytes/1.0.6/concat.js"); const _ethers_js_1 = require("./_ethers.js"); Object.defineProperty(exports, "isAbstractEthersSigner", { enumerable: true, get: function () { return _ethers_js_1.isAbstractEthersSigner; } }); Object.defineProperty(exports, "isAbstractEthersV5Signer", { enumerable: true, get: function () { return _ethers_js_1.isAbstractEthersV5Signer; } }); const _private_key_js_1 = require("./_private_key.js"); Object.defineProperty(exports, "isValidPrivateKey", { enumerable: true, get: function () { return _private_key_js_1.isValidPrivateKey; } }); const _viem_js_1 = require("./_viem.js"); Object.defineProperty(exports, "isAbstractViemWalletClient", { enumerable: true, get: function () { return _viem_js_1.isAbstractViemWalletClient; } }); const _window_js_1 = require("./_window.js"); Object.defineProperty(exports, "isAbstractWindowEthereum", { enumerable: true, get: function () { return _window_js_1.isAbstractWindowEthereum; } }); __exportStar(require("./_sorter.js"), exports); /** * Create a hash of the L1 action. * * Note: Hash generation depends on the order of the action keys. * * @param action - The action to be hashed. * @param nonce - Unique request identifier (recommended current timestamp in ms). * @param vaultAddress - Optional vault address used in the action. * @param expiresAfter - Optional expiration time of the action in milliseconds since the epoch. * @returns The hash of the action. */ function createL1ActionHash(action, nonce, vaultAddress, expiresAfter) { // 1. Action const actionBytes = (0, encode_js_1.encode)(normalizeIntegersForMsgPack(action)); // 2. Nonce const nonceBytes = new Uint8Array(8); new DataView(nonceBytes.buffer).setBigUint64(0, BigInt(nonce)); // 3. Vault address const vaultMarker = vaultAddress ? Uint8Array.of(1) : Uint8Array.of(0); const vaultBytes = vaultAddress ? (0, hex_js_1.decodeHex)(vaultAddress.slice(2)) : new Uint8Array(); // 4. Expires after let expiresMarker; let expiresBytes; if (expiresAfter !== undefined) { expiresMarker = Uint8Array.of(0); expiresBytes = new Uint8Array(8); new DataView(expiresBytes.buffer).setBigUint64(0, BigInt(expiresAfter)); } else { expiresMarker = new Uint8Array(); expiresBytes = new Uint8Array(); } // Create a keccak256 hash const bytes = (0, concat_js_1.concat)([ actionBytes, nonceBytes, vaultMarker, vaultBytes, expiresMarker, expiresBytes, ]); const hash = (0, sha3_1.keccak_256)(bytes); return `0x${(0, hex_js_1.encodeHex)(hash)}`; } /** Layer to make {@link https://jsr.io/@std/msgpack | @std/msgpack} compatible with {@link https://github.com/msgpack/msgpack-javascript | @msgpack/msgpack}. */ function normalizeIntegersForMsgPack(obj) { const THIRTY_ONE_BITS = 2147483648; const THIRTY_TWO_BITS = 4294967296; if (typeof obj === "number" && Number.isInteger(obj) && obj <= Number.MAX_SAFE_INTEGER && obj >= Number.MIN_SAFE_INTEGER && (obj >= THIRTY_TWO_BITS || obj < -THIRTY_ONE_BITS)) { return BigInt(obj); } if (Array.isArray(obj)) { return obj.map(normalizeIntegersForMsgPack); } if (obj && typeof obj === "object" && obj !== null) { return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, normalizeIntegersForMsgPack(value)])); } return obj; } /** * Sign an L1 action. * * Note: Signature generation depends on the order of the action keys. * @param args - Arguments for signing the action. * @returns The signature components r, s, and v. * @example * ```ts * import { signL1Action } from "@nktkas/hyperliquid/signing"; * * const privateKey = "0x..."; // or `viem`, `ethers` * * const action = { * type: "cancel", * cancels: [ * { a: 0, o: 12345 }, // Asset index and order ID * ], * }; * const nonce = Date.now(); * * const signature = await signL1Action({ * wallet: privateKey, * action, * nonce, * isTestnet: true, // Change to false for mainnet * }); * * const response = await fetch("https://api.hyperliquid-testnet.xyz/exchange", { * method: "POST", * headers: { "Content-Type": "application/json" }, * body: JSON.stringify({ action, signature, nonce }), * }); * const body = await response.json(); * ``` */ async function signL1Action(args) { const { wallet, action, nonce, isTestnet = false, vaultAddress, expiresAfter, } = args; const domain = { name: "Exchange", version: "1", chainId: 1337, // hyperliquid requires a fixed chain verifyingContract: "0x0000000000000000000000000000000000000000", }; const types = { Agent: [ { name: "source", type: "string" }, { name: "connectionId", type: "bytes32" }, ], }; const actionHash = createL1ActionHash(action, nonce, vaultAddress, expiresAfter); const message = { source: isTestnet ? "b" : "a", connectionId: actionHash, }; const signature = await abstractSignTypedData({ wallet, domain, types, message }); return splitSignature(signature); } /** * Sign a user-signed action. * * Note: Signature generation depends on the order of types. * * @param args - Arguments for signing the action. * @returns The signature components r, s, and v. * @example * ```ts * import { signUserSignedAction } from "@nktkas/hyperliquid/signing"; * * const privateKey = "0x..."; // or `viem`, `ethers` * * const action = { * type: "approveAgent", * hyperliquidChain: "Testnet", // "Mainnet" or "Testnet" * signatureChainId: "0x66eee", * nonce: Date.now(), * agentAddress: "0x...", // Change to your agent address * agentName: "Agent", * }; * * const signature = await signUserSignedAction({ * wallet: privateKey, * action, * types: { * "HyperliquidTransaction:ApproveAgent": [ * { name: "hyperliquidChain", type: "string" }, * { name: "agentAddress", type: "address" }, * { name: "agentName", type: "string" }, * { name: "nonce", type: "uint64" }, * ], * }, * chainId: parseInt(action.signatureChainId, 16), * }); * * const response = await fetch("https://api.hyperliquid-testnet.xyz/exchange", { * method: "POST", * headers: { "Content-Type": "application/json" }, * body: JSON.stringify({ action, signature, nonce: action.nonce }), * }); * const body = await response.json(); * ``` */ async function signUserSignedAction(args) { const { wallet, action, types, chainId } = args; const domain = { name: "HyperliquidSignTransaction", version: "1", chainId, verifyingContract: "0x0000000000000000000000000000000000000000", }; const signature = await abstractSignTypedData({ wallet, domain, types, message: action }); return splitSignature(signature); } /** * Sign a multi-signature action. * * Note: Signature generation depends on the order of the action keys. * * @param args - Arguments for signing the action. * @returns The signature components r, s, and v. * @example * ```ts * import { signL1Action, signMultiSigAction } from "@nktkas/hyperliquid/signing"; * import { privateKeyToAccount } from "viem/accounts"; * * const wallet = privateKeyToAccount("0x..."); * const multiSigUser = "0x..."; // Multi-sig user address * * const nonce = Date.now(); * const action = { // Example action * type: "scheduleCancel", * time: Date.now() + 10000 * }; * * // First, create signature from one of the authorized signers * const signature = await signL1Action({ * wallet, * action: [multiSigUser.toLowerCase(), wallet.address.toLowerCase(), action], * nonce, * isTestnet: true, * }); * * // Then use it in the multi-sig action * const multiSigSignature = await signMultiSigAction({ * wallet, * action: { * type: "multiSig", * signatureChainId: "0x66eee", * signatures: [signature], * payload: { * multiSigUser, * outerSigner: wallet.address, * action, * } * }, * nonce, * hyperliquidChain: "Testnet", * signatureChainId: "0x66eee", * }); * ``` */ async function signMultiSigAction(args) { const { wallet, action, nonce, hyperliquidChain, signatureChainId, vaultAddress, expiresAfter, } = args; const multiSigActionHash = createL1ActionHash(action, nonce, vaultAddress, expiresAfter); const message = { multiSigActionHash, hyperliquidChain, signatureChainId, nonce, }; return await signUserSignedAction({ wallet, action: message, types: { "HyperliquidTransaction:SendMultiSig": [ { name: "hyperliquidChain", type: "string" }, { name: "multiSigActionHash", type: "bytes32" }, { name: "nonce", type: "uint64" }, ], }, chainId: parseInt(signatureChainId, 16), }); } /** Signs typed data with the provided wallet using EIP-712. */ async function abstractSignTypedData(args) { const { wallet, domain, types, message } = args; if ((0, _private_key_js_1.isValidPrivateKey)(wallet)) { return await (0, _private_key_js_1.signTypedDataWithPrivateKey)({ privateKey: wallet, domain, types, primaryType: Object.keys(types)[0], message, }); } else if ((0, _viem_js_1.isAbstractViemWalletClient)(wallet)) { return await wallet.signTypedData({ domain, types: { EIP712Domain: [ { name: "name", type: "string" }, { name: "version", type: "string" }, { name: "chainId", type: "uint256" }, { name: "verifyingContract", type: "address" }, ], ...types, }, primaryType: Object.keys(types)[0], message, }); } else if ((0, _ethers_js_1.isAbstractEthersSigner)(wallet)) { return await wallet.signTypedData(domain, types, message); } else if ((0, _ethers_js_1.isAbstractEthersV5Signer)(wallet)) { return await wallet._signTypedData(domain, types, message); } else if ((0, _window_js_1.isAbstractWindowEthereum)(wallet)) { return await (0, _window_js_1.signTypedDataWithWindowEthereum)(wallet, domain, types, message); } else { throw new Error("Unsupported wallet for signing typed data"); } } /** Splits a signature hexadecimal string into its components. */ function splitSignature(signature) { const r = `0x${signature.slice(2, 66)}`; const s = `0x${signature.slice(66, 130)}`; const v = parseInt(signature.slice(130, 132), 16); return { r, s, v }; } });