defmodule BitcoinLib.Key.Address do @moduledoc """ Bitcoin address management Inspired by https://learnmeabitcoin.com/technical/public-key-hash """ alias BitcoinLib.Crypto alias BitcoinLib.Key.PublicKey alias BitcoinLib.Key.Address.{P2PKH, P2SH, Bech32} @doc """ Turns a public key into an address of the specified format ## Examples iex> %BitcoinLib.Key.PublicKey{ ...> key: <<0x0343B337DEC65A47B3362C9620A6E6FF39A1DDFA908ABAB1666C8A30A3F8A7CCCC::264>> ...> } ...> |> BitcoinLib.Key.Address.from_public_key(:bech32, :mainnet) "bc1qa5gyew808tdta3wjh6qh3jvcglukjsnfg0qx4u" """ @spec from_public_key(%PublicKey{}, :p2pkh | :p2sh | :bech32, :mainnet | :testnet) :: binary() def from_public_key(%PublicKey{} = public_key, :p2pkh, :mainnet), do: P2PKH.from_public_key(public_key) def from_public_key(%PublicKey{} = public_key, :p2pkh, :testnet), do: P2PKH.from_public_key(public_key, :testnet) def from_public_key(%PublicKey{} = public_key, :p2sh, :mainnet), do: P2SH.from_public_key(public_key) def from_public_key(%PublicKey{} = public_key, :p2sh, :testnet), do: P2SH.from_public_key(public_key, :testnet) def from_public_key(%PublicKey{} = public_key, :bech32, :mainnet), do: Bech32.from_public_key(public_key) def from_public_key(%PublicKey{} = public_key, :bech32, :testnet), do: Bech32.from_public_key(public_key, :testnet) @doc """ Convert public key hash into a P2PKH Bitcoin address. Details can be found here: https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses ## Examples iex> <<0x6ae201797de3fa7d1d95510f50c1a9c50ce4cc36::160>> ...> |> BitcoinLib.Key.Address.from_public_key_hash(:p2pkh) "1Ak9NVPmwCHEpsSWvM6cNRC7dsYniRmwMG" """ @spec from_public_key_hash(binary(), :p2pkh | :p2sh, :mainnet | :testnet) :: bitstring() def from_public_key_hash(public_key_hash, address_type \\ :p2sh, network \\ :mainnet) do public_key_hash |> Binary.to_hex() |> prepend_prefix(address_type, network) |> Binary.from_hex() |> append_checksum |> Base58.encode() end @doc """ Extracts the public key hash from an address, and make sure the checkum is ok ## Examples iex> address = "mwYKDe7uJcgqyVHJAPURddeZvM5zBVQj5L" ...> BitcoinLib.Key.Address.destructure(address) {:ok, <<0xafc3e518577316386188af748a816cd14ce333f2::160>>, :p2pkh, :testnet} """ @spec destructure(binary()) :: {:ok, <<_::160>>, :p2pkh | :p2sh, :mainnet | :testnet} | {:error, binary()} def destructure(address) do <> = address |> Base58.decode() {address_type, network} = get_address_type_from_prefix(prefix) case test_checksum(prefix, public_key_hash, checksum) do {:ok} -> {:ok, public_key_hash, address_type, network} {:error, message} -> {:error, message} end end defp prepend_prefix(public_key_hash, address_type, network) do get_prefix(address_type, network) <> public_key_hash end defp append_checksum(public_key_hash) do checksum = public_key_hash |> Crypto.checksum() public_key_hash <> checksum end @spec test_checksum(integer(), bitstring(), bitstring()) :: {:ok} | {:error, binary()} defp test_checksum(prefix, public_key_hash, original_checksum) do calculated_checksum = <> |> Crypto.checksum() case calculated_checksum do ^original_checksum -> {:ok} _ -> {:error, "checksums don't match"} end end # Here is a useful list of address prefixes # https://en.bitcoin.it/wiki/List_of_address_prefixes defp get_prefix(:p2pkh, :mainnet), do: "00" defp get_prefix(:p2sh, :mainnet), do: "05" defp get_prefix(:p2pkh, :testnet), do: "6F" defp get_prefix(:p2sh, :testnet), do: "C4" defp get_prefix(_, _), do: "" # example 17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem defp get_address_type_from_prefix(0), do: {:p2pkh, :mainnet} # example 3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX defp get_address_type_from_prefix(5), do: {:p2sh, :mainnet} # example mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn defp get_address_type_from_prefix(111), do: {:p2pkh, :testnet} # example 2MzQwSSnBHWHqSAqtTVQ6v47XtaisrJa1Vc defp get_address_type_from_prefix(196), do: {:p2sh, :testnet} defp get_address_type_from_prefix(_), do: :unknown end