lib/web3/types/hash.ex

defmodule Web3.Type.Hash do
  @moduledoc """
  A [KECCAK-256](https://en.wikipedia.org/wiki/SHA-3) hash.

  Copy from [Blockscout](https://github.com/blockscout/blockscout)

  """

  import Bitwise

  @bits_per_byte 8
  @hexadecimal_digits_per_byte 2
  @max_byte_count 32

  defstruct ~w(byte_count bytes)a

  @typedoc """
  A full [KECCAK-256](https://en.wikipedia.org/wiki/SHA-3) hash is #{@max_byte_count}, but it can also be truncated to
  fewer bytes.
  """
  @type byte_count :: 1..unquote(@max_byte_count)

  @typedoc """
  A module that implements this behaviour's callbacks
  """
  @type t :: %__MODULE__{
          byte_count: byte_count,
          bytes: <<_::_*8>>
        }

  @callback byte_count() :: byte_count()

  @doc """
  Number of bits in a byte
  """
  def bits_per_byte, do: 8

  @doc """
  How many hexadecimal digits are used to represent a byte
  """
  def hexadecimal_digits_per_byte, do: 2

  @doc """
  Casts `term` to `t:t/0` using `c:byte_count/0` in `module`
  """
  @spec cast(module(), term()) :: {:ok, t()} | :error
  def cast(callback_module, term) when is_atom(callback_module) do
    byte_count = callback_module.byte_count()

    case term do
      %__MODULE__{
        byte_count: ^byte_count,
        bytes: <<_::big-integer-size(byte_count)-unit(@bits_per_byte)>>
      } = cast ->
        {:ok, cast}

      <<_::big-integer-size(byte_count)-unit(@bits_per_byte)>> ->
        {:ok, %__MODULE__{byte_count: byte_count, bytes: term}}

      <<"0x", hexadecimal_digits::binary>> ->
        cast_hexadecimal_digits(hexadecimal_digits, byte_count)

      integer when is_integer(integer) ->
        cast_integer(integer, byte_count)

      _ ->
        :error
    end
  end

  @doc """
  Dumps the `t` `bytes` to `:binary` (`bytea`) format used in database.
  """
  @spec dump(module(), term()) :: {:ok, binary} | :error
  def dump(callback_module, term) when is_atom(callback_module) do
    byte_count = callback_module.byte_count()

    case term do
      # ensure inconsistent `t` with a different `byte_count` from the `callback_module` isn't dumped to the database,
      # in case `%__MODULE__{}` is set in a field value directly
      %__MODULE__{
        byte_count: ^byte_count,
        bytes: <<_::big-integer-size(byte_count)-unit(@bits_per_byte)>> = bytes
      } ->
        {:ok, bytes}

      _ ->
        :error
    end
  end

  @doc """
  Loads the binary hash from the database into `t:t/0` if it has `c:byte_count/0` bytes from `callback_module`.
  """
  @spec load(module(), term()) :: {:ok, t} | :error
  def load(callback_module, term) do
    byte_count = callback_module.byte_count()

    case term do
      # ensure that only hashes of `byte_count` that matches `callback_module` can be loaded back from database to
      # prevent using `Ecto.Type` with wrong byte_count on a database column
      <<_::big-integer-size(byte_count)-unit(@bits_per_byte)>> ->
        {:ok, %__MODULE__{byte_count: byte_count, bytes: term}}

      _ ->
        :error
    end
  end

  @doc """
  Converts the `t:t/0` to the integer version of the hash

      iex> Web3.Type.Hash.to_integer(
      ...>   %Web3.Type.Hash{
      ...>     byte_count: 32,
      ...>     bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b ::
      ...>              big-integer-size(32)-unit(8)>>
      ...>   }
      ...> )
      0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b
      iex> Web3.Type.Hash.to_integer(
      ...>   %Web3.Type.Hash{
      ...>     byte_count: 20,
      ...>     bytes: <<0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed :: big-integer-size(20)-unit(8)>>
      ...>   }
      ...> )
      0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed

  """
  @spec to_integer(t()) :: pos_integer()
  def to_integer(%__MODULE__{byte_count: byte_count, bytes: bytes}) do
    <<integer::big-integer-size(byte_count)-unit(8)>> = bytes

    integer
  end

  @doc """
  Converts the `t:t/0` to `iodata` representation shown to users.

      iex> %Web3.Type.Hash{
      ...>   byte_count: 32,
      ...>   bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b ::
      ...>            big-integer-size(32)-unit(8)>>
      ...> } |>
      ...> Web3.Type.Hash.to_iodata() |>
      ...> IO.iodata_to_binary()
      "0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b"

  Always pads number, so that it is a valid format for casting.

      iex> %Web3.Type.Hash{
      ...>   byte_count: 32,
      ...>   bytes: <<0x1234567890abcdef :: big-integer-size(32)-unit(8)>>
      ...> } |>
      ...> Web3.Type.Hash.to_iodata() |>
      ...> IO.iodata_to_binary()
      "0x0000000000000000000000000000000000000000000000001234567890abcdef"

  """
  @spec to_iodata(t) :: iodata()
  def to_iodata(%__MODULE__{byte_count: byte_count} = hash) do
    integer = to_integer(hash)
    hexadecimal_digit_count = byte_count_to_hexadecimal_digit_count(byte_count)
    unprefixed = :io_lib.format('~#{hexadecimal_digit_count}.16.0b', [integer])

    ["0x", unprefixed]
  end

  @doc """
  Converts the `t:t/0` to string representation shown to users.

      iex> Web3.Type.Hash.to_string(
      ...>   %Web3.Type.Hash{
      ...>     byte_count: 32,
      ...>     bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b ::
      ...>              big-integer-size(32)-unit(8)>>
      ...>   }
      ...> )
      "0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b"

  Always pads number, so that it is a valid format for casting.

      iex> Web3.Type.Hash.to_string(
      ...>   %Web3.Type.Hash{
      ...>     byte_count: 32,
      ...>     bytes: <<0x1234567890abcdef :: big-integer-size(32)-unit(8)>>
      ...>   }
      ...> )
      "0x0000000000000000000000000000000000000000000000001234567890abcdef"

  """
  @spec to_string(t) :: String.t()
  def to_string(%__MODULE__{} = hash) do
    hash
    |> to_iodata()
    |> IO.iodata_to_binary()
  end

  defp byte_count_to_hexadecimal_digit_count(byte_count) do
    byte_count * @hexadecimal_digits_per_byte
  end

  defp byte_count_to_max_integer(byte_count) do
    (1 <<< (byte_count * @bits_per_byte + 1)) - 1
  end

  defp cast_hexadecimal_digits(hexadecimal_digits, byte_count)
       when is_binary(hexadecimal_digits) do
    hexadecimal_digit_count = byte_count_to_hexadecimal_digit_count(byte_count)

    with ^hexadecimal_digit_count <- String.length(hexadecimal_digits),
         {:ok, bytes} <- Base.decode16(hexadecimal_digits, case: :mixed) do
      {:ok, %__MODULE__{byte_count: byte_count, bytes: bytes}}
    else
      _ -> :error
    end
  end

  defp cast_integer(integer, byte_count) when is_integer(integer) do
    max_integer = byte_count_to_max_integer(byte_count)

    case integer do
      in_range when 0 <= in_range and in_range <= max_integer ->
        {:ok,
         %__MODULE__{
           byte_count: byte_count,
           bytes: <<integer::big-integer-size(byte_count)-unit(@bits_per_byte)>>
         }}

      _ ->
        :error
    end
  end

  defimpl String.Chars do
    def to_string(hash) do
      @for.to_string(hash)
    end
  end

  defimpl Jason.Encoder do
    alias Jason.Encode

    def encode(hash, opts) do
      hash
      |> to_string()
      |> Encode.string(opts)
    end
  end
end