@nktkas/hyperliquid
Version:
Hyperliquid API SDK for all major JS runtimes, written in TypeScript.
175 lines (174 loc) • 5.74 kB
JavaScript
/**
* L1 (phantom-agent) signing for trading actions.
* @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/encode.js";
import { signTypedData } from "./_abstractWallet.js";
import { trimSignature } from "./_multiSig.js";
/**
* Creates a hash of the L1 action.
*
* @param args The action and metadata to hash.
* @return The keccak256 hash as a hex string.
*
* @example
* ```ts
* import { createL1ActionHash } from "@nktkas/hyperliquid/signing";
*
* const action = { 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;
const actionBytes = encodeMsgpack(adjust(action));
const nonceBytes = toUint64Bytes(nonce);
const vaultMarker = vaultAddress ? new Uint8Array([
1
]) : new Uint8Array([
0
]);
const vaultBytes = vaultAddress ? hexToBytes(vaultAddress.slice(2)) : new Uint8Array();
const expiresMarker = expiresAfter !== undefined ? new Uint8Array([
0
]) : new Uint8Array();
const expiresBytes = expiresAfter !== undefined ? toUint64Bytes(expiresAfter) : new Uint8Array();
const bytes = concatBytes(actionBytes, nonceBytes, vaultMarker, vaultBytes, expiresMarker, expiresBytes);
return `0x${bytesToHex(keccak_256(bytes))}`;
}
/**
* Normalizes a value into a shape that `@std/msgpack` encodes the way Hyperliquid expects on the wire:
* - drops `undefined` properties (otherwise the encoder throws)
* - widens `number`s outside the int32 range to `BigInt` (otherwise they would be encoded as float64 instead of int64)
*/ function adjust(value) {
if (Array.isArray(value)) return value.map(adjust);
if (typeof value === "object" && value !== null) {
const result = {};
for(const key in value){
const entry = value[key];
if (entry !== undefined) result[key] = adjust(entry);
}
return result;
}
if (typeof value === "number" && Number.isInteger(value) && (value >= 0x100000000 || value < -0x80000000)) {
return BigInt(value);
}
return value;
}
function toUint64Bytes(n) {
const bytes = new Uint8Array(8);
new DataView(bytes.buffer).setBigUint64(0, BigInt(n));
return bytes;
}
/**
* Signs an L1 action.
*
* @param args The wallet, action, and signing parameters.
* @return The ECDSA signature.
*
* @throws {AbstractWalletError} If signing fails.
*
* @example
* ```ts
* import { signL1Action } from "@nktkas/hyperliquid/signing";
* import { privateKeyToAccount } from "npm:viem/accounts";
*
* const wallet = privateKeyToAccount("0x..."); // `viem` or `ethers` or any `AbstractWallet`
*
* const action = { type: "cancel", cancels: [{ a: 0, o: 12345 }] };
* const nonce = Date.now();
*
* const signature = await signL1Action({ wallet, action, nonce });
* ```
*
* @example
* \- Full cycle of signing and sending an L1 action to the Hyperliquid API
* ```ts
* import { canonicalize, signL1Action } from "@nktkas/hyperliquid/signing";
* import { CancelRequest } from "@nktkas/hyperliquid/api/exchange";
* import { privateKeyToAccount } from "npm:viem/accounts";
*
* const wallet = privateKeyToAccount("0x..."); // `viem` or `ethers` or any `AbstractWallet`
*
* // 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 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;
const actionHash = createL1ActionHash({
action,
nonce,
vaultAddress,
expiresAfter
});
return await signTypedData({
wallet,
domain: {
name: "Exchange",
version: "1",
chainId: 1337,
verifyingContract: "0x0000000000000000000000000000000000000000"
},
types: {
Agent: [
{
name: "source",
type: "string"
},
{
name: "connectionId",
type: "bytes32"
}
]
},
primaryType: "Agent",
message: {
source: isTestnet ? "b" : "a",
connectionId: actionHash
}
});
}
/**
* Signs an inner per-signer contribution to a multi-sig L1 action.
*
* Signs `[multiSigUser, outerSigner, action]` with both addresses lowercased;
* the returned signature is trimmed for inclusion in the multi-sig wrapper.
*
* @param args The signer, action, and signing parameters.
* @return The trimmed ECDSA signature.
*
* @throws {AbstractWalletError} If signing fails.
*/ export async function signL1Inner(args) {
const signature = await signL1Action({
wallet: args.signer,
action: [
args.multiSigUser.toLowerCase(),
args.outerSigner.toLowerCase(),
args.action
],
nonce: args.nonce,
isTestnet: args.isTestnet,
vaultAddress: args.vaultAddress,
expiresAfter: args.expiresAfter
});
return trimSignature(signature);
}
//# sourceMappingURL=_l1.js.map