Packages

Higher-level Elixir SDK for Polymarket: Gamma market discovery, Data API, CTF/pUSD helpers, and convenience facades. Depends on polymarket_clob for the low-level CLOB surface.

Current section

Files

Jump to
polymarket_sdk lib polymarket abi.ex
Raw

lib/polymarket/abi.ex

defmodule Polymarket.ABI do
@moduledoc """
Hand-rolled Solidity ABI primitives for the fixed on-chain surfaces in
this SDK.
This module covers exactly what `Polymarket.ERC20`, `Polymarket.CTF`,
and `Polymarket.Collateral` need: function selectors, fixed-size types
(`address`, `uint256`, `bytes32`), and the `uint256[]` dynamic-array
tail. It is intentionally narrow — there is no general-purpose ABI
decoder, no support for `bytes`/`string`/nested tuples, and no
`ex_abi` / `ethereumex` / `ethers` framework dep (per `DECISIONS.md`
#4 — extra dep weight previously cost the live bot ~10 seconds of
startup latency).
Calldata produced by these functions is validated byte-for-byte
against ethers.js v6 fixtures in `test/fixtures/calldata.json`.
## Encoding rules summary
* Function selector — first 4 bytes of `keccak256(signature)`.
* `uint256` — 32-byte big-endian unsigned.
* `address` — 20-byte address right-aligned in a 32-byte word
(zero-padded on the left).
* `bytes32` — 32 bytes inline.
* `uint256[]` — dynamic. The function head emits a 32-byte offset
pointer (the byte position of the array's tail relative to the
start of the args, post-selector). The tail at that offset is
`[length :: uint256][element0 :: uint256]…`. The caller is
responsible for placing the offset in the head; this module
provides `encode_uint256_array_tail/1` for the tail bytes.
"""
import Bitwise, only: [<<<: 2]
@doc """
Returns the 4-byte function selector for a Solidity signature like
`"approve(address,uint256)"`.
"""
@spec function_selector(String.t()) :: <<_::32>>
def function_selector(signature) when is_binary(signature) do
<<sel::binary-size(4), _::binary>> = ExKeccak.hash_256(signature)
sel
end
@doc """
Encodes a non-negative integer as 32-byte big-endian unsigned.
"""
@spec encode_uint256(non_neg_integer()) :: <<_::256>>
def encode_uint256(n) when is_integer(n) and n >= 0 and n < 1 <<< 256 do
<<n::unsigned-big-256>>
end
@doc """
Encodes a 20-byte hex Ethereum address as a 32-byte right-aligned
word (12 bytes of zero padding + 20 address bytes).
Accepts the address with or without a leading `0x` and in any case.
EIP-55 checksum is preserved on input but the output is the canonical
Solidity representation (lowercase nibbles, byte-equivalent regardless
of input case).
"""
@spec encode_address(String.t()) :: <<_::256>>
def encode_address("0x" <> hex), do: encode_address(hex)
def encode_address(hex) when is_binary(hex) and byte_size(hex) == 40 do
case Base.decode16(hex, case: :mixed) do
{:ok, addr_bytes} when byte_size(addr_bytes) == 20 ->
<<0::96, addr_bytes::binary-size(20)>>
_ ->
raise ArgumentError, "address must be 20 bytes of hex, got: #{inspect(hex)}"
end
end
def encode_address(other) do
raise ArgumentError, "address must be 20 bytes of hex, got: #{inspect(other)}"
end
@doc """
Encodes a 32-byte hex value as 32 bytes inline.
Accepts `0x`-prefixed or bare hex (any case). Used for `bytes32`
parameters such as `parentCollectionId` and `conditionId`.
"""
@spec encode_bytes32(String.t()) :: <<_::256>>
def encode_bytes32("0x" <> hex), do: encode_bytes32(hex)
def encode_bytes32(hex) when is_binary(hex) and byte_size(hex) == 64 do
case Base.decode16(hex, case: :mixed) do
{:ok, bytes} when byte_size(bytes) == 32 ->
bytes
_ ->
raise ArgumentError, "bytes32 must be 32 bytes of hex, got: #{inspect(hex)}"
end
end
def encode_bytes32(other) do
raise ArgumentError, "bytes32 must be 32 bytes of hex, got: #{inspect(other)}"
end
@doc """
Returns the dynamic tail of a `uint256[]` argument: a 32-byte length
word followed by each element as 32-byte big-endian unsigned.
The caller must separately place the head offset — typically
`head_size` bytes (i.e. number of static head words × 32) — using
`encode_uint256/1`.
"""
@spec encode_uint256_array_tail([non_neg_integer()]) :: binary()
def encode_uint256_array_tail(values) when is_list(values) do
length_word = encode_uint256(length(values))
elements =
Enum.reduce(values, <<>>, fn n, acc ->
acc <> encode_uint256(n)
end)
length_word <> elements
end
@doc """
Hex-encodes raw calldata for human display or fixture comparison.
Always lowercase, always `0x`-prefixed.
"""
@spec to_hex(binary()) :: String.t()
def to_hex(binary) when is_binary(binary) do
"0x" <> Base.encode16(binary, case: :lower)
end
end