lib/tx_build/preconditions.ex

defmodule Stellar.TxBuild.Preconditions do
  @moduledoc """
  `Preconditions` struct definition.
  """
  alias Stellar.TxBuild.{
    TimeBounds,
    LedgerBounds,
    SequenceNumber,
    SignerKey,
    OptionalSequenceNumber
  }

  alias StellarBase.XDR.TimeBounds, as: TimeBoundsXDR

  alias StellarBase.XDR.{
    Preconditions,
    PreconditionsV2,
    PreconditionType,
    Duration,
    SignerKeyList,
    TimePoint,
    UInt32,
    Void
  }

  @behaviour Stellar.TxBuild.XDR

  @type precond_v2 :: [
          time_bounds: TimeBounds.t(),
          ledger_bounds: LedgerBounds.t(),
          min_seq_num: OptionalSequenceNumber.t(),
          min_seq_age: non_neg_integer(),
          min_seq_ledger_gap: non_neg_integer(),
          extra_signers: list(SignerKey.t())
        ]
  @type valid :: non_neg_integer() | struct() | list(struct())
  @type validation :: {:ok, valid()} | {:error, atom()}
  @type type :: :precond_v2 | :precond_time | :none
  @type preconditions :: precond_v2() | TimeBounds.t() | nil

  @type t :: %__MODULE__{type: type(), preconditions: preconditions()}

  defstruct [:type, :preconditions]

  @impl true
  def new(preconditions \\ [], opts \\ [])

  def new([], _opts) do
    %__MODULE__{type: :none, preconditions: nil}
  end

  def new([time_bounds: %TimeBounds{} = time_bounds], _opts) do
    %__MODULE__{type: :precond_time, preconditions: time_bounds}
  end

  def new(preconditions, _opts) do
    time_bounds = Keyword.get(preconditions, :time_bounds)
    ledger_bounds = Keyword.get(preconditions, :ledger_bounds)
    min_seq_num = Keyword.get(preconditions, :min_seq_num)
    min_seq_age = Keyword.get(preconditions, :min_seq_age, 0)
    min_seq_ledger_gap = Keyword.get(preconditions, :min_seq_ledger_gap, 0)
    extra_signers = Keyword.get(preconditions, :extra_signers, [])

    with {:ok, time_bounds} <- validate_time_bounds(time_bounds),
         {:ok, ledger_bounds} <- validate_ledger_bounds(ledger_bounds),
         {:ok, min_seq_num} <- validate_optional_min_seq_num(min_seq_num),
         {:ok, min_seq_age} <- validate_min_seq_age(min_seq_age),
         {:ok, min_seq_ledger_gap} <- validate_min_seq_ledger_gap(min_seq_ledger_gap),
         {:ok, extra_signers} <- validate_extra_signers([], extra_signers) do
      %__MODULE__{
        type: :precond_v2,
        preconditions: [
          time_bounds: time_bounds,
          ledger_bounds: ledger_bounds,
          min_seq_num: min_seq_num,
          min_seq_age: min_seq_age,
          min_seq_ledger_gap: min_seq_ledger_gap,
          extra_signers: extra_signers
        ]
      }
    end
  end

  @impl true
  def to_xdr(%__MODULE__{type: :none, preconditions: nil}) do
    preconditions_type = PreconditionType.new(:PRECOND_NONE)

    Preconditions.new(Void.new(), preconditions_type)
  end

  def to_xdr(%__MODULE__{
        type: :precond_time,
        preconditions: %TimeBounds{min_time: min_time, max_time: max_time}
      }) do
    preconditions_type = PreconditionType.new(:PRECOND_TIME)

    min_time = TimePoint.new(min_time)
    max_time = TimePoint.new(max_time)

    min_time
    |> TimeBoundsXDR.new(max_time)
    |> Preconditions.new(preconditions_type)
  end

  def to_xdr(%__MODULE__{
        type: :precond_v2,
        preconditions: [
          time_bounds: time_bounds,
          ledger_bounds: ledger_bounds,
          min_seq_num: min_seq_num,
          min_seq_age: min_seq_age,
          min_seq_ledger_gap: min_seq_ledger_gap,
          extra_signers: extra_signers
        ]
      }) do
    preconditions_type = PreconditionType.new(:PRECOND_V2)

    time_bounds = TimeBounds.to_xdr(time_bounds)
    ledger_bounds = LedgerBounds.to_xdr(ledger_bounds)
    min_seq_num = OptionalSequenceNumber.to_xdr(min_seq_num)
    min_seq_age = Duration.new(min_seq_age)
    min_seq_ledger_gap = UInt32.new(min_seq_ledger_gap)
    extra_signers_list = get_extra_signers_list(extra_signers)

    time_bounds
    |> PreconditionsV2.new(
      ledger_bounds,
      min_seq_num,
      min_seq_age,
      min_seq_ledger_gap,
      extra_signers_list
    )
    |> Preconditions.new(preconditions_type)
  end

  @spec get_extra_signers_list(extra_signers :: list(SignerKey.t())) :: SignerKeyList.t()
  defp get_extra_signers_list(extra_signers) do
    extra_signers
    |> Enum.map(&SignerKey.to_xdr(&1))
    |> SignerKeyList.new()
  end

  @spec validate_time_bounds(time_bounds :: TimeBounds.t()) :: validation()
  defp validate_time_bounds(%TimeBounds{} = time_bounds), do: {:ok, time_bounds}
  defp validate_time_bounds(_time_bounds), do: {:error, :invalid_time_bounds}

  @spec validate_ledger_bounds(ledger_bounds :: LedgerBounds.t()) :: validation()
  defp validate_ledger_bounds(%LedgerBounds{} = ledger_bounds),
    do: {:ok, ledger_bounds}

  defp validate_ledger_bounds(_ledger_bounds), do: {:error, :invalid_ledger_bounds}

  @spec validate_optional_min_seq_num(min_seq_num :: SequenceNumber.t() | nil) :: validation()
  defp validate_optional_min_seq_num(nil),
    do: {:ok, OptionalSequenceNumber.new()}

  defp validate_optional_min_seq_num(%SequenceNumber{} = min_seq_num),
    do:
      min_seq_num
      |> OptionalSequenceNumber.new()
      |> (&{:ok, &1}).()

  defp validate_optional_min_seq_num(_min_seq_num), do: {:error, :invalid_min_seq_num}

  @spec validate_min_seq_age(min_seq_age :: non_neg_integer()) :: validation()
  defp validate_min_seq_age(min_seq_age)
       when is_integer(min_seq_age) and min_seq_age >= 0,
       do: {:ok, min_seq_age}

  defp validate_min_seq_age(_min_seq_age), do: {:error, :invalid_min_seq_age}

  @spec validate_min_seq_ledger_gap(min_seq_ledger_gap :: non_neg_integer()) :: validation()
  defp validate_min_seq_ledger_gap(min_seq_ledger_gap)
       when is_integer(min_seq_ledger_gap) and min_seq_ledger_gap >= 0,
       do: {:ok, min_seq_ledger_gap}

  defp validate_min_seq_ledger_gap(_min_seq_ledger_gap), do: {:error, :invalid_min_seq_ledger_gap}

  @spec validate_extra_signers(
          response :: list(String.t()),
          extra_signers :: list(String.t())
        ) :: validation()
  def validate_extra_signers(response, []), do: {:ok, response}

  def validate_extra_signers(response, [h | t]) do
    case SignerKey.new(h) do
      %SignerKey{} = signer_key -> validate_extra_signers(response ++ [signer_key], t)
      _error -> {:error, :invalid_extra_signers}
    end
  end

  def validate_extra_signers(_response, _extra_signers),
    do: {:error, :invalid_extra_signers}
end