Current section
Files
Jump to
Current section
Files
lib/clob/order_payload.ex
defmodule Polymarket.Clob.OrderPayload do
@moduledoc """
Serialises signed `Polymarket.Schemas.Order`s into the JSON wire body the CLOB
expects — a single `SendOrder` object for `POST /order`, or a JSON array of them
for the batch `POST /orders`.
The body is produced once, as a string, so the L2 HMAC can sign exactly the bytes
that go on the wire. Field formats mirror the reference client: `salt` is a JSON
number; `tokenId`, `makerAmount`, `takerAmount`, `expiration` and `timestamp` are
decimal strings; `maker`/`signer` are lowercase `0x` hex; `metadata`/`builder` are
`0x`+64 hex; `side` is `"BUY"`/`"SELL"`; `signatureType` is a number. `expiration`
rides on the wire body (for GTD/expiry) even though it is not part of the signed
V2 struct.
"""
alias Polymarket.Crypto
alias Polymarket.Schemas.Order
alias Polymarket.Schemas.SendOrder
@doc """
Builds the JSON `SendOrder` body string for a caller-constructed
`Polymarket.Schemas.SendOrder` (signed order + owner + time-in-force + flags).
`post_only`/`defer_exec` are omitted from the body when `nil`, matching the
reference client.
"""
@spec serialize(SendOrder.t()) :: String.t()
def serialize(%SendOrder{} = send_order) do
send_order |> send_order_object() |> Jason.encode!()
end
@doc """
Builds the JSON array body string for the batch `POST /orders` from a list of
caller-constructed `Polymarket.Schemas.SendOrder`s.
Each element is the identical object `serialize/1` produces for a single order,
in the given order. The CLOB caps a batch at 15 orders; this only serialises, so
the server enforces that limit.
"""
@spec serialize_many([SendOrder.t()]) :: String.t()
def serialize_many(send_orders) when is_list(send_orders) do
send_orders |> Enum.map(&send_order_object/1) |> Jason.encode!()
end
@spec send_order_object(SendOrder.t()) :: map()
defp send_order_object(%SendOrder{} = send_order) do
%{
"order" => order_object(send_order.order, send_order.signature),
"orderType" => order_type_string(send_order.order_type),
"owner" => send_order.owner
}
|> maybe_put("postOnly", send_order.post_only)
|> maybe_put("deferExec", send_order.defer_exec)
end
@spec order_object(Order.t(), String.t()) :: map()
defp order_object(order, signature) do
%{
"salt" => order.salt,
"maker" => address(order.maker),
"signer" => address(order.signer),
"tokenId" => Integer.to_string(order.token_id),
"makerAmount" => Integer.to_string(order.maker_amount),
"takerAmount" => Integer.to_string(order.taker_amount),
"side" => side_string(order.side),
"expiration" => Integer.to_string(order.expiration),
"signatureType" => Order.signature_type_value(order.signature_type),
"timestamp" => Integer.to_string(order.timestamp),
"metadata" => bytes32(order.metadata),
"builder" => bytes32(order.builder),
"signature" => signature
}
end
@spec maybe_put(map(), String.t(), boolean() | nil) :: map()
defp maybe_put(map, _key, nil), do: map
defp maybe_put(map, key, value) when is_boolean(value), do: Map.put(map, key, value)
@spec address(<<_::160>>) :: String.t()
defp address(<<_::160>> = bytes), do: Crypto.to_hex(bytes)
@spec bytes32(<<_::256>>) :: String.t()
defp bytes32(<<_::256>> = bytes), do: Crypto.to_hex(bytes)
@spec side_string(Order.side()) :: String.t()
defp side_string(:buy), do: "BUY"
defp side_string(:sell), do: "SELL"
@spec order_type_string(SendOrder.order_type()) :: String.t()
defp order_type_string(:gtc), do: "GTC"
defp order_type_string(:fok), do: "FOK"
defp order_type_string(:gtd), do: "GTD"
defp order_type_string(:fak), do: "FAK"
end