lib/tx_build/invoke_host_function.ex

defmodule Stellar.TxBuild.InvokeHostFunction do
  @moduledoc """
  Performs the following operations:
  - Invokes contract host_functions.
  - Upload WASM of the new contracts.
  - Deploys new contracts using the uploaded WASM or built-in implementations.
  """

  import Stellar.TxBuild.Validations, only: [validate_optional_account: 1]

  alias Stellar.TxBuild.{HostFunction, SorobanAuthorizationEntry, OptionalAccount}

  alias StellarBase.XDR.Operations.InvokeHostFunction

  alias StellarBase.XDR.{
    OperationBody,
    OperationType,
    SorobanAuthorizationEntryList
  }

  alias StellarBase.XDR.SorobanAuthorizationEntry, as: SorobanAuthorizationEntryXDR

  @behaviour Stellar.TxBuild.XDR

  @type auths :: list(SorobanAuthorizationEntry.t()) | list(String.t())
  @type error :: {:error, atom()}
  @type host_function :: HostFunction.t()
  @type soroban_auth_xdr :: SorobanAuthorizationEntryXDR.t()
  @type validation :: {:ok, any()} | {:error, atom()}

  @type t :: %__MODULE__{
          host_function: host_function(),
          auths: auths(),
          source_account: OptionalAccount.t()
        }

  defstruct [:host_function, :auths, :source_account]

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

  def new(args, _opts) when is_list(args) do
    host_function = Keyword.get(args, :host_function)
    auths = Keyword.get(args, :auths, [])
    source_account = Keyword.get(args, :source_account)

    with {:ok, host_function} <- validate_host_host_function(host_function),
         {:ok, auths} <- validate_soroban_auth_entries(auths),
         {:ok, source_account} <- validate_optional_account({:source_account, source_account}) do
      %__MODULE__{
        host_function: host_function,
        auths: auths,
        source_account: source_account
      }
    end
  end

  def new(_args, _opts), do: {:error, :invalid_operation_attributes}

  @impl true
  def to_xdr(%__MODULE__{
        host_function: host_function,
        auths: [%SorobanAuthorizationEntry{} | _] = auths
      }) do
    op_type = OperationType.new(:INVOKE_HOST_FUNCTION)

    auth =
      auths
      |> Enum.map(&SorobanAuthorizationEntry.to_xdr/1)
      |> SorobanAuthorizationEntryList.new()

    host_function
    |> HostFunction.to_xdr()
    |> InvokeHostFunction.new(auth)
    |> OperationBody.new(op_type)
  end

  def to_xdr(%__MODULE__{
        host_function: host_function,
        auths: auths
      })
      when is_list(auths) do
    op_type = OperationType.new(:INVOKE_HOST_FUNCTION)

    auth =
      auths
      |> Enum.map(&decode_soroban_auth/1)
      |> SorobanAuthorizationEntryList.new()

    host_function
    |> HostFunction.to_xdr()
    |> InvokeHostFunction.new(auth)
    |> OperationBody.new(op_type)
  end

  @spec set_auth(module :: t(), auths :: auths()) :: t() | error()
  def set_auth(%__MODULE__{} = module, auths) when is_list(auths) do
    with {:ok, auths} <- validate_auth_strings(auths) do
      %{module | auths: auths}
    end
  end

  def set_auth(_module, _auths), do: {:error, :invalid_auth}

  defp validate_auth_strings(auth) do
    if Enum.all?(auth, &validate_xdr_string/1),
      do: {:ok, auth},
      else: {:error, :invalid_auth}
  end

  @spec validate_xdr_string(xdr :: String.t() | nil) :: boolean()
  defp validate_xdr_string(nil), do: true

  defp validate_xdr_string(xdr) when is_binary(xdr) do
    case Base.decode64(xdr) do
      {:ok, _} -> true
      :error -> false
    end
  end

  @spec validate_host_host_function(host_function :: host_function()) :: validation()
  defp validate_host_host_function(%HostFunction{} = host_function), do: {:ok, host_function}
  defp validate_host_host_function(_host_function), do: {:error, :invalid_host_host_function}

  defp validate_soroban_auth_entries(auths) when is_list(auths) do
    if Enum.all?(auths, &is_soroban_auth_entry?/1),
      do: {:ok, auths},
      else: {:error, :invalid_soroban_auth_entries}
  end

  defp validate_soroban_auth_entries(_auths), do: {:error, :invalid_soroban_auth_entries}

  defp is_soroban_auth_entry?(%SorobanAuthorizationEntry{}), do: true
  defp is_soroban_auth_entry?(_auth), do: false

  @spec decode_soroban_auth(auth :: String.t()) :: soroban_auth_xdr()
  defp decode_soroban_auth(auth) do
    {contract_auth, ""} =
      auth
      |> Base.decode64!()
      |> SorobanAuthorizationEntryXDR.decode_xdr!()

    contract_auth
  end
end