Skip to main content

lib/rujira/deployments.ex

defmodule Rujira.Deployments do
  @moduledoc """
  Resolves deployed Rujira contracts from THORChain's contract-info index.

  Queries `Thorchain.Types.Query.Stub.contract_infos/2` via the configured
  `Rujira.Node` implementation and maps each on-chain `ContractInfo` to a
  `Rujira.Deployments.Target`.

  ## Configuration

      config :rujira_ex,
        # Map of contract-name string -> module implementing the resource.
        # Rujira's own protocols (e.g. "rujira-fin") are mapped by default;
        # consumers add their own entries here.
        protocol_modules: %{"rujira-bow" => MyApp.Bow},

        # Addresses to exclude from the resolved target list (e.g. legacy
        # or unmaintained deployments).
        deployments_omit: []

  Consumers wire up cache invalidation themselves by calling
  `invalidate/0` whenever a `MsgInstantiateContract`, `MsgInstantiateContract2`
  or `MsgMigrateContract` is observed.
  """

  alias Rujira.Deployments.Target
  alias Thorchain.Types.ContractInfo
  alias Thorchain.Types.Query.Stub
  alias Thorchain.Types.QueryContractInfosRequest

  use Memoize

  @spec contract_infos() :: {:ok, [ContractInfo.t()]} | {:error, term()}
  defmemo contract_infos do
    with {:ok, %{infos: infos}} <-
           Rujira.Node.query(&Stub.contract_infos/2, %QueryContractInfosRequest{}) do
      {:ok, Enum.reject(infos, &(&1.address in omit()))}
    end
  end

  @spec get_target(module()) :: Target.t() | nil
  defmemo get_target(module) do
    case list_all_targets() do
      {:ok, targets} -> Enum.find(targets, &(&1.module === module))
      _ -> nil
    end
  end

  @spec from_address(String.t()) :: {:ok, Target.t()} | {:error, term()}
  defmemo from_address(address) do
    with {:ok, targets} <- list_all_targets() do
      case Enum.find(targets, &(&1.address == address)) do
        nil -> {:error, :not_found}
        target -> {:ok, target}
      end
    end
  end

  @spec list_all_targets() :: {:ok, [Target.t()]} | {:error, term()}
  defmemo list_all_targets do
    with {:ok, infos} <- contract_infos() do
      Rujira.Enum.reduce_while_ok(infos, [], &target/1)
    end
  end

  @doc "List all targets for a given module."
  @spec list_targets(module()) :: [Target.t()]
  defmemo list_targets(module) do
    case list_all_targets() do
      {:ok, targets} -> Enum.filter(targets, &(&1.module === module))
      _ -> []
    end
  end

  @doc "Invalidate all memoized deployment metadata."
  @spec invalidate() :: :ok
  def invalidate do
    Memoize.invalidate(__MODULE__, :contract_infos)
    Memoize.invalidate(__MODULE__, :get_target)
    Memoize.invalidate(__MODULE__, :from_address)
    Memoize.invalidate(__MODULE__, :list_all_targets)
    Memoize.invalidate(__MODULE__, :list_targets)
    :ok
  end

  # --- Private ---

  defp target(%{contract: name, version: version, address: address} = info) do
    case module_from(info) do
      {:ok, module} ->
        {:ok,
         %Target{
           id: address,
           address: address,
           module: module,
           name: name,
           version: version
         }}

      _ ->
        :skip
    end
  end

  defp module_from(%{contract: "rujira-fin"}), do: {:ok, Rujira.Fin.Pair}

  defp module_from(%{contract: name}) do
    case Map.get(protocol_modules(), name) do
      nil -> {:error, :unknown_protocol}
      module -> {:ok, module}
    end
  end

  defp protocol_modules, do: Application.get_env(:rujira_ex, :protocol_modules, %{})

  defp omit, do: Application.get_env(:rujira_ex, :deployments_omit, [])
end