# `Bourse.Signing.Hyperliquid`
[🔗](https://github.com/ZenHive/bourse/blob/main/lib/bourse/signing/hyperliquid.ex#L1)

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.

# `action_hash`

```elixir
@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`

```elixir
@spec sign(Bourse.Signing.request(), Bourse.Credentials.t(), Bourse.Signing.config()) ::
  Bourse.Signing.signed_request()
```

`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`

```elixir
@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`)
- `:testnet` — `true` flips the phantom-agent `source` to `"b"` (default `false`)

# `sign_user_signed_action`

```elixir
@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).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
