defmodule Onchain.ERC1155 do @moduledoc """ ERC-1155 (Multi Token) read operations. Thin wrappers around `Onchain.Contract.call/5` for standard ERC-1155 queries. ERC-1155 is semi-fungible: the same token ID can have multiple owners with different balances. ## Error Format Errors pass through from underlying modules: | Source | Error Shape | |--------|-------------| | `Onchain.Address.validate/1` | `{:error, {:invalid_address, input}}` | | `Onchain.Contract.call/5` | `{:error, {:encode_error, ...}}`, `{:error, {:rpc_error, ...}}`, `{:error, {:decode_error, ...}}` | ## Functions | Function | Purpose | |----------|---------| | `balance_of/4` | Balance of a specific token ID for an owner | | `balance_of!/4` | Same, raises on error | | `balance_of_batch/4` | Batch balance query for multiple owner/token pairs | | `balance_of_batch!/4` | Same, raises on error | | `uri/3` | Metadata URI for a token ID | | `uri!/3` | Same, raises on error | | `approved_for_all?/4` | Whether an operator is approved for all tokens | | `approved_for_all!/4` | Same, raises on error | """ use Descripex, namespace: "/erc1155" alias Onchain.Address alias Onchain.Contract # Contract.call/5 inherits the upstream Signet.Hex spec mismatch that # makes ABI.decode_response/2 appear to return no_return(). Every function # here unwraps Contract.call results, so the same cascade applies. @dialyzer {:no_match, [ balance_of: 4, balance_of!: 4, balance_of_batch: 4, balance_of_batch!: 4, uri: 3, uri!: 3, approved_for_all?: 4, approved_for_all!: 4 ]} @dialyzer {:no_return, [ balance_of!: 3, balance_of!: 4, balance_of_batch!: 3, balance_of_batch!: 4, uri!: 2, uri!: 3, approved_for_all!: 3, approved_for_all!: 4 ]} @dialyzer {:no_contracts, [ balance_of!: 3, balance_of!: 4, balance_of_batch!: 3, balance_of_batch!: 4, uri!: 2, uri!: 3, approved_for_all!: 3, approved_for_all!: 4 ]} # --- balance_of --- api(:balance_of, "Get the balance of a specific token ID for an owner.", params: [ contract: [kind: :value, description: "ERC-1155 contract address"], owner: [kind: :value, description: "Address to check balance for"], token_id: [kind: :value, description: "Token ID (non-negative integer)"], opts: [kind: :value, default: [], description: "Options: :rpc_url, :timeout, :block"] ], returns: %{ type: "{:ok, non_neg_integer()} | {:error, term()}", description: "Balance of the token ID for the owner", example: "10" } ) @spec balance_of(String.t() | binary(), String.t() | binary(), non_neg_integer(), keyword()) :: {:ok, non_neg_integer()} | {:error, term()} def balance_of(contract, owner, token_id, opts \\ []) do with {:ok, owner_bin} <- Address.validate(owner), {:ok, [balance]} <- Contract.call( contract, "balanceOf(address,uint256)", [owner_bin, token_id], "(uint256)", opts ) do {:ok, balance} end end # --- balance_of! --- api(:balance_of!, "Get the balance of a specific token ID for an owner. Raises on error.", params: [ contract: [kind: :value, description: "ERC-1155 contract address"], owner: [kind: :value, description: "Address to check balance for"], token_id: [kind: :value, description: "Token ID (non-negative integer)"], opts: [kind: :value, default: [], description: "Options: :rpc_url, :timeout, :block"] ], returns: %{type: :non_neg_integer, description: "Balance of the token ID"} ) @spec balance_of!(String.t() | binary(), String.t() | binary(), non_neg_integer(), keyword()) :: non_neg_integer() def balance_of!(contract, owner, token_id, opts \\ []) do case balance_of(contract, owner, token_id, opts) do {:ok, balance} -> balance {:error, reason} -> raise "balance_of failed: #{inspect(reason)}" end end # --- balance_of_batch --- api(:balance_of_batch, "Batch query balances for multiple owner/token ID pairs.", params: [ contract: [kind: :value, description: "ERC-1155 contract address"], owners: [kind: :value, description: "List of owner addresses (must match token_ids length)"], token_ids: [kind: :value, description: "List of token IDs (must match owners length)"], opts: [kind: :value, default: [], description: "Options: :rpc_url, :timeout, :block"] ], returns: %{ type: "{:ok, [non_neg_integer()]} | {:error, term()}", description: "List of balances corresponding to each owner/token_id pair", example: "[10, 0, 5]" } ) @spec balance_of_batch( String.t() | binary(), [String.t() | binary()], [non_neg_integer()], keyword() ) :: {:ok, [non_neg_integer()]} | {:error, term()} def balance_of_batch(contract, owners, token_ids, opts \\ []) def balance_of_batch(_contract, owners, _token_ids, _opts) when not is_list(owners) do {:error, {:invalid_input, "owners must be a list"}} end def balance_of_batch(_contract, _owners, token_ids, _opts) when not is_list(token_ids) do {:error, {:invalid_input, "token_ids must be a list"}} end def balance_of_batch(_contract, owners, token_ids, _opts) when length(owners) != length(token_ids) do {:error, {:length_mismatch, %{owners: length(owners), token_ids: length(token_ids)}}} end def balance_of_batch(contract, owners, token_ids, opts) do with {:ok, owner_bins} <- validate_addresses(owners), {:ok, [balances]} <- Contract.call( contract, "balanceOfBatch(address[],uint256[])", [owner_bins, token_ids], "(uint256[])", opts ) do {:ok, balances} end end # --- balance_of_batch! --- api(:balance_of_batch!, "Batch query balances. Raises on error.", params: [ contract: [kind: :value, description: "ERC-1155 contract address"], owners: [kind: :value, description: "List of owner addresses (must match token_ids length)"], token_ids: [kind: :value, description: "List of token IDs (must match owners length)"], opts: [kind: :value, default: [], description: "Options: :rpc_url, :timeout, :block"] ], returns: %{type: "[non_neg_integer()]", description: "List of balances"} ) @spec balance_of_batch!( String.t() | binary(), [String.t() | binary()], [non_neg_integer()], keyword() ) :: [non_neg_integer()] def balance_of_batch!(contract, owners, token_ids, opts \\ []) do case balance_of_batch(contract, owners, token_ids, opts) do {:ok, balances} -> balances {:error, reason} -> raise "balance_of_batch failed: #{inspect(reason)}" end end # --- uri --- api(:uri, "Get the metadata URI for a token ID.", params: [ contract: [kind: :value, description: "ERC-1155 contract address"], token_id: [kind: :value, description: "Token ID (non-negative integer)"], opts: [kind: :value, default: [], description: "Options: :rpc_url, :timeout, :block"] ], returns: %{ type: "{:ok, String.t()} | {:error, term()}", description: "Token metadata URI (may contain {id} placeholder per ERC-1155 spec)", example: ~s("https://token-cdn-domain/{id}.json") } ) @spec uri(String.t() | binary(), non_neg_integer(), keyword()) :: {:ok, String.t()} | {:error, term()} def uri(contract, token_id, opts \\ []) do with {:ok, [value]} <- Contract.call(contract, "uri(uint256)", [token_id], "(string)", opts) do {:ok, value} end end # --- uri! --- api(:uri!, "Get the metadata URI for a token ID. Raises on error.", params: [ contract: [kind: :value, description: "ERC-1155 contract address"], token_id: [kind: :value, description: "Token ID (non-negative integer)"], opts: [kind: :value, default: [], description: "Options: :rpc_url, :timeout, :block"] ], returns: %{type: :string, description: "Token metadata URI"} ) @spec uri!(String.t() | binary(), non_neg_integer(), keyword()) :: String.t() def uri!(contract, token_id, opts \\ []) do case uri(contract, token_id, opts) do {:ok, value} -> value {:error, reason} -> raise "uri failed: #{inspect(reason)}" end end # --- approved_for_all? --- api(:approved_for_all?, "Check if an operator is approved for all tokens of an owner.", params: [ contract: [kind: :value, description: "ERC-1155 contract address"], owner: [kind: :value, description: "Token owner address"], operator: [kind: :value, description: "Operator address to check"], opts: [kind: :value, default: [], description: "Options: :rpc_url, :timeout, :block"] ], returns: %{ type: "{:ok, boolean()} | {:error, term()}", description: "Whether the operator is approved for all tokens", example: "false" } ) @spec approved_for_all?( String.t() | binary(), String.t() | binary(), String.t() | binary(), keyword() ) :: {:ok, boolean()} | {:error, term()} def approved_for_all?(contract, owner, operator, opts \\ []) do with {:ok, owner_bin} <- Address.validate(owner), {:ok, operator_bin} <- Address.validate(operator), {:ok, [approved]} <- Contract.call( contract, "isApprovedForAll(address,address)", [owner_bin, operator_bin], "(bool)", opts ) do {:ok, approved} end end # --- approved_for_all! --- api(:approved_for_all!, "Check if an operator is approved for all tokens. Raises on error.", params: [ contract: [kind: :value, description: "ERC-1155 contract address"], owner: [kind: :value, description: "Token owner address"], operator: [kind: :value, description: "Operator address to check"], opts: [kind: :value, default: [], description: "Options: :rpc_url, :timeout, :block"] ], returns: %{type: :boolean, description: "Whether the operator is approved for all tokens"} ) @spec approved_for_all!( String.t() | binary(), String.t() | binary(), String.t() | binary(), keyword() ) :: boolean() def approved_for_all!(contract, owner, operator, opts \\ []) do case approved_for_all?(contract, owner, operator, opts) do {:ok, approved} -> approved {:error, reason} -> raise "approved_for_all? failed: #{inspect(reason)}" end end # Validates a list of addresses, returning all as binaries or the first error. @spec validate_addresses([String.t() | binary()]) :: {:ok, [binary()]} | {:error, term()} defp validate_addresses(addresses) do addresses |> Enum.reduce_while({:ok, []}, fn addr, {:ok, acc} -> case Address.validate(addr) do {:ok, bin} -> {:cont, {:ok, [bin | acc]}} error -> {:halt, error} end end) |> case do {:ok, bins} -> {:ok, Enum.reverse(bins)} error -> error end end end