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