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

Signing pattern library for exchange authentication.

Provides a unified interface for signing API requests across 100+
cryptocurrency exchanges. Instead of per-exchange signing code, 9
parameterized patterns cover 95%+ of all exchanges:

| Pattern | Exchanges | Description |
|---------|-----------|-------------|
| `:hmac_sha256_query` | ~40 | Binance-style: sign query string |
| `:hmac_sha256_headers` | ~30 | Bybit-style: sign body, headers |
| `:hmac_sha256_iso_passphrase` | ~10 | OKX-style: ISO timestamp + passphrase |
| `:hmac_sha256_passphrase_signed` | ~3 | KuCoin-style: HMAC-signed passphrase |
| `:hmac_sha512_nonce` | ~3 | Kraken-style: SHA512 + nonce + base64 secret |
| `:hmac_sha512_gate` | 1 | Gate.io-style: SHA512 + newline payload |
| `:hmac_sha384_payload` | ~3 | Bitfinex-style: payload signing |
| `:deribit` | 1 | Custom Authorization header format |
| `:custom` | <5% | Escape hatch for edge cases |

## Usage

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

## Custom Signing Patterns

Implement `CCXT.Signing.Behaviour` and use the `:custom` pattern:

    defmodule MyApp.Signing.MyExchange do
      @behaviour CCXT.Signing.Behaviour

      @impl true
      def sign(request, credentials, config) do
        %{url: request.path, method: request.method, headers: [], body: request.body}
      end
    end

See `CCXT.Signing.Behaviour` for the full contract.

# `config`

```elixir
@type config() :: %{
  optional(:api_key_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,
  optional(:custom_module) =&gt; module(),
  optional(atom()) =&gt; term()
}
```

# `method`

```elixir
@type method() :: :get | :post | :put | :delete
```

# `pattern`

```elixir
@type pattern() ::
  :hmac_sha256_query
  | :hmac_sha256_headers
  | :hmac_sha256_iso_passphrase
  | :hmac_sha256_passphrase_signed
  | :hmac_sha512_nonce
  | :hmac_sha512_gate
  | :hmac_sha384_payload
  | :deribit
  | :custom
```

# `request`

```elixir
@type request() :: %{
  method: method(),
  path: String.t(),
  body: String.t() | nil,
  params: map()
}
```

# `signed_request`

```elixir
@type signed_request() :: %{
  url: String.t(),
  method: method(),
  headers: [{String.t(), String.t()}],
  body: String.t() | nil
}
```

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

# `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(), CCXT.Credentials.t(), config()) :: signed_request()
```

Signs a request using the specified pattern and configuration.

## Parameters

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

## Returns

A signed request map 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()
```

Returns `config[:timestamp_ms_override]` when set, otherwise current wall
time in milliseconds. Used by pattern modules so fixture replay (T65) can
inject the frozen timestamp CCXT JS signed with.

# `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()
```

Like `timestamp_ms_from_config/1` but in seconds.

# `urlencode`

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

URL-encodes params as a sorted query string.

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