Current section
Files
Jump to
Current section
Files
lib/clob/hmac_auth.ex
defmodule Polymarket.Clob.HmacAuth do
@moduledoc """
Polymarket CLOB **L2** (HMAC) authentication.
Once a wallet holds API credentials (`Polymarket.Schemas.Credentials`, obtained
via the L1 flow in `Polymarket.Clob.ClobAuth`), every authenticated request is
signed with an HMAC-SHA256 over `"{timestamp}{METHOD}{path}{body}"`, keyed by the
base64url-decoded `secret`. The resulting headers prove the request came from the
credential holder.
Headers produced: `POLY_ADDRESS` (EIP-55 checksummed), `POLY_API_KEY`,
`POLY_PASSPHRASE`, `POLY_SIGNATURE`, `POLY_TIMESTAMP`.
"""
alias Polymarket.Crypto
alias Polymarket.Schemas.Credentials
@poly_address "POLY_ADDRESS"
@poly_api_key "POLY_API_KEY"
@poly_passphrase "POLY_PASSPHRASE"
@poly_signature "POLY_SIGNATURE"
@poly_timestamp "POLY_TIMESTAMP"
@doc """
Computes the base64url HMAC-SHA256 signature for the L2 `message`.
`message` is `"{timestamp}{method}{path}{body}"`. `secret` is the base64url API
secret, decoded to form the HMAC key exactly as the server does.
"""
@spec sign(String.t(), String.t()) :: String.t()
def sign(secret, message) do
key = Base.url_decode64!(secret, padding: true)
Base.url_encode64(:crypto.mac(:hmac, :sha256, key, message), padding: true)
end
@doc """
Builds the five L2 headers authenticating a `method` request to `path` carrying
`body` at `timestamp` (Unix seconds), for the wallet `address` and `credentials`.
`body` must be the exact bytes sent on the wire — the HMAC covers it verbatim
(single quotes are normalised to double quotes, mirroring the reference client).
"""
@spec headers(<<_::160>>, Credentials.t(), String.t(), String.t(), binary(), integer()) ::
[{String.t(), String.t()}]
def headers(<<_::160>> = address, %Credentials{} = credentials, method, path, body, timestamp) do
message = "#{timestamp}#{method}#{path}#{normalize_body(body)}"
[
{@poly_address, Crypto.to_checksum_address(address)},
{@poly_api_key, credentials.api_key},
{@poly_passphrase, credentials.passphrase},
{@poly_signature, sign(credentials.secret, message)},
{@poly_timestamp, Integer.to_string(timestamp)}
]
end
@spec normalize_body(binary()) :: binary()
defp normalize_body(body), do: String.replace(body, "'", "\"")
end