defmodule Polymarket.Collateral do @moduledoc """ Polymarket CollateralOnramp / CollateralOfframp pUSD operations. Polymarket's v2 collateral lifecycle: * `wrap(_asset, _to, _amount)` on the CollateralOnramp converts an approved asset (typically USDC.e on Polygon) into pUSD credited to `_to`. The caller must `approve` the onramp for `_amount` of `_asset` first; see `Polymarket.ERC20.approve/2` (or `approve_calldata/2`). * `unwrap(_asset, _to, _amount)` on the CollateralOfframp does the reverse: burns `_amount` of pUSD from the caller and credits the equivalent `_asset` to `_to`. The caller must `approve` the offramp for `_amount` of pUSD first. ## Two surfaces * **Calldata builders** — `wrap_calldata/3` and `unwrap_calldata/3` produce the raw function-call bytes. Use them when you want the bytes for inspection, off-line signing, or submission through your own Polygon RPC client. * **Submit wrappers** — `wrap/2` and `unwrap/2` build, sign, and broadcast in one call. Compose the calldata builder + `Polymarket.Tx` + `Polymarket.RPC`. Caller provides every signing field (`:nonce`, `:gas_price`, `:gas_limit`, `:chain_id`, `:private_key`); the wrappers do not auto-fetch from RPC. ## Confirmed v2 contract addresses Per `V2_HANDOFF.md`, re-verified 2026-04-27: * pUSD CollateralToken proxy: `0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB` * CollateralOnramp: `0x93070a847efEf7F70739046A929D47a521F5B8ee` * CollateralOfframp: `0x2957922Eb93258b93368531d39fAcCA3B4dC5854` * USDC.e on Polygon: `0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174` Calldata bytes are validated against the canonical ethers.js v6 encoder in `test/fixtures/calldata.json`. The submit-wrapper composition (calldata → tx → signed bytes) is verified against a manually-composed reference in `test/polymarket/submit_test.exs`. """ alias Polymarket.ABI @wrap_signature "wrap(address,address,uint256)" @unwrap_signature "unwrap(address,address,uint256)" @doc """ Builds calldata for `wrap(address _asset, address _to, uint256 _amount)` on the CollateralOnramp. """ @spec wrap_calldata(String.t(), String.t(), non_neg_integer()) :: binary() def wrap_calldata(asset, to, amount) when is_binary(asset) and is_binary(to) and is_integer(amount) and amount >= 0 do ABI.function_selector(@wrap_signature) <> ABI.encode_address(asset) <> ABI.encode_address(to) <> ABI.encode_uint256(amount) end @doc """ Builds calldata for `unwrap(address _asset, address _to, uint256 _amount)` on the CollateralOfframp. """ @spec unwrap_calldata(String.t(), String.t(), non_neg_integer()) :: binary() def unwrap_calldata(asset, to, amount) when is_binary(asset) and is_binary(to) and is_integer(amount) and amount >= 0 do ABI.function_selector(@unwrap_signature) <> ABI.encode_address(asset) <> ABI.encode_address(to) <> ABI.encode_uint256(amount) end # ── Submit wrappers ── @doc """ Builds, signs, and broadcasts a `wrap(_asset, _to, _amount)` transaction to the CollateralOnramp at `:contract`. Returns the on-chain transaction hash on success. The caller must have already approved the onramp to spend `:_amount` of `:_asset` (use `Polymarket.ERC20.approve/2`). Required `opts`: `:contract`, `:asset`, `:to`, `:amount`, plus `:nonce`, `:gas_price`, `:gas_limit`, `:chain_id`, `:private_key`. """ @spec wrap(Polymarket.RPC.t(), keyword()) :: {:ok, String.t()} | {:error, term()} def wrap(%Polymarket.RPC{} = rpc, opts) when is_list(opts) do with :ok <- Polymarket.Submit.require_opts(opts, [:contract, :asset, :to, :amount]) do calldata = wrap_calldata( Keyword.fetch!(opts, :asset), Keyword.fetch!(opts, :to), Keyword.fetch!(opts, :amount) ) Polymarket.Submit.send_call(rpc, Keyword.fetch!(opts, :contract), calldata, opts) end end @doc """ Builds, signs, and broadcasts an `unwrap(_asset, _to, _amount)` transaction to the CollateralOfframp at `:contract`. Returns the on-chain transaction hash on success. The caller must have already approved the offramp to spend `:_amount` of pUSD. Required `opts`: same as `wrap/2`. """ @spec unwrap(Polymarket.RPC.t(), keyword()) :: {:ok, String.t()} | {:error, term()} def unwrap(%Polymarket.RPC{} = rpc, opts) when is_list(opts) do with :ok <- Polymarket.Submit.require_opts(opts, [:contract, :asset, :to, :amount]) do calldata = unwrap_calldata( Keyword.fetch!(opts, :asset), Keyword.fetch!(opts, :to), Keyword.fetch!(opts, :amount) ) Polymarket.Submit.send_call(rpc, Keyword.fetch!(opts, :contract), calldata, opts) end end end