lib/siwe.ex

defmodule Siwe do
  @moduledoc """
  Siwe provides validation and parsing for Sign-In with Ethereum messages and signatures.
  """

  use Rustler, otp_app: :siwe, crate: "siwe_ex"

  # The result of the Siwe.parse and parse_if_valid, a formatted form
  # of what siwe-rs uses
  defmodule Message do
    defstruct domain: "",
              address: "",
              # or a string statement.
              statement: nil,
              uri: "",
              version: "",
              chain_id: "",
              nonce: "",
              issued_at: "",
              # or a string datetime
              expiration_time: nil,
              # or a string datetime
              not_before: nil,
              # or string
              request_id: nil,
              resources: []
  end

  @doc """
    Parses a Sign In With Ethereum message string into the Message struct, or reports an error
  """
  @spec parse(String.t()) :: {:ok | :error, Message.t() | String.t()}
  def parse(_msg) do
    {:error, "NIF not loaded"}
  end

  @doc """
    Converts a Message struct to a Sign In With Ethereum message string, or reports an error
  """
  @spec to_str(Message.t()) :: {:ok | :error, String.t()}
  def to_str(_msg) do
    {:error, "NIF not loaded"}
  end

  @doc """
    Given a Message struct and a signature, returns true if the Message.address
    signing the Message would produce the signature.
  """
  @spec verify_sig(Message.t(), String.t()) :: boolean()
  def verify_sig(_msg, _sig) do
    :erlang.nif_error(:nif_not_loaded)
  end

  @doc """
    Given a Message, signature, and optionally, domain, nonce and timestamp, returns true if:
    the current time or timestamp, if provided, is between the messages' not_before and expiration_time
    the Message.address signing the Message would produce the signature.
    the domain, if provided, matches Message.domain
    the nonce, if provided, matches Message.nonce
  """
  @spec verify(Message.t(), String.t(), String.t() | nil.t(), String.t() | nil.t(), String.t() | nil.t()) :: boolean()
  def verify(_msg, _sig, _domain_binding, _match_nonce, _timestamp) do
    :erlang.nif_error(:nif_not_loaded)
  end

  @doc """
   Tests that a message and signature pair correspond and that the current
   time is valid (after not_before, and before expiration_time)

   Returns a Message structure based on the passed message

   ## Examples
    iex> Siwe.parse_if_valid(Enum.join(["login.xyz wants you to sign in with your Ethereum account:",
    ...> "0xfA151B5453CE69ABf60f0dbdE71F6C9C5868800E",
    ...> "",
    ...> "Sign-In With Ethereum Example Statement",
    ...> "",
    ...> "URI: https://login.xyz",
    ...> "Version: 1",
    ...> "Chain ID: 1",
    ...> "Nonce: ToTaLLyRanDOM",
    ...> "Issued At: 2021-12-17T00:38:39.834Z",
    ...> ], "\\n"),
    ...> "0x8d1327a1abbdf172875e5be41706c50fc3bede8af363b67aefbb543d6d082fb76a22057d7cb6d668ceba883f7d70ab7f1dc015b76b51d226af9d610fa20360ad1c")
    {:ok, %{ __struct__: Siwe, address: "0xfA151B5453CE69ABf60f0dbdE71F6C9C5868800E", chain_id: 1, domain: "login.xyz", expiration_time: nil, issued_at: "2021-12-17T00:38:39.834Z", nonce: "ToTaLLyRanDOM", not_before: nil, request_id: nil, resources: [], statement: "Sign-In With Ethereum Example Statement", uri: "https://login.xyz", version: "1" }}
  """
  @spec parse_if_valid(String.t(), String.t()) :: {:ok | :error, Message.t() | String.t()}
  def parse_if_valid(_msg, _sig) do
    {:error, "NIF not loaded"}
  end

  @doc """
  Generates an alphanumeric nonce for use in SIWE messages.
  """
  @spec generate_nonce() :: String.t()
  def generate_nonce() do
    :erlang.nif_error(:nif_not_loaded)
  end
end