Bourse.Signing.Hyperliquid (bourse v0.1.0)

Copy Markdown View Source

First-party signing for Hyperliquid.

Hyperliquid signs two kinds of payloads, both EIP-712 typed data sealed with a secp256k1 signature (%{r, s, v} with v = 27 + recovery_id):

  • L1 actions (sign_l1_action/2) — order / cancel / transfer actions. The action is MessagePack-serialised (packb), concatenated with the nonce, optional vault address and optional expiresAfter, hashed with Keccak-256 into a connectionId, wrapped in an Agent phantom struct and signed under the Exchange domain (chainId: 1337).
  • User-signed actions (sign_user_signed_action/3) — USD transfers, withdrawals, approvals. Plain EIP-712 typed data under the HyperliquidSignTransaction domain (chainId: 421614), no msgpack.

Credentials

Hyperliquid authenticates with an EVM wallet, not an API key/secret pair. The EVM private key is carried in credentials.secret; the wallet address (when needed for vault threading) in credentials.api_key.

L1 action packing

pack_l1_action!/1 writes the field order from Hyperliquid's exchange docs and official Python SDK. The supported L1 actions cover orders, cancellations, isolated-margin updates, TWAPs, sub-account transfers, and vault transfers. Unknown shapes raise instead of falling back to unordered map serialization.

Summary

Functions

Computes the 32-byte Keccak-256 action hash (the EIP-712 connectionId).

Signs an L1 action, returning %{r, s, v} byte-equal to Bourse signL1Action.

Signs a user-signed action under the HyperliquidSignTransaction domain.

Functions

action_hash(action, vault_address, nonce, expires_after \\ nil)

@spec action_hash(map(), String.t() | nil, non_neg_integer(), non_neg_integer() | nil) ::
  binary()

Computes the 32-byte Keccak-256 action hash (the EIP-712 connectionId).

Computes keccak256(packb(action) ‖ nonce ‖ vault ‖ expiresAfter) as defined by Hyperliquid's signing contract. vault_address and expires_after are optional (nil to omit).

sign(request, credentials, config)

Bourse.Signing.Behaviour entry point.

Expects the L1 or user-signed action and nonce in request.params (:action/:nonce, with optional :vault_address/:expires_after). Produces the Hyperliquid POST /exchange envelope — %{"action", "nonce", "signature"} (+ vaultAddress) — as the JSON request body.

Unified callers receive a fully built :action from the internal Hyperliquid request-shape layer; raw callers may still hand-feed :action as an override.

sign_l1_action(action, nonce, opts)

@spec sign_l1_action(map(), non_neg_integer(), keyword()) ::
  Bourse.Signing.Crypto.signature()

Signs an L1 action, returning %{r, s, v} byte-equal to Bourse signL1Action.

Options:

  • :private_key (required) — 0x-prefixed EVM private key hex
  • :vault_address — vault / sub-account address (omitted when nil)
  • :expires_after — action expiry, ms (omitted when nil)
  • :testnettrue flips the phantom-agent source to "b" (default false)

sign_user_signed_action(message_types, message, opts)

@spec sign_user_signed_action(map(), map(), keyword()) ::
  Bourse.Signing.Crypto.signature()

Signs a user-signed action under the HyperliquidSignTransaction domain.

message_types is the EIP-712 type map (e.g. %{"HyperliquidTransaction:UsdSend" => [...]}); message carries the typed values. Returns %{r, s, v} byte-equal to Bourse signUserSignedAction.

Options: :private_key (required).