lib/binance/rest/http_client.ex

defmodule Binance.Rest.HTTPClient do
  require Logger

  def get_binance(url, headers \\ [], options \\ []) do
    HTTPoison.get("#{url}", headers, options)
    |> parse_response
  end

  def delete_binance(url, headers \\ []) do
    HTTPoison.delete("#{url}", headers)
    |> parse_response
  end

  def get_binance(url, params, secret_key, api_key) do
    case prepare_request(url, params, secret_key, api_key) do
      {:error, _} = error ->
        error

      {:ok, url, headers} ->
        get_binance(url, headers, Map.get(params, "request_opts", []))
    end
  end

  def delete_binance(url, params, secret_key, api_key) do
    case prepare_request(url, params, secret_key, api_key) do
      {:error, _} = error ->
        error

      {:ok, url, headers} ->
        delete_binance(url, headers)
    end
  end

  defp prepare_request(url, params, secret_key, api_key) do
    case validate_credentials(secret_key, api_key) do
      {:error, _} = error ->
        error

      _ ->
        headers = [{"X-MBX-APIKEY", api_key}]
        receive_window = 5000
        ts = DateTime.utc_now() |> DateTime.to_unix(:millisecond)

        params =
          Map.merge(params, %{
            timestamp: ts,
            recvWindow: receive_window
          })

        argument_string = URI.encode_query(params)

        signature =
          generate_signature(
            :sha256,
            secret_key,
            argument_string
          )
          |> Base.encode16()

        {:ok, "#{url}?#{argument_string}&signature=#{signature}", headers}
    end
  end

  def signed_request_binance(url, params, method, api_secret, api_key, opts \\ []) do
    argument_string =
      params
      |> prepare_query_params()

    # generate signature
    signature =
      generate_signature(
        :sha256,
        api_secret,
        argument_string
      )
      |> Base.encode16()

    body = "#{argument_string}&signature=#{signature}"

    case apply(HTTPoison, method, [
           "#{url}",
           body,
           [
             {"X-MBX-APIKEY", api_key},
             {"Content-type", "application/x-www-form-urlencoded"}
           ],
           opts
         ]) do
      {:error, err} ->
        {:error, {:http_error, err}}

      {:ok, response} ->
        case Poison.decode(response.body) do
          {:ok, data} -> {:ok, data}
          {:error, err} -> {:error, {:poison_decode_error, err}}
        end
    end
  end

  def signed_querystring_request_binance(
        url,
        params,
        method,
        api_secret,
        api_key
      ) do
    argument_string =
      params
      |> prepare_query_params()

    # generate signature
    signature =
      generate_signature(
        :sha256,
        api_secret,
        argument_string
      )
      |> Base.encode16()

    query = "#{argument_string}&signature=#{signature}"

    case apply(HTTPoison, method, [
           "#{url}?#{query}",
           [
             {"X-MBX-APIKEY", api_key}
           ]
         ]) do
      {:error, err} ->
        {:error, {:http_error, err}}

      {:ok, response} ->
        case Poison.decode(response.body) do
          {:ok, data} -> {:ok, data}
          {:error, err} -> {:error, {:poison_decode_error, err}}
        end
    end
  end

  @doc """
  You need to send an empty body and the api key
  to be able to create a new listening key.

  """
  def unsigned_request_binance(url, data, method) do
    headers = [
      {"X-MBX-APIKEY", Application.get_env(:binance_futures, :api_key)}
    ]

    case do_unsigned_request(url, data, method, headers) do
      {:error, err} ->
        {:error, {:http_error, err}}

      {:ok, response} ->
        case Poison.decode(response.body) do
          {:ok, data} -> {:ok, data}
          {:error, err} -> {:error, {:poison_decode_error, err}}
        end
    end
  end

  defp do_unsigned_request(url, nil, method, headers) do
    apply(HTTPoison, method, [
      "#{url}",
      headers
    ])
  end

  defp do_unsigned_request(url, data, :get, headers) do
    argument_string =
      data
      |> prepare_query_params()

    apply(HTTPoison, :get, [
      "#{url}" <> "?#{argument_string}",
      headers
    ])
  end

  defp do_unsigned_request(url, body, method, headers) do
    apply(HTTPoison, method, [
      "#{url}",
      body,
      headers
    ])
  end

  defp validate_credentials(nil, nil),
    do: {:error, {:config_missing, "Secret and API key missing"}}

  defp validate_credentials(nil, _api_key),
    do: {:error, {:config_missing, "Secret key missing"}}

  defp validate_credentials(_secret_key, nil),
    do: {:error, {:config_missing, "API key missing"}}

  defp validate_credentials(_secret_key, _api_key),
    do: :ok

  defp parse_response({:ok, response}) do
    response.body
    |> Poison.decode()
    |> parse_response_body
  end

  defp parse_response({:error, err}) do
    {:error, {:http_error, err}}
  end

  defp parse_response_body({:ok, data}) do
    case data do
      %{"code" => 200, "msg" => ""} = data -> {:ok, data}
      %{"code" => _c, "msg" => _m} = error -> {:error, error}
      _ -> {:ok, data}
    end
  end

  defp parse_response_body({:error, err}) do
    {:error, {:poison_decode_error, err}}
  end

  defp prepare_query_params(params) do
    params
    |> Map.to_list()
    |> Enum.map(fn x -> Tuple.to_list(x) |> Enum.join("=") end)
    |> Enum.join("&")
  end

  defp generate_signature(digest, key, argument_string),
    do: :crypto.mac(:hmac, digest, key, argument_string)
end