Current section
Files
Jump to
Current section
Files
lib/http/http.ex
defmodule Polymarket.Http do
@moduledoc """
Shared `Req`-based HTTP plumbing for the Polymarket API clients
(`Polymarket.Gamma`, `Polymarket.Clob`).
Each client passes itself as the `owner` so that, in tests, requests route
through that client's own `Req.Test` stub (see `req_options/1`).
"""
@typedoc "An HTTP header as a `{name, value}` pair."
@type header :: {String.t(), String.t()}
@doc """
Issue a `GET` request and return the decoded JSON body on a `200` response.
`params` are sent as query parameters; array-valued params are expanded into
repeated keys (`clob_token_ids: ["1", "2"]` -> `?clob_token_ids=1&clob_token_ids=2`)
since `Req` rejects list values. `headers` are extra request headers (e.g. the
`POLY_*` authentication headers). `owner` is the calling client module, used to
scope the `Req.Test` stub during tests.
"""
@spec get(String.t(), keyword(), module(), [header()]) ::
{:ok, term()} | {:error, :failed_to_get}
def get(url, params, owner, headers \\ []) do
expanded =
Enum.flat_map(params, fn
{key, values} when is_list(values) -> Enum.map(values, &{key, &1})
pair -> [pair]
end)
options =
[decode_json: [keys: :atoms], params: expanded, headers: headers]
|> Keyword.merge(req_options(owner))
case Req.get!(url, options) do
%{status: 200, body: body} -> {:ok, body}
_ -> {:error, :failed_to_get}
end
end
@doc """
Issue a `POST` request with a raw `body`, returning the decoded JSON body on a
`2xx` response and `{:error, {status, body}}` otherwise.
`body` is sent verbatim — callers that authenticate the request (L2 HMAC) must
sign exactly these bytes, so the body is never re-encoded here. A
`content-type: application/json` header is added for non-empty bodies. `headers`
are extra request headers and `owner` scopes the `Req.Test` stub during tests.
Unlike `get/4`, a non-2xx surfaces the status and decoded body so callers can
report the server's error (e.g. an order rejection) rather than a bare atom. A
transport failure (no HTTP response) is returned as `{:error, {0, message}}`
rather than raised, so callers' `{:ok, _} | {:error, _}` contracts hold.
"""
@spec post(String.t(), binary(), module(), [header()]) ::
{:ok, term()} | {:error, {non_neg_integer(), term()}}
def post(url, body, owner, headers \\ []) do
options =
[decode_json: [keys: :atoms], body: body, headers: content_type(body, headers)]
|> Keyword.merge(req_options(owner))
case Req.post(url, options) do
{:ok, %{status: status, body: resp}} when status in 200..299 -> {:ok, resp}
{:ok, %{status: status, body: resp}} -> {:error, {status, resp}}
{:error, exception} -> {:error, {0, Exception.message(exception)}}
end
end
# A JSON body needs its content type; an empty body (e.g. L1 `create_api_key`)
# carries no content type.
@spec content_type(binary(), [header()]) :: [header()]
defp content_type("", headers), do: headers
defp content_type(_body, headers), do: [{"content-type", "application/json"} | headers]
# Base options from config, plus the per-owner `Req.Test` plug when testing.
@spec req_options(module()) :: keyword()
defp req_options(owner) do
base = Application.get_env(:ex_polymarket, :req_options, [])
if Application.get_env(:ex_polymarket, :http_test_mode, false) do
Keyword.put(base, :plug, {Req.Test, owner})
else
base
end
end
end