Packages

Elixir client library for cryptocurrency exchanges — generated from CCXT specs via compile-time macros.

Current section

Files

Jump to
ccxt_client lib ccxt signing.ex
Raw

lib/ccxt/signing.ex

defmodule CCXT.Signing do
@moduledoc """
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.
"""
alias CCXT.Credentials
alias CCXT.Signing.Custom
alias CCXT.Signing.Deribit
alias CCXT.Signing.HmacSha256Headers
alias CCXT.Signing.HmacSha256Iso
alias CCXT.Signing.HmacSha256Kucoin
alias CCXT.Signing.HmacSha256Query
alias CCXT.Signing.HmacSha384Payload
alias CCXT.Signing.HmacSha512Gate
alias CCXT.Signing.HmacSha512Nonce
@type method :: :get | :post | :put | :delete
@type request :: %{
method: method(),
path: String.t(),
body: String.t() | nil,
params: map()
}
@type signed_request :: %{
url: String.t(),
method: method(),
headers: [{String.t(), String.t()}],
body: String.t() | nil
}
@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
@type config :: %{
optional(:api_key_header) => String.t(),
optional(:timestamp_header) => String.t(),
optional(:signature_header) => String.t(),
optional(:passphrase_header) => String.t(),
optional(:recv_window_header) => String.t(),
optional(:recv_window) => non_neg_integer(),
optional(:signature_encoding) => :hex | :base64,
optional(:custom_module) => module(),
optional(atom()) => term()
}
# --- Dispatcher (function-head routing) ---
@doc """
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`.
"""
@spec sign(pattern(), request(), Credentials.t(), config()) :: signed_request()
def sign(:hmac_sha256_query, request, credentials, config) do
HmacSha256Query.sign(request, credentials, config)
end
def sign(:hmac_sha256_headers, request, credentials, config) do
HmacSha256Headers.sign(request, credentials, config)
end
def sign(:hmac_sha256_iso_passphrase, request, credentials, config) do
HmacSha256Iso.sign(request, credentials, config)
end
def sign(:hmac_sha256_passphrase_signed, request, credentials, config) do
HmacSha256Kucoin.sign(request, credentials, config)
end
def sign(:hmac_sha512_nonce, request, credentials, config) do
HmacSha512Nonce.sign(request, credentials, config)
end
def sign(:hmac_sha512_gate, request, credentials, config) do
HmacSha512Gate.sign(request, credentials, config)
end
def sign(:hmac_sha384_payload, request, credentials, config) do
HmacSha384Payload.sign(request, credentials, config)
end
def sign(:deribit, request, credentials, config) do
Deribit.sign(request, credentials, config)
end
def sign(:custom, request, credentials, config) do
Custom.sign(request, credentials, config)
end
# --- Introspection ---
@doc """
Returns the list of supported signing patterns.
"""
@spec patterns() :: [pattern()]
def patterns do
[
: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
]
end
@doc """
Checks if a pattern is supported.
"""
@spec pattern?(atom()) :: boolean()
def pattern?(pattern), do: pattern in patterns()
@doc """
Returns the signing module for a given pattern.
"""
@spec module_for_pattern(pattern()) :: module() | nil
def module_for_pattern(:hmac_sha256_query), do: HmacSha256Query
def module_for_pattern(:hmac_sha256_headers), do: HmacSha256Headers
def module_for_pattern(:hmac_sha256_iso_passphrase), do: HmacSha256Iso
def module_for_pattern(:hmac_sha256_passphrase_signed), do: HmacSha256Kucoin
def module_for_pattern(:hmac_sha512_nonce), do: HmacSha512Nonce
def module_for_pattern(:hmac_sha512_gate), do: HmacSha512Gate
def module_for_pattern(:hmac_sha384_payload), do: HmacSha384Payload
def module_for_pattern(:deribit), do: Deribit
def module_for_pattern(:custom), do: Custom
def module_for_pattern(_), do: nil
# --- Shared Crypto Helpers ---
# Used by signing pattern modules for building signatures.
@doc "Current UTC time in milliseconds."
@spec timestamp_ms() :: non_neg_integer()
def timestamp_ms, do: System.system_time(:millisecond)
@doc "Current UTC time in seconds."
@spec timestamp_seconds() :: non_neg_integer()
def timestamp_seconds, do: System.system_time(:second)
@doc "Current UTC time as ISO 8601 string, truncated to millisecond precision."
@spec timestamp_iso8601() :: String.t()
def timestamp_iso8601 do
DateTime.utc_now()
|> DateTime.truncate(:millisecond)
|> DateTime.to_iso8601()
end
# Deterministic-override helpers.
# Production callers never set `:timestamp_ms_override` / `:nonce_override` in
# config, so behavior is unchanged. Only the fixture-replay test generator
# injects these to reproduce CCXT JS signing output byte-for-byte.
@doc """
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.
"""
@spec timestamp_ms_from_config(map()) :: non_neg_integer()
def timestamp_ms_from_config(config) do
case Map.get(config, :timestamp_ms_override) do
nil -> timestamp_ms()
ms when is_integer(ms) -> ms
end
end
@doc "Like `timestamp_ms_from_config/1` but in seconds."
@spec timestamp_seconds_from_config(map()) :: non_neg_integer()
def timestamp_seconds_from_config(config) do
case Map.get(config, :timestamp_ms_override) do
nil -> timestamp_seconds()
ms when is_integer(ms) -> div(ms, 1000)
end
end
@doc "Like `timestamp_ms_from_config/1` but rendered as ISO 8601 UTC."
@spec timestamp_iso8601_from_config(map()) :: String.t()
def timestamp_iso8601_from_config(config) do
case Map.get(config, :timestamp_ms_override) do
nil ->
timestamp_iso8601()
ms when is_integer(ms) ->
ms
|> DateTime.from_unix!(:millisecond)
|> DateTime.truncate(:millisecond)
|> DateTime.to_iso8601()
end
end
@doc """
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)`).
"""
@spec nonce_from_config(map(), (-> integer())) :: integer()
def nonce_from_config(config, fallback) when is_function(fallback, 0) do
case Map.get(config, :nonce_override) do
nil -> fallback.()
n when is_integer(n) -> n
end
end
@doc "HMAC-SHA256 of `data` with `secret`."
@spec hmac_sha256(iodata(), iodata()) :: binary()
def hmac_sha256(data, secret) do
:crypto.mac(:hmac, :sha256, secret, data)
end
@doc "HMAC-SHA384 of `data` with `secret`."
@spec hmac_sha384(iodata(), iodata()) :: binary()
def hmac_sha384(data, secret) do
:crypto.mac(:hmac, :sha384, secret, data)
end
@doc "HMAC-SHA512 of `data` with `secret`."
@spec hmac_sha512(iodata(), iodata()) :: binary()
def hmac_sha512(data, secret) do
:crypto.mac(:hmac, :sha512, secret, data)
end
@doc "SHA-256 hash of `data`."
@spec sha256(iodata()) :: binary()
def sha256(data) do
:crypto.hash(:sha256, data)
end
@doc "SHA-512 hash of `data`."
@spec sha512(iodata()) :: binary()
def sha512(data) do
:crypto.hash(:sha512, data)
end
@doc "Lowercase hex-encodes a binary."
@spec encode_hex(binary()) :: String.t()
def encode_hex(binary) do
Base.encode16(binary, case: :lower)
end
@doc "Base64-encodes a binary."
@spec encode_base64(binary()) :: String.t()
def encode_base64(binary) do
Base.encode64(binary)
end
@doc "Decodes a Base64-encoded string. Raises on invalid input."
@spec decode_base64(String.t()) :: binary()
def decode_base64(encoded) do
Base.decode64!(encoded)
end
@doc "URL-encodes params as a sorted query string."
@spec urlencode(map() | nil) :: String.t()
def urlencode(nil), do: ""
def urlencode(params) when params == %{}, do: ""
def urlencode(params) do
params
|> Enum.sort_by(fn {k, _v} -> to_string(k) end)
|> URI.encode_query()
end
@doc "URL-encodes params as a sorted query string without percent-encoding values."
@spec urlencode_raw(map() | nil) :: String.t()
def urlencode_raw(nil), do: ""
def urlencode_raw(params) when params == %{}, do: ""
def urlencode_raw(params) do
params
|> Enum.sort_by(fn {k, _v} -> to_string(k) end)
|> Enum.map_join("&", fn {k, v} -> "#{k}=#{v}" end)
end
end