defmodule Bitcoinex.Segwit do
@moduledoc """
SegWit address serialization.
"""
alias Bitcoinex.Bech32
@valid_witness_program_length_range 2..40
@valid_witness_version 0..16
@supported_network [:mainnet, :testnet, :regtest]
@type hrp :: String.t()
@type data :: list(integer)
# seem no way to use list of atom module attribute in type spec
@type network :: :testnet | :mainnet | :regtest
@type witness_version :: 0..16
@type witness_program :: list(integer)
@type error :: atom()
@doc """
Decodes an address and returns its network, witness version, and witness program.
"""
@spec decode_address(String.t()) ::
{:ok, {network, witness_version, witness_program}} | {:error, error}
def decode_address(address) when is_binary(address) do
with {_, {:ok, {encoding_type, hrp, data}}} <- {:decode_bech32, Bech32.decode(address)},
{_, {:ok, network}} <- {:parse_network, parse_network(hrp |> String.to_charlist())},
{_, {:ok, {version, program}}} <- {:parse_segwit_data, parse_segwit_data(data)} do
case witness_version_to_bech_encoding(version) do
^encoding_type ->
{:ok, {network, version, program}}
_ ->
# encoding type derived from witness version (first byte of data) is different from the code derived from bech32 decoding
{:error, :invalid_checksum}
end
else
{_, {:error, error}} ->
{:error, error}
end
end
@doc """
Encodes an address string.
"""
@spec encode_address(network, witness_version, witness_program) ::
{:ok, String.t()} | {:error, error}
def encode_address(network, _, _) when network not in @supported_network do
{:error, :invalid_network}
end
def encode_address(_, witness_version, _)
when witness_version not in @valid_witness_version do
{:error, :invalid_witness_version}
end
def encode_address(network, version, program) do
with {:ok, converted_program} <- Bech32.convert_bits(program, 8, 5),
{:is_program_length_valid, true} <-
{:is_program_length_valid, is_program_length_valid?(version, program)} do
hrp =
case network do
:mainnet ->
"bc"
:testnet ->
"tb"
:regtest ->
"bcrt"
end
Bech32.encode(hrp, [version | converted_program], witness_version_to_bech_encoding(version))
else
{:is_program_length_valid, false} ->
{:error, :invalid_program_length}
error ->
error
end
end
@doc """
Simpler Interface to check if address is valid
"""
@spec is_valid_segswit_address?(String.t()) :: boolean
def is_valid_segswit_address?(address) when is_binary(address) do
case decode_address(address) do
{:ok, _} ->
true
_ ->
false
end
end
@spec get_segwit_script_pubkey(witness_version, witness_program) :: String.t()
def get_segwit_script_pubkey(version, program) do
# OP_0 is encoded as 0x00, but OP_1 through OP_16 are encoded as 0x51 though 0x60
wit_version_adjusted = if(version == 0, do: 0, else: version + 0x50)
[
wit_version_adjusted,
Enum.count(program) | program
]
|> :erlang.list_to_binary()
# to hex and all lower case for better readability
|> Base.encode16(case: :lower)
end
defp parse_segwit_data([]) do
{:error, :empty_segwit_data}
end
defp parse_segwit_data([version | encoded]) when version in @valid_witness_version do
case Bech32.convert_bits(encoded, 5, 8, false) do
{:ok, program} ->
if is_program_length_valid?(version, program) do
{:ok, {version, program}}
else
{:error, :invalid_program_length}
end
{:error, error} ->
{:error, error}
end
end
defp parse_segwit_data(_), do: {:error, :invalid_witness_version}
defp is_program_length_valid?(version, program)
when length(program) in @valid_witness_program_length_range do
case {version, length(program)} do
# BIP141 specifies If the version byte is 0, but the witness program is neither 20 nor 32 bytes, the script must fail.
{0, program_length} when program_length == 20 or program_length == 32 ->
true
{0, _} ->
false
_ ->
true
end
end
defp is_program_length_valid?(_, _), do: false
defp parse_network('bc'), do: {:ok, :mainnet}
defp parse_network('tb'), do: {:ok, :testnet}
defp parse_network('bcrt'), do: {:ok, :regtest}
defp parse_network(_), do: {:error, :invalid_network}
defp witness_version_to_bech_encoding(0), do: :bech32
defp witness_version_to_bech_encoding(witver) when witver in 1..16, do: :bech32m
end