defmodule Polymarket.ERC20 do @moduledoc """ ERC-20 helpers for the SDK's on-chain surface. The SDK currently needs only one ERC-20 function — `approve` — to authorize the CollateralOnramp / CollateralOfframp and the CTF contract to spend tokens on the caller's behalf. ## Two surfaces * **Calldata builder** — `approve_calldata/2` produces the raw function-call bytes for `approve(spender, amount)`. Use this when you want the bytes for inspection, off-line signing, or submission through your own Polygon RPC client. * **Submit wrapper** — `approve/2` builds, signs, and broadcasts the transaction in one call. Composes `approve_calldata/2` + `Polymarket.Tx` + `Polymarket.RPC`. Caller provides every signing field (`:nonce`, `:gas_price`, `:gas_limit`, `:chain_id`, `:private_key`); the wrapper does not auto-fetch from RPC. Calldata bytes are validated against the canonical ethers.js v6 encoder in `test/fixtures/calldata.json`. The submit wrapper's composition (calldata → tx → signed bytes) is verified against a manually-composed reference in `test/polymarket/submit_test.exs`. No transaction-submission policy is baked in. Confirmation polling, retry on `nonce too low`, and EIP-1559 type-2 fees are out of scope here. """ alias Polymarket.ABI @approve_signature "approve(address,uint256)" @doc """ Builds calldata for `approve(address spender, uint256 amount)`. ## Examples iex> Polymarket.ERC20.approve_calldata( ...> "0x0000000000000000000000000000000000000001", ...> 1_000 ...> ) ...> |> Polymarket.ABI.to_hex() "0x095ea7b3000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000003e8" """ @spec approve_calldata(String.t(), non_neg_integer()) :: binary() def approve_calldata(spender, amount) when is_binary(spender) and is_integer(amount) and amount >= 0 do ABI.function_selector(@approve_signature) <> ABI.encode_address(spender) <> ABI.encode_uint256(amount) end @doc """ Builds, signs, and broadcasts an `approve(spender, amount)` transaction to the ERC-20 token at `:contract`. Returns the on-chain transaction hash on success. ## Required `opts` * `:contract` — the ERC-20 token address being approved (e.g. pUSD). * `:spender` — the address being authorized (e.g. CTF or onramp). * `:amount` — non-negative integer, in the token's smallest unit. * `:nonce`, `:gas_price`, `:gas_limit`, `:chain_id` — caller-provided transaction fields. The wrapper does not auto-fetch from RPC; use `Polymarket.RPC.nonce/2`, `gas_price/1`, and `estimate_gas/2` first if you need that. * `:private_key` — 32-byte private key (raw binary or `0x`-prefixed hex) for the sender. Returns `{:error, {:missing_option, key}}` when a required option is absent, honoring the `{:ok, _} | {:error, _}` contract instead of raising. """ @spec approve(Polymarket.RPC.t(), keyword()) :: {:ok, String.t()} | {:error, term()} def approve(%Polymarket.RPC{} = rpc, opts) when is_list(opts) do with :ok <- Polymarket.Submit.require_opts(opts, [:contract, :spender, :amount]) do contract = Keyword.fetch!(opts, :contract) calldata = approve_calldata(Keyword.fetch!(opts, :spender), Keyword.fetch!(opts, :amount)) Polymarket.Submit.send_call(rpc, contract, calldata, opts) end end end