Current section
Files
Jump to
Current section
Files
lib/ccxt/signing/hmac_sha256_query.ex
defmodule CCXT.Signing.HmacSha256Query do
@moduledoc """
HMAC-SHA256 query string signing pattern (Binance-style).
Used by: Binance, MEXC, Huobi, and ~40 other exchanges.
## How it works
1. Add timestamp to query parameters
2. URL-encode all parameters (sorted alphabetically)
3. Sign the query string with HMAC-SHA256
4. Append signature to URL as `&signature=...`
5. API key sent in header
## Configuration
signing: %{
pattern: :hmac_sha256_query,
api_key_header: "X-MBX-APIKEY",
timestamp_key: "timestamp",
signature_key: "signature",
recv_window_key: "recvWindow",
recv_window: 5000,
signature_encoding: :hex,
post_as_json: false
}
Optional: set `post_as_json: true` to send POST params as JSON body
instead of query string (used by some Binance-derivative exchanges).
"""
@behaviour CCXT.Signing.Behaviour
alias CCXT.Credentials
alias CCXT.Defaults
alias CCXT.Signing
@impl true
@spec sign(Signing.request(), Credentials.t(), Signing.config()) :: Signing.signed_request()
def sign(request, credentials, config) do
timestamp_key = Map.get(config, :timestamp_key, "timestamp")
signature_key = Map.get(config, :signature_key, "signature")
params = build_params(request.params, timestamp_key, config, Signing.timestamp_ms_from_config(config))
query_string = Signing.urlencode(params)
signature = sign_payload(query_string, credentials.secret, config)
final_query = query_string <> "&" <> signature_key <> "=" <> signature
{url, body} =
case request.method do
method when method in [:get, :delete] ->
{request.path <> "?" <> final_query, nil}
_post_or_put ->
if Map.get(config, :post_as_json, false) do
{request.path, Jason.encode!(Map.put(params, signature_key, signature))}
else
{request.path <> "?" <> final_query, request.body}
end
end
headers = build_headers(credentials, config)
%{url: url, method: request.method, headers: headers, body: body}
end
# Builds params with timestamp and optional recvWindow.
defp build_params(params, timestamp_key, config, timestamp_ms) do
params
|> Map.put(timestamp_key, timestamp_ms)
|> maybe_add_recv_window(config)
end
# Adds recvWindow only if configured, auto_recv_window is true, and not already present.
# Checks both string and atom key variants to prevent duplicate query params.
defp maybe_add_recv_window(params, config) do
recv_window_key = Map.get(config, :recv_window_key)
auto = Map.get(config, :auto_recv_window, false)
cond do
is_nil(recv_window_key) -> params
has_key_any_type?(params, recv_window_key) -> params
not auto -> params
true -> Map.put(params, recv_window_key, Map.get(config, :recv_window, Defaults.recv_window_ms()))
end
end
# Checks for a key as both string and atom, since callers may use either.
# Uses Enum.any? to check all keys converted to string, avoiding String.to_atom.
defp has_key_any_type?(params, key) when is_binary(key) do
Map.has_key?(params, key) or Enum.any?(Map.keys(params), fn k -> to_string(k) == key end)
end
defp has_key_any_type?(params, key), do: Map.has_key?(params, key)
defp sign_payload(query_string, secret, config) do
signature_bytes = Signing.hmac_sha256(query_string, secret)
case Map.get(config, :signature_encoding, :hex) do
:hex -> Signing.encode_hex(signature_bytes)
:base64 -> Signing.encode_base64(signature_bytes)
end
end
defp build_headers(credentials, config) do
api_key_header = Map.get(config, :api_key_header, "X-MBX-APIKEY")
[
{api_key_header, credentials.api_key},
{"Content-Type", "application/x-www-form-urlencoded"}
]
end
end