Current section
Files
Jump to
Current section
Files
lib/ccxt/signing/hmac_sha256_headers.ex
defmodule CCXT.Signing.HmacSha256Headers do
@moduledoc """
HMAC-SHA256 headers signing pattern (Bybit-style).
Used by: Bybit, Bitget, Phemex, Poloniex, and ~30 other exchanges.
## How it works
1. Create a payload string: `timestamp + apiKey + recvWindow + body_or_query`
2. Sign with HMAC-SHA256 using the secret
3. Add signature and auth headers to request
## Configuration
signing: %{
pattern: :hmac_sha256_headers,
api_key_header: "X-BAPI-API-KEY",
timestamp_header: "X-BAPI-TIMESTAMP",
signature_header: "X-BAPI-SIGN",
recv_window_header: "X-BAPI-RECV-WINDOW", # optional
recv_window: 5000, # optional, default 5000ms
signature_encoding: :hex # or :base64
}
## Payload Construction
For GET requests: `timestamp + apiKey + recvWindow + queryString`
For POST requests: `timestamp + apiKey + recvWindow + body`
"""
@behaviour CCXT.Signing.Behaviour
alias CCXT.Credentials
alias CCXT.Defaults
alias CCXT.Signing
@doc """
Signs a request using the HMAC-SHA256 headers pattern.
"""
@impl true
@spec sign(Signing.request(), Credentials.t(), Signing.config()) :: Signing.signed_request()
def sign(request, credentials, config) do
timestamp = to_string(Signing.timestamp_ms())
recv_window = config |> Map.get(:recv_window, Defaults.recv_window_ms()) |> to_string()
# Build the payload to sign
{query_string, body} = build_query_and_body(request)
payload =
case request.method do
:get -> timestamp <> credentials.api_key <> recv_window <> query_string
:delete -> timestamp <> credentials.api_key <> recv_window <> query_string
_post_or_put -> timestamp <> credentials.api_key <> recv_window <> (body || "")
end
# Sign the payload
signature = sign_payload(payload, credentials.secret, config)
# Build headers
headers = build_headers(credentials, timestamp, signature, recv_window, config)
# Build URL
url =
case request.method do
method when method in [:get, :delete] and query_string != "" ->
request.path <> "?" <> query_string
_ ->
request.path
end
%{
url: url,
method: request.method,
headers: headers,
body: body
}
end
defp build_query_and_body(%{params: params, body: body, method: method}) do
cond do
method in [:get, :delete] ->
query_string = Signing.urlencode_raw(params)
{query_string, nil}
body != nil ->
# Body already provided (e.g., JSON string)
{"", body}
params != %{} ->
# Convert params to JSON body
json_body = Jason.encode!(params)
{"", json_body}
true ->
{"", nil}
end
end
defp sign_payload(payload, secret, config) do
signature_bytes = Signing.hmac_sha256(payload, 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, timestamp, signature, recv_window, config) do
api_key_header = Map.get(config, :api_key_header, "X-BAPI-API-KEY")
timestamp_header = Map.get(config, :timestamp_header, "X-BAPI-TIMESTAMP")
signature_header = Map.get(config, :signature_header, "X-BAPI-SIGN")
headers = [
{api_key_header, credentials.api_key},
{timestamp_header, timestamp},
{signature_header, signature},
{"Content-Type", "application/json"}
]
# Add recv_window header if configured
case Map.get(config, :recv_window_header) do
nil -> headers
header_name -> [{header_name, recv_window} | headers]
end
end
end