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(<> <> 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(<> <> 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