lib/fast_eip55.ex

defmodule FastEIP55 do
  @moduledoc """
  Github link: https://github.com/gregors/fast_eip_55

  Rust-powered Keccak version of EIP-55

  Provides EIP-55 encoding and validation functions.
  """

  @doc """
  Encodes an Ethereum address into an EIP-55 checksummed address.

  ## Examples

  iex> alias FastEIP55, as: EIP55
  iex> EIP55.encode("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed")
  {:ok, "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"}

  iex> EIP55.encode(<<90, 174, 182, 5, 63, 62, 148, 201, 185, 160, 159, 51, 102, 148, 53, 231, 239, 27, 234, 237>>)
  {:ok, "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"}

  iex> EIP55.encode("not an address")
  {:error, :unrecognized_address_format}

  """
  def encode("0x" <> address) when byte_size(address) == 40 do
    address = String.downcase(address, :ascii)

    hash =
      address
      |> ExKeccak.hash_256()
      |> Base.encode16(case: :lower)

    encoded = _encode(address, hash)
    {:ok, encoded}
  end

  def encode(address) when byte_size(address) == 20 do
    encode("0x" <> Base.encode16(address, case: :lower))
  end

  def encode(_) do
    {:error, :unrecognized_address_format}
  end

  defp checksum(c, _) when c >= 48 and c <= 57, do: c

  defp checksum(c, x) when (x >= 97 and x <= 102) or x == 56 or x == 57 do
    c - 32
  end

  defp checksum(c, _), do: c

  defp _encode(address, hash) do
    address = :binary.bin_to_list(address)
    hash = :binary.bin_to_list(hash)

    _encode(address, hash, ['x', '0'])
    |> :lists.reverse()
    |> :binary.list_to_bin()
  end

  defp _encode([], _, acc), do: acc

  defp _encode([a | address], [b | hash], acc) do
    _encode(address, hash, [checksum(a, b) | acc])
  end

  @doc """
  Determines whether the given Ethereum address has a valid EIP-55 checksum.

  ## Examples

  iex> alias FastEIP55, as: EIP55
  iex> EIP55.valid?("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed")
  true

  iex> EIP55.valid?("0x5AAEB6053f3e94c9b9a09f33669435e7ef1beaed")
  false

  iex> EIP55.valid?("not an address")
  false
  """
  def valid?("0x" <> _ = address) when byte_size(address) == 42 do
    case encode(address) do
      {:ok, ^address} -> true
      _ -> false
    end
  end

  def valid?(_), do: false
end