Skip to main content

lib/rujira/fin/pair.ex

defmodule Rujira.Fin.Pair do
  @moduledoc """
  Trading pair for the FIN protocol.

  Struct, construction, and queries. Use `Rujira.Fin` as the public API.
  """

  alias Rujira.Assets
  alias Rujira.Contracts
  alias Rujira.Deployments
  alias Rujira.Fin.Book
  alias Rujira.Logger
  alias Rujira.Math
  alias Rujira.Thorchain.Oracle

  use Memoize

  defstruct id: nil,
            address: nil,
            market_makers: [],
            token_base: nil,
            token_quote: nil,
            oracle_base: nil,
            oracle_quote: nil,
            tick: 0,
            fee_taker: Decimal.new(0),
            fee_maker: Decimal.new(0),
            fee_address: nil,
            book: :not_loaded,
            history: :not_loaded,
            summary: :not_loaded

  @type t :: %__MODULE__{
          id: String.t(),
          address: String.t(),
          market_makers: [String.t()],
          token_base: String.t(),
          token_quote: String.t(),
          oracle_base: Oracle.t() | nil,
          oracle_quote: Oracle.t() | nil,
          tick: integer(),
          fee_taker: Decimal.t(),
          fee_maker: Decimal.t(),
          fee_address: String.t(),
          book: :not_loaded | Book.t(),
          summary: :not_loaded | term()
        }

  # --- Construction ---

  @spec new(map()) :: {:ok, t()} | {:error, term()}

  def new(%{"market_maker" => nil} = attrs) do
    attrs |> Map.delete("market_maker") |> Map.put("market_makers", []) |> new()
  end

  def new(%{"market_maker" => market_maker} = attrs) do
    attrs |> Map.delete("market_maker") |> Map.put("market_makers", [market_maker]) |> new()
  end

  def new(%{
        "address" => address,
        "market_makers" => market_makers,
        "denoms" => denoms,
        "oracles" => oracles,
        "tick" => tick,
        "fee_taker" => fee_taker,
        "fee_maker" => fee_maker,
        "fee_address" => fee_address
      }) do
    with {:ok, fee_taker} <- Math.to_decimal(fee_taker),
         {:ok, fee_maker} <- Math.to_decimal(fee_maker),
         {:ok, oracle_base} <- oracle_from_config(Enum.at(oracles || [], 0)),
         {:ok, oracle_quote} <- oracle_from_config(Enum.at(oracles || [], 1)) do
      {:ok,
       %__MODULE__{
         id: address,
         address: address,
         market_makers: market_makers,
         token_base: Enum.at(denoms, 0),
         token_quote: Enum.at(denoms, 1),
         oracle_base: oracle_base,
         oracle_quote: oracle_quote,
         tick: tick,
         fee_taker: fee_taker,
         fee_maker: fee_maker,
         fee_address: fee_address,
         book: :not_loaded,
         summary: :not_loaded
       }}
    end
  end

  # --- Queries ---

  @spec get(String.t()) :: {:ok, t()} | {:error, term()}
  def get(address), do: Contracts.get({__MODULE__, address})

  @spec list() :: {:ok, [t()]} | {:error, term()}
  defmemo list do
    targets = Deployments.list_targets(__MODULE__)

    Rujira.Enum.reduce_while_ok(targets, [], fn %{module: module, address: address} ->
      case Contracts.get({module, address}) do
        {:ok, v} ->
          {:ok, v}

        {:error, err} ->
          Logger.error(__MODULE__, "#{address} error #{inspect(err)}")
          :skip
      end
    end)
  end

  @spec find_stable(String.t()) :: {:ok, t()} | {:error, term()}
  def find_stable(base_denom) do
    with {:ok, pairs} <- list(),
         %__MODULE__{} = pair <-
           Enum.find(
             pairs,
             &(&1.token_base == base_denom &&
                 (String.contains?(&1.token_quote, "usdc") ||
                    String.contains?(&1.token_quote, "usdt")))
           ) do
      {:ok, pair}
    else
      nil -> {:error, :not_found}
      err -> err
    end
  end

  @spec find_by_denoms(String.t(), String.t()) :: {:ok, t()} | {:error, term()}
  defmemo find_by_denoms(base_denom, quote_denom) do
    with {:ok, pairs} <- list(),
         %__MODULE__{} = pair <-
           Enum.find(
             pairs,
             &(&1.token_base == base_denom && &1.token_quote == quote_denom)
           ) do
      {:ok, pair}
    else
      nil -> {:error, :not_found}
      err -> err
    end
  end

  @spec from_id(String.t()) :: {:ok, t()} | {:error, term()}
  def from_id("sthor" <> _ = address), do: get(address)
  def from_id("thor" <> _ = address), do: get(address)

  def from_id(assets) do
    with {:ok, pair} <- lookup(assets) do
      {:ok, %{pair | id: assets}}
    end
  end

  @spec ticker_id!(t()) :: String.t()
  def ticker_id!(%__MODULE__{token_base: token_base, token_quote: token_quote}) do
    {:ok, base} = Assets.from_denom(token_base)
    {:ok, target} = Assets.from_denom(token_quote)

    "#{Assets.label(base)}_#{Assets.label(target)}"
  end

  @spec tvl(String.t()) :: {:ok, non_neg_integer()} | {:error, term()}
  def tvl(address) do
    case get(address) do
      {:ok, %__MODULE__{market_makers: mms} = pair} ->
        mm_tvl = mms |> Enum.map(&mm_tvl_or_zero/1) |> Enum.sum()

        case Rujira.Fin.Range.tvl(pair) do
          {:ok, r_tvl} -> {:ok, mm_tvl + r_tvl}
          {:error, _} = err -> err
        end

      _ ->
        {:ok, 0}
    end
  end

  # --- Deployment protocol ---

  @spec init_msg(map()) :: map()
  def init_msg(
        %{
          "denoms" => [x, y],
          "fee_address" => fee_address
        } = config
      ) do
    market_makers = Map.get(config, "market_makers")
    tick = Map.get(config, "tick", 6)

    with {:ok, base} <- Assets.from_denom(x),
         {:ok, quote_} <- Assets.from_denom(y) do
      %{
        denoms: [x, y],
        oracles: [
          %{chain: base.chain, symbol: base.symbol},
          %{chain: quote_.chain, symbol: quote_.symbol}
        ],
        market_makers: market_makers,
        tick: tick,
        fee_taker: "0.0015",
        fee_maker: "0.00075",
        fee_address: fee_address
      }
    else
      _ ->
        %{
          denoms: [x, y],
          market_makers: market_makers,
          tick: tick,
          fee_taker: "0.0015",
          fee_maker: "0.00075",
          fee_address: fee_address
        }
    end
  end

  @spec migrate_msg(term(), term(), term()) :: map()
  def migrate_msg(_from, _to, _), do: %{}

  @spec init_label(term(), map()) :: String.t()
  def init_label(_, %{"denoms" => [x, y]}), do: "rujira-fin:#{x}:#{y}"

  # --- Private ---

  defp oracle_from_config(%{"chain" => chain, "symbol" => symbol}),
    do: oracle_from_config(%{chain: chain, symbol: symbol})

  defp oracle_from_config(%{chain: chain, symbol: symbol}) do
    id = String.upcase(chain) <> "." <> symbol
    {:ok, %Oracle{id: id, symbol: id, asset: Assets.from_string(id)}}
  end

  defp oracle_from_config(str) when is_binary(str) do
    {:ok, %Oracle{id: String.upcase(str), symbol: String.upcase(str)}}
  end

  defp oracle_from_config(nil), do: {:ok, nil}

  defp mm_tvl_or_zero(mm) do
    case get_mm_tvl(mm) do
      {:ok, tvl} -> tvl
      _ -> 0
    end
  end

  defp get_mm_tvl(mm) do
    with {:ok, %Deployments.Target{module: module}} <- Deployments.from_address(mm),
         {:ok, pool} <- module.pool_from_id(mm) do
      {:ok, module.tvl(pool)}
    end
  end

  defp lookup(assets) do
    with [b, q] <- String.split(assets, "/"),
         {:ok, pairs} <- list(),
         %__MODULE__{} = pair <-
           Enum.find(
             pairs,
             &(Assets.eq_denom(
                 Assets.from_shortcode(b),
                 &1.token_base
               ) and
                 Assets.eq_denom(
                   Assets.from_shortcode(q),
                   &1.token_quote
                 ))
           ) do
      {:ok, pair}
    else
      nil -> {:error, :not_found}
      _ -> {:error, :invalid_id}
    end
  end
end