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

Signing pattern library for exchange authentication.

Provides a unified interface for signing API requests across the in-scope
exchange registry. Authored HMAC recipes cover the common exchange families,
with first-party modules for venue-specific signers:

| Pattern | In-scope use | Description |
|---------|-----------|-------------|
| `:hmac_sha256_query` | Binance family | Sign query string |
| `:hmac_sha256_headers` | Bybit-style | Sign body/headers |
| `:hmac_sha256_iso_passphrase` | OKX-style | ISO timestamp + passphrase |
| `:deribit` | Deribit | Custom Authorization header format |
| `:hyperliquid` | Hyperliquid | EIP-712 / action signing |
| `:derive` | Derive | EIP-712 order/message signing |
| `:lighter` | Lighter | Isolated official zk-Schnorr signer |
| `:api_key_secret_headers` | Alpaca-style | API key and secret headers |

## Usage

    signed = Bourse.Signing.sign(
      :hmac_sha256_headers,
      %Bourse.Signing.Request{method: :get, path: "/v5/account/wallet-balance"},
      credentials,
      signing_config
    )

Pattern selection is authored in each supported runtime spec. Consumers
cannot register reference-only exchanges or inject long-tail signers.

# `config`

```elixir
@type config() :: %{
  optional(:api_key_header) =&gt; String.t(),
  optional(:secret_header) =&gt; String.t(),
  optional(:timestamp_header) =&gt; String.t(),
  optional(:signature_header) =&gt; String.t(),
  optional(:passphrase_header) =&gt; String.t(),
  optional(:recv_window_header) =&gt; String.t(),
  optional(:recv_window) =&gt; non_neg_integer(),
  optional(:signature_encoding) =&gt; :hex | :base64 | :url,
  optional(:sign_recipe) =&gt; map(),
  optional(:sign_recipe_section) =&gt; String.t(),
  optional(atom()) =&gt; term()
}
```

# `method`

```elixir
@type method() :: Bourse.Signing.Request.method()
```

# `pattern`

```elixir
@type pattern() ::
  :hmac_sha256_query
  | :hmac_sha256_headers
  | :hmac_sha256_iso_passphrase
  | :deribit
  | :hyperliquid
  | :derive
  | :lighter
  | :api_key_secret_headers
```

# `request`

```elixir
@type request() :: Bourse.Signing.Request.t()
```

# `signed_request`

```elixir
@type signed_request() :: Bourse.Signing.SignedRequest.t()
```

# `decode_base64`

```elixir
@spec decode_base64(String.t()) :: binary()
```

Decodes a Base64-encoded string. Raises on invalid input.

# `encode_base64`

```elixir
@spec encode_base64(binary()) :: String.t()
```

Base64-encodes a binary.

# `encode_hex`

```elixir
@spec encode_hex(binary()) :: String.t()
```

Lowercase hex-encodes a binary.

# `encode_query_pairs`

```elixir
@spec encode_query_pairs(
  [{term(), term()}],
  keyword()
) :: String.t()
```

Encodes an ordered list of `{key, value}` pairs as a query string.

Percent-encodes keys and values the same way as `urlencode/1` (space → `%20`,
not `+`). See that function's moduledoc for the venue-doc rationale.

## Options

- `:array_style` — `:brackets` (default, `key[]=v`) or `:repeat` (`key=v` repeated)

# `hmac_sha256`

```elixir
@spec hmac_sha256(iodata(), iodata()) :: binary()
```

HMAC-SHA256 of `data` with `secret`.

# `hmac_sha384`

```elixir
@spec hmac_sha384(iodata(), iodata()) :: binary()
```

HMAC-SHA384 of `data` with `secret`.

# `hmac_sha512`

```elixir
@spec hmac_sha512(iodata(), iodata()) :: binary()
```

HMAC-SHA512 of `data` with `secret`.

# `module_for_pattern`

```elixir
@spec module_for_pattern(pattern()) :: module() | nil
```

Returns the signing module for a given pattern.

# `nonce_from_config`

```elixir
@spec nonce_from_config(map(), (-&gt; integer())) :: integer()
```

Returns `config[:nonce_override]` when set, otherwise invokes `fallback`.
Fallback is a zero-arity function so callers control nonce semantics
(e.g. `:erlang.unique_integer` vs `System.system_time(:microsecond)`).

# `pattern?`

```elixir
@spec pattern?(atom()) :: boolean()
```

Checks if a pattern is supported.

# `patterns`

```elixir
@spec patterns() :: [pattern()]
```

Returns the list of supported signing patterns.

# `sha256`

```elixir
@spec sha256(iodata()) :: binary()
```

SHA-256 hash of `data`.

# `sha512`

```elixir
@spec sha512(iodata()) :: binary()
```

SHA-512 hash of `data`.

# `sign`

```elixir
@spec sign(pattern(), request(), Bourse.Credentials.t(), config()) ::
  signed_request()
  | {:error, {:unsupported_signing, term()} | {:lighter_signing, term()}}
```

Signs a request using the specified pattern and configuration.

## Parameters

- `pattern` - The signing pattern atom (e.g., `:hmac_sha256_headers`)
- `request` - `Bourse.Signing.Request` or equivalent map (`:method`, `:path`, `:body`, `:params`)
- `credentials` - `Bourse.Credentials` struct with API key and secret
- `config` - Pattern-specific configuration from the exchange spec

## Returns

A `Bourse.Signing.SignedRequest` with `:url`, `:method`, `:headers`, and `:body`.

# `timestamp_iso8601`

```elixir
@spec timestamp_iso8601() :: String.t()
```

Current UTC time as ISO 8601 string, truncated to millisecond precision.

# `timestamp_iso8601_from_config`

```elixir
@spec timestamp_iso8601_from_config(map()) :: String.t()
```

Like `timestamp_ms_from_config/1` but rendered as ISO 8601 UTC.

# `timestamp_ms`

```elixir
@spec timestamp_ms() :: non_neg_integer()
```

Current UTC time in milliseconds.

# `timestamp_ms_from_config`

```elixir
@spec timestamp_ms_from_config(map()) :: non_neg_integer() | String.t()
```

Returns a preformatted `config[:timestamp]` first, then
`config[:timestamp_ms_override]`, otherwise current wall time in milliseconds.
Used by pattern modules so dispatch can provide the v4 sign recipe timestamp
and signature-vector tests can inject a frozen provider timestamp.

# `timestamp_seconds`

```elixir
@spec timestamp_seconds() :: non_neg_integer()
```

Current UTC time in seconds.

# `timestamp_seconds_from_config`

```elixir
@spec timestamp_seconds_from_config(map()) :: non_neg_integer() | String.t()
```

Like `timestamp_ms_from_config/1` but in seconds.

# `urlencode`

```elixir
@spec urlencode(map() | keyword() | [{term(), term()}] | nil) :: String.t()
```

URL-encodes params as a sorted query string.

Uses RFC 3986 percent-encoding via `URI.encode/2` with `URI.char_unreserved?/1`
— **spaces become `%20`**, never the `application/x-www-form-urlencoded` `+`
that Elixir's `URI.encode_query/1` emits. Venue authority for the default:
Huobi/HTX spot signing docs require URL-encoded query params with uppercase
hex and show the space as `%20` (not `+`) —
https://huobiapi.github.io/docs/spot/v1/en/#authentication (Signature
Method). Hex digits from `URI.encode/2` are
uppercase, matching the same Huobi rule. No first-class venue has been
observed to require www-form `+` for the signed canonical query — if one
does, register a per-venue carve rather than reverting this default (see
`docs/authored-specs.md` C21).

List-valued scalar params use empty-bracket keys
(`%{"ids" => ["a", "b"]}` → `"ids%5B%5D=a&ids%5B%5D=b"`). That is the form
Deribit's JSON-RPC-over-GET accepts live (OpenAPI lists `style=form,
explode=true`, but the live parser only materializes a list for `key[]=…`;
Bracket-index `ids[0]=…` is rejected with `value required`, and bare repeated
keys with `value must be a list`. Nested list/map items raise
`ArgumentError` naming the param — never `URI.encode_query`'s opaque list crash.

Recipe venues that need plain repeated keys use
`urlencodeWithArrayRepeat` via `encode_query_pairs/2`.

# `urlencode_raw`

```elixir
@spec urlencode_raw(map() | nil) :: String.t()
```

URL-encodes params as a sorted query string without percent-encoding values.

---

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