Skip to main content

lib/rujira/events.ex

defmodule Rujira.Events do
  @moduledoc """
  Generic event parser for all Rujira events.

  Takes a raw event, creates a default `Event` struct, then routes it
  to the correct protocol parser. Each protocol returns an envelope
  struct so consumers can match at protocol, type, or field level.

  ## Usage

      case Rujira.Events.parse(raw_event) do
        # Match all FIN events
        {:ok, %Rujira.Fin.Events.Event{} = e} -> handle_fin(e)

        # Match a specific FIN event by inner struct
        {:ok, %Rujira.Fin.Events.Event{data: %Rujira.Fin.Events.Trade{} = trade}} -> ...

        # Match all Thorchain events
        {:ok, %Rujira.Thorchain.Events.Event{} = e} -> handle_tc(e)

        # Unrecognized protocol
        {:ok, %Rujira.Events.Event{} = event} -> handle_unknown(event)
      end
  """

  alias Rujira.Events.Event
  alias Thorchain.Types.BlockEvent

  @doc """
  Casts a raw `BlockEvent` protobuf struct into the standard
  `%{type: String.t(), attributes: map()}` format.
  """
  @spec cast(BlockEvent.t()) :: %{type: String.t(), attributes: map()}
  def cast(%BlockEvent{
        event_kv_pair: [
          %{key: "type", value: type}
          | attributes
        ]
      }) do
    attrs =
      Enum.reduce(attributes, %{}, fn %{key: key, value: value}, acc ->
        Map.put(acc, key, value)
      end)

    %{type: type, attributes: attrs}
  end

  @doc """
  Parses a raw event into a typed struct from the matching protocol.

  Accepts:
  - `%{type: String.t(), attributes: map()}` — already cast
  - `%BlockEvent{}` — raw protobuf, cast first

  Returns `{:ok, struct}` for known events or `{:ok, %Event{}}` for
  unrecognized events so consumers never lose data.
  """
  alias Rujira.Fin.Events.Event, as: FinEvent
  alias Rujira.Thorchain.Events.Event, as: TcEvent

  @spec parse(map() | BlockEvent.t()) ::
          {:ok, FinEvent.t() | TcEvent.t() | Event.t()} | {:error, term()}

  def parse(%BlockEvent{} = event), do: event |> cast() |> parse()

  def parse(%{type: type, attributes: attrs}) do
    event = Event.new(type, attrs)
    route(event)
  end

  def parse(_), do: {:error, :invalid_event}

  defp route(%Event{type: "wasm-rujira-fin/" <> _} = event),
    do: Rujira.Fin.Events.parse(event)

  defp route(%Event{type: type} = event)
       when type in ~w(swap transfer add_liquidity withdraw pending_liquidity oracle_price bond rebond set_mimir),
       do: Rujira.Thorchain.Events.parse(event)

  # --- Not yet implemented ---
  # defp route(%Event{type: "wasm-rujira-bow/" <> _} = event), do: Rujira.Bow.Events.parse(event)
  # defp route(%Event{type: "wasm-rujira-ghost-vault/" <> _} = event), do: Rujira.Ghost.Events.parse(event)
  # defp route(%Event{type: "wasm-rujira-ghost-credit/" <> _} = event), do: Rujira.Ghost.Events.parse(event)
  # defp route(%Event{type: "wasm-rujira-staking/" <> _} = event), do: Rujira.Staking.Events.parse(event)
  # defp route(%Event{type: "wasm-rujira-merge/" <> _} = event), do: Rujira.Merge.Events.parse(event)
  # defp route(%Event{type: "wasm-rujira-brune/" <> _} = event), do: Rujira.Brune.Events.parse(event)
  # defp route(%Event{type: "wasm-rujira-thorchain-swap/" <> _} = event), do: Rujira.Thorchain.Swap.Events.parse(event)
  # defp route(%Event{type: "wasm-rujira-ventures-factory/" <> _} = event), do: Rujira.Keiko.Events.parse(event)
  # defp route(%Event{type: "wasm-calc-" <> _} = event), do: Rujira.Calc.Events.parse(event)

  defp route(%Event{} = event), do: {:ok, event}
end