Current section
Files
Jump to
Current section
Files
lib/ccxt/signing/hmac_sha512_nonce.ex
defmodule CCXT.Signing.HmacSha512Nonce do
@moduledoc """
HMAC-SHA512 with nonce signing pattern (Kraken-style).
Used by: Kraken and ~20 other exchanges.
## How it works
1. Generate incrementing nonce (microsecond timestamp)
2. Add nonce to request body
3. Hash: `sha256(nonce_string + body)`
4. Concat: `url_path_bytes + hash`
5. Sign: `hmac_sha512(concat, base64_decode(secret))`
6. Encode as Base64
## Configuration
signing: %{
pattern: :hmac_sha512_nonce,
api_key_header: "API-Key",
signature_header: "API-Sign",
nonce_key: "nonce"
}
"""
@behaviour CCXT.Signing.Behaviour
alias CCXT.Credentials
alias CCXT.Signing
@impl true
@spec sign(Signing.request(), Credentials.t(), Signing.config()) :: Signing.signed_request()
def sign(request, credentials, config) do
# Kraken requires nonces to be always-increasing per API key across BEAM restarts.
# System.system_time(:microsecond) survives restarts; :erlang.unique_integer does not.
nonce = Signing.nonce_from_config(config, fn -> System.system_time(:microsecond) end)
nonce_key = Map.get(config, :nonce_key, "nonce")
body_params = Map.put(request.params, nonce_key, nonce)
body = encode_body(body_params, config)
# sha256(nonce_string + body) → concat with path → hmac_sha512 with decoded secret
nonce_string = to_string(nonce)
hash = Signing.sha256(nonce_string <> body)
message = request.path <> hash
secret_decoded = Signing.decode_base64(credentials.secret)
signature =
message
|> Signing.hmac_sha512(secret_decoded)
|> Signing.encode_base64()
headers = build_headers(credentials, signature, config)
%{url: request.path, method: request.method, headers: headers, body: body}
end
defp encode_body(params, config) do
case Map.get(config, :body_encoding, :urlencoded) do
:urlencoded -> Signing.urlencode(params)
:json -> Jason.encode!(params)
end
end
defp build_headers(credentials, signature, config) do
api_key_header = Map.get(config, :api_key_header, "API-Key")
signature_header = Map.get(config, :signature_header, "API-Sign")
content_type =
case Map.get(config, :body_encoding, :urlencoded) do
:urlencoded -> "application/x-www-form-urlencoded"
:json -> "application/json"
end
[
{api_key_header, credentials.api_key},
{signature_header, signature},
{"Content-Type", content_type}
]
end
end