lib/tx_build/invoke_host_function.ex

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

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

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

  alias StellarBase.XDR.Operations.InvokeHostFunction

  alias StellarBase.XDR.{
    OperationBody,
    OperationType,
    LedgerFootprint,
    ContractAuthList,
    LedgerKeyList
  }

  @behaviour Stellar.TxBuild.XDR

  @type validation :: {:ok, any()} | {:error, atom()}

  @type t :: %__MODULE__{
          function: HostFunction.t(),
          footprint: String.t(),
          auth: list(ContractAuth.t()),
          source_account: OptionalAccount.t()
        }

  defstruct [:function, :footprint, :auth, :source_account]

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

  def new(args, _opts) when is_list(args) do
    function = Keyword.get(args, :function)
    footprint = Keyword.get(args, :footprint)
    auth = Keyword.get(args, :auth)
    source_account = Keyword.get(args, :source_account)

    with {:ok, function} <- validate_function(function),
         {:ok, footprint} <- validate_footprint({:footprint, footprint}),
         {:ok, source_account} <- validate_optional_account({:source_account, source_account}),
         {:ok, auth} <- validate_auth(auth) do
      %__MODULE__{
        function: function,
        footprint: footprint,
        auth: auth,
        source_account: source_account
      }
    end
  end

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

  @impl true
  def to_xdr(%__MODULE__{
        function: function,
        footprint: nil,
        auth: nil
      }) do
    op_type = OperationType.new(:INVOKE_HOST_FUNCTION)
    host_function = HostFunction.to_xdr(function)
    ledger_key_list = LedgerKeyList.new([])
    ledger_footprint = LedgerFootprint.new(ledger_key_list, ledger_key_list)
    contract_auth_list = ContractAuthList.new([])

    host_function
    |> InvokeHostFunction.new(ledger_footprint, contract_auth_list)
    |> OperationBody.new(op_type)
  end

  def to_xdr(%__MODULE__{
        function: function,
        footprint: nil,
        auth: auth
      }) do
    op_type = OperationType.new(:INVOKE_HOST_FUNCTION)
    host_function = HostFunction.to_xdr(function)
    ledger_key_list = LedgerKeyList.new([])
    ledger_footprint = LedgerFootprint.new(ledger_key_list, ledger_key_list)
    contract_auth_list = auth |> Enum.map(&ContractAuth.to_xdr/1) |> ContractAuthList.new()

    host_function
    |> InvokeHostFunction.new(ledger_footprint, contract_auth_list)
    |> OperationBody.new(op_type)
  end

  def to_xdr(%__MODULE__{
        function: function,
        footprint: footprint,
        auth: nil
      }) do
    op_type = OperationType.new(:INVOKE_HOST_FUNCTION)
    host_function = HostFunction.to_xdr(function)

    {ledger_footprint, _} =
      footprint
      |> Base.decode64!()
      |> LedgerFootprint.decode_xdr!()

    contract_auth_list = ContractAuthList.new([])

    host_function
    |> InvokeHostFunction.new(ledger_footprint, contract_auth_list)
    |> OperationBody.new(op_type)
  end

  def to_xdr(%__MODULE__{
        function: function,
        footprint: footprint,
        auth: auth
      }) do
    op_type = OperationType.new(:INVOKE_HOST_FUNCTION)
    host_function = HostFunction.to_xdr(function)

    {ledger_footprint, _} =
      footprint
      |> Base.decode64!()
      |> LedgerFootprint.decode_xdr!()

    contract_auth_list = auth |> Enum.map(&ContractAuth.to_xdr/1) |> ContractAuthList.new()

    host_function
    |> InvokeHostFunction.new(ledger_footprint, contract_auth_list)
    |> OperationBody.new(op_type)
  end

  @spec set_footprint(module :: t(), footprint :: String.t()) :: t()
  def set_footprint(%__MODULE__{} = module, footprint) do
    with {:ok, footprint} <- validate_footprint({:footprint, footprint}) do
      %{module | footprint: footprint}
    end
  end

  @spec validate_function(function :: HostFunction.t()) :: validation()
  defp validate_function(%HostFunction{} = function), do: {:ok, function}
  defp validate_function(_), do: {:error, :invalid_function}

  @spec validate_footprint(tuple :: tuple()) :: validation()
  defp validate_footprint({:footprint, nil}), do: {:ok, nil}

  defp validate_footprint({:footprint, footprint}) when is_binary(footprint) do
    case Base.decode64(footprint) do
      {:ok, _} -> {:ok, footprint}
      :error -> {:error, :invalid_footprint}
    end
  end

  defp validate_footprint({:footprint, _}), do: {:error, :invalid_footprint}

  @spec validate_auth(function :: list(ContractAuth.t())) :: validation()
  defp validate_auth([%ContractAuth{} | _] = auth), do: {:ok, auth}
  defp validate_auth(nil), do: {:ok, nil}
  defp validate_auth(_), do: {:error, :invalid_auth}
end