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 <> = 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: <> }} _ -> :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