defmodule Polymarket.RLP do @moduledoc """ Hand-rolled RLP (Recursive Length Prefix) **encoder** for Ethereum legacy (type-0) transaction serialization. This module implements only the encoding side โ€” there is no decoder. The set of inputs we encode is narrow: bytes, non-negative integers, and lists of already-encoded items. That's enough for legacy EIP-155 transactions and pure RLP test vectors. ## Encoding rules (per the Ethereum spec) Bytes / strings: * Empty: `0x80`. * Single byte `b` where `0x00 <= b <= 0x7f`: encoded as itself (single byte, no length prefix). * Other byte strings of length `len`: * `1 <= len <= 55`: `<<0x80 + len, ...bytes>>`. * `len > 55`: `<<0xb7 + size_of_len, ...len_bytes, ...bytes>>` where `len_bytes` is the minimal big-endian encoding of `len`. Lists with combined-payload size `len`: * `0 <= len <= 55`: `<<0xc0 + len, ...payload>>`. * `len > 55`: `<<0xf7 + size_of_len, ...len_bytes, ...payload>>`. Non-negative integers are encoded as their minimal-byte big-endian representation, then RLP-encoded as a byte string. Zero collapses to the empty string `0x80` โ€” the integer `0` does NOT encode as `<<0x00>>`. Output is byte-for-byte verified against `ethers.encodeRlp` in `test/fixtures/tx.json` ยง rlp_primitives. """ @doc """ Encodes a binary as an RLP string. """ @spec encode_bytes(binary()) :: binary() def encode_bytes(<<>>), do: <<0x80>> def encode_bytes(<>) when b < 0x80, do: <> def encode_bytes(bin) when is_binary(bin) and byte_size(bin) <= 55 do <<0x80 + byte_size(bin)>> <> bin end def encode_bytes(bin) when is_binary(bin) do len_bytes = :binary.encode_unsigned(byte_size(bin), :big) <<0xB7 + byte_size(len_bytes)>> <> len_bytes <> bin end @doc """ Encodes a non-negative integer as an RLP byte string carrying the minimal big-endian representation. Zero encodes as `0x80` (empty string), matching the canonical Ethereum/RLP convention. """ @spec encode_integer(non_neg_integer()) :: binary() def encode_integer(0), do: <<0x80>> def encode_integer(n) when is_integer(n) and n > 0 do encode_bytes(:binary.encode_unsigned(n, :big)) end @doc """ Encodes a list of already-RLP-encoded items as an RLP list. The caller is responsible for encoding each element first (`encode_bytes/1`, `encode_integer/1`, or a nested `encode_list/1`). This module deliberately keeps element-encoding decisions in the caller โ€” there is no value tagging that would let one function decide between "string" and "integer" interpretations of arbitrary Elixir terms. """ @spec encode_list([binary()]) :: binary() def encode_list(items) when is_list(items) do payload = IO.iodata_to_binary(items) payload_size = byte_size(payload) if payload_size <= 55 do <<0xC0 + payload_size>> <> payload else len_bytes = :binary.encode_unsigned(payload_size, :big) <<0xF7 + byte_size(len_bytes)>> <> len_bytes <> payload end end @doc """ Hex-encodes raw RLP output for human display or fixture comparison. Always lowercase, always `0x`-prefixed. """ @spec to_hex(binary()) :: String.t() def to_hex(bin) when is_binary(bin) do "0x" <> Base.encode16(bin, case: :lower) end end