@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
JavaScript
/**
* 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 };
}
});