lib/address.ex

defmodule Bitcoinex.Address do
  @moduledoc """
  Bitcoinex.Address supports Base58 and Bech32 address encoding and validation.
  """

  alias Bitcoinex.{Segwit, Base58, Network}

  @typedoc """
    The address_type describes the address type to use.

    Four address types are supported:
    * p2pkh: Pay-to-Public-Key-Hash
    * p2sh: Pay-to-Script-Hash
    * p2wpkh: Pay-to-Witness-Public-Key-Hash
    * p2wsh: Pay-To-Witness-Script-Hash
  """
  @type address_type :: :p2pkh | :p2sh | :p2wpkh | :p2wsh
  @address_types ~w(p2pkh p2sh p2wpkh p2wsh p2tr)a

  @doc """
  Accepts a public key hash, network, and address_type and returns its address.
  """
  @spec encode(binary, Bitcoinex.Network.network_name(), address_type) :: String.t()
  def encode(pubkey_hash, network_name, :p2pkh) do
    network = Network.get_network(network_name)
    decimal_prefix = network.p2pkh_version_decimal_prefix

    Base58.encode(<<decimal_prefix>> <> pubkey_hash)
  end

  def encode(script_hash, network_name, :p2sh) do
    network = Network.get_network(network_name)
    decimal_prefix = network.p2sh_version_decimal_prefix
    Base58.encode(<<decimal_prefix>> <> script_hash)
  end

  @doc """
  Checks if the address is valid.
  Both encoding and network is checked.
  """
  @spec is_valid?(String.t(), Bitcoinex.Network.network_name()) :: boolean
  def is_valid?(address, network_name) do
    Enum.any?(@address_types, &is_valid?(address, network_name, &1))
  end

  @doc """
  Checks if the address is valid and matches the given address_type.
  Both encoding and network is checked.
  """
  @spec is_valid?(String.t(), Bitcoinex.Network.network_name(), address_type) :: boolean
  def is_valid?(address, network_name, :p2pkh) do
    network = apply(Bitcoinex.Network, network_name, [])
    is_valid_base58_check_address?(address, network.p2pkh_version_decimal_prefix)
  end

  def is_valid?(address, network_name, :p2sh) do
    network = apply(Bitcoinex.Network, network_name, [])
    is_valid_base58_check_address?(address, network.p2sh_version_decimal_prefix)
  end

  def is_valid?(address, network_name, :p2wpkh) do
    case Segwit.decode_address(address) do
      {:ok, {^network_name, witness_version, witness_program}}
      when witness_version == 0 and length(witness_program) == 20 ->
        true

      # network is not same as network set in config
      {:ok, {_network_name, _, _}} ->
        false

      {:error, _error} ->
        false
    end
  end

  def is_valid?(address, network_name, :p2wsh) do
    case Segwit.decode_address(address) do
      {:ok, {^network_name, witness_version, witness_program}}
      when witness_version == 0 and length(witness_program) == 32 ->
        true

      # network is not same as network set in config
      {:ok, {_network_name, _, _}} ->
        false

      {:error, _error} ->
        false
    end
  end

  def is_valid?(address, network_name, :p2tr) do
    case Segwit.decode_address(address) do
      {:ok, {^network_name, witness_version, witness_program}}
      when witness_version == 1 and length(witness_program) == 32 ->
        true

      # network is not same as network set in config
      {:ok, {_network_name, _, _}} ->
        false

      {:error, _error} ->
        false
    end
  end

  @doc """
  Returns a list of supported address types.
  """
  def supported_address_types() do
    @address_types
  end

  defp is_valid_base58_check_address?(address, valid_prefix) do
    case Base58.decode(address) do
      {:ok, <<^valid_prefix::8, _::binary>>} ->
        true

      _ ->
        false
    end
  end

  @doc """
  Decodes an address and returns the address_type.
  """
  @spec decode_type(String.t(), Bitcoinex.Network.network_name()) ::
          {:ok, address_type} | {:error, :decode_error}
  def decode_type(address, network_name) do
    case Enum.find(@address_types, &is_valid?(address, network_name, &1)) do
      nil -> {:error, :decode_error}
      type -> {:ok, type}
    end
  end
end