Signing
hlz implements two signing paths, matching the Hyperliquid protocol. The secp256k1 and EIP-712 implementation is adapted from zabi.
Signing Paths
RMP Path (Orders, Cancels)
Used for: place, cancel, cancelByCloid, modify, batchModify, scheduleCancel
Action → MessagePack → prepend nonce + vault → keccak256 → Agent EIP-712 (chainId 1337)const signing = hlz.hypercore.signing;
const sig = try signing.signOrder(signer, batch, nonce, .mainnet, null, null);
// sig.r, sig.s, sig.v ready for JSON: {"r":"0x...","s":"0x...","v":27}Typed Data Path (Transfers, Approvals)
Used for: sendUsdc, spotSend, sendAsset, withdraw, usdClassTransfer, approveAgent, approveBuilderFee, tokenDelegate, userDexAbstraction, userSetAbstraction, convertToMultiSig
Fields → EIP-712 struct hash → Arbitrum domain (chainId 42161 mainnet / 421614 testnet)const sig = try signing.signUsdSend(signer, destination, amount, nonce, .mainnet);The Signer
const Signer = hlz.crypto.signer.Signer;
// From hex string (64 chars, no 0x prefix)
const signer = try Signer.fromHex("abcdef1234...");
// The signer holds the private key and can produce ECDSA signatures
// It uses RFC 6979 deterministic nonces (no randomness needed)Chain Enum
const Chain = signing.Chain;
Chain.mainnet // Arbitrum One (42161) for typed data, 1337 for agent
Chain.testnet // Arbitrum Sepolia (421614) for typed data, 1337 for agentMessagePack Compatibility
The msgpack encoding must be byte-exact with Rust's rmp-serde::to_vec_named. This means:
- Named fields (not positional)
- Specific integer encoding widths
- Map ordering matches Rust struct field order
- The
typefield is embedded inside the map (serde#[serde(tag = "type")])
EIP-712 Details
All 14 EIP-712 type hashes are computed at compile time — no runtime string hashing or allocation.
Crypto Backend
By default, hlz uses Zig's stdlib secp256k1 (constant-time, safe for servers). For latency-sensitive use cases, opt into the custom GLV backend:
zig build -Dfast-crypto=true # ~3.4x faster signing| Backend | Median sign_order | Use case |
|---|---|---|
| stdlib (default) | ~112 µs | Servers, production |
| custom GLV | ~34 µs | Trading bots, low-latency |
The domain separator uses:
name = "Exchange"version = "1"chainId= 42161 (mainnet) or 421614 (testnet) for typed datachainId= 1337 for agent-signed actions (orders, cancels)verifyingContract = 0x0000000000000000000000000000000000000000