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 hmac_sha384_payload.ex
Raw

lib/ccxt/signing/hmac_sha384_payload.ex

defmodule CCXT.Signing.HmacSha384Payload do
@moduledoc """
HMAC-SHA384 payload signing pattern (Bitfinex/Gemini-style).
Used by: Bitfinex, Gemini, and ~3 other exchanges.
Two variants selected via `:variant` config key:
**Bitfinex** (default): `/api/path + nonce + body` → HMAC-SHA384 → hex
**Gemini**: `base64(json(payload))` → HMAC-SHA384 → hex, payload in header
## Configuration
signing: %{
pattern: :hmac_sha384_payload,
variant: :bitfinex,
api_key_header: "bfx-apikey",
signature_header: "bfx-signature",
nonce_header: "bfx-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
nonce =
to_string(
Signing.nonce_from_config(config, fn ->
:erlang.unique_integer([:positive, :monotonic])
end)
)
case Map.get(config, :variant, :bitfinex) do
:bitfinex -> sign_bitfinex(request, credentials, config, nonce)
:gemini -> sign_gemini(request, credentials, config, nonce)
end
end
# Bitfinex signs the body content. When caller provides a pre-serialized body,
# use it directly (consistent with Headers/Iso/Kucoin modules). When body is nil,
# serialize from params — this is the normal path for most callers.
defp sign_bitfinex(request, credentials, config, nonce) do
body = request.body || encode_body(request.params)
auth_path = "/api" <> request.path
auth = auth_path <> nonce <> body
signature = sign_payload(auth, credentials.secret)
headers = build_bitfinex_headers(credentials, nonce, signature, config)
%{url: request.path, method: request.method, headers: headers, body: body}
end
defp sign_gemini(request, credentials, config, nonce) do
payload_map =
request.params
|> Map.put("request", request.path)
|> Map.put("nonce", nonce)
payload_b64 = payload_map |> Jason.encode!() |> Signing.encode_base64()
signature = sign_payload(payload_b64, credentials.secret)
headers = build_gemini_headers(credentials, payload_b64, signature, config)
%{url: request.path, method: request.method, headers: headers, body: nil}
end
defp encode_body(params) when params == %{}, do: "{}"
defp encode_body(params), do: Jason.encode!(params)
defp sign_payload(data, secret) do
data
|> Signing.hmac_sha384(secret)
|> Signing.encode_hex()
end
defp build_bitfinex_headers(credentials, nonce, signature, config) do
api_key_header = Map.get(config, :api_key_header, "bfx-apikey")
signature_header = Map.get(config, :signature_header, "bfx-signature")
nonce_header = Map.get(config, :nonce_header, "bfx-nonce")
[
{api_key_header, credentials.api_key},
{nonce_header, nonce},
{signature_header, signature},
{"Content-Type", "application/json"}
]
end
defp build_gemini_headers(credentials, payload_b64, signature, config) do
api_key_header = Map.get(config, :api_key_header, "X-GEMINI-APIKEY")
payload_header = Map.get(config, :payload_header, "X-GEMINI-PAYLOAD")
signature_header = Map.get(config, :signature_header, "X-GEMINI-SIGNATURE")
[
{api_key_header, credentials.api_key},
{payload_header, payload_b64},
{signature_header, signature},
{"Content-Type", "text/plain"}
]
end
end