lib/contract/invoke_host_function.ex

defmodule Soroban.Contract.InvokeHostFunction do
  @moduledoc """
  `InvokeHostFunction` implementation to invoke authorized and not authorized contract functions.
  """

  alias Soroban.RPC
  alias Soroban.RPC.{SendTransactionResponse, SimulateTransactionResponse}
  alias Stellar.{Horizon.Accounts, TxBuild}

  alias Stellar.TxBuild.{
    Account,
    ContractAuth,
    HostFunction,
    InvokeHostFunction,
    SCVal,
    SequenceNumber,
    Signature
  }

  @type account :: Account.t()
  @type function_args :: list(struct())
  @type auth :: String.t() | nil
  @type auth_account :: String.t() | nil
  @type auth_accounts :: list(binary())
  @type invoke_host_function :: InvokeHostFunction.t()
  @type function_name :: String.t()
  @type contract_id :: binary()
  @type source_secret_key :: binary()
  @type simulate_response :: {:ok, SimulateTransactionResponse.t()}
  @type send_response :: {:ok, SendTransactionResponse.t()}
  @type signature :: Signature.t()
  @type sequence_number :: SequenceNumber.t()
  @type sc_val_list :: list(SCVal.t())

  @spec invoke(
          contract_id :: contract_id(),
          source_secret_key :: source_secret_key(),
          function_name :: function_name(),
          function_args :: function_args(),
          auth_accounts :: auth_accounts()
        ) :: send_response()
  def invoke(
        contract_id,
        source_secret_key,
        function_name,
        function_args,
        auth_accounts \\ []
      ) do
    with {public_key, _secret} = keypair <- Stellar.KeyPair.from_secret_seed(source_secret_key),
         {:ok, seq_num} <- Accounts.fetch_next_sequence_number(public_key),
         {:ok, function_args} <- convert_to_sc_val(function_args),
         signature <- Signature.new(keypair),
         source_account <- Account.new(public_key),
         sequence_number <- SequenceNumber.new(seq_num),
         auth_account <- Enum.at(auth_accounts, 0) do
      invoke_host_function_op = create_host_function_op(contract_id, function_name, function_args)

      invoke_host_function_op
      |> simulate(source_account, sequence_number)
      |> send_transaction(
        source_account,
        sequence_number,
        signature,
        auth_account,
        invoke_host_function_op
      )
    end
  end

  @spec simulate(
          invoke_host_function_op :: invoke_host_function(),
          source_account :: account(),
          sequence_number :: sequence_number()
        ) :: simulate_response()
  defp simulate(
         invoke_host_function_op,
         source_account,
         sequence_number
       ) do
    {:ok, envelop_xdr} =
      source_account
      |> TxBuild.new(sequence_number: sequence_number)
      |> TxBuild.add_operation(invoke_host_function_op)
      |> TxBuild.envelope()

    RPC.simulate_transaction(envelop_xdr)
  end

  @spec send_transaction(
          simulate_response :: simulate_response(),
          source_account :: account(),
          sequence_number :: sequence_number(),
          signature :: signature(),
          auth_account :: auth_account(),
          invoke_host_function_op :: invoke_host_function()
        ) :: send_response() | simulate_response()
  defp send_transaction(
         {:ok, %SimulateTransactionResponse{results: [%{footprint: footprint, auth: auth}]}},
         source_account,
         sequence_number,
         signature,
         auth_account,
         invoke_host_function_op
       ) do
    invoke_host_function_op =
      set_invoke_host_function_params(invoke_host_function_op, footprint, auth, auth_account)

    {:ok, envelope_xdr} =
      source_account
      |> TxBuild.new(sequence_number: sequence_number)
      |> TxBuild.add_operation(invoke_host_function_op)
      |> TxBuild.sign(signature)
      |> TxBuild.envelope()

    RPC.send_transaction(envelope_xdr)
  end

  defp send_transaction(
         {:ok, %SimulateTransactionResponse{}} = response,
         _source_account,
         _sequence_number,
         _signature,
         _auth_account,
         _invoke_host_function_op
       ),
       do: response

  @spec create_host_function_op(
          contract_id :: contract_id(),
          function_name :: function_name(),
          function_args :: function_args()
        ) :: invoke_host_function()
  defp create_host_function_op(contract_id, function_name, function_args) do
    function =
      HostFunction.new(
        type: :invoke,
        contract_id: contract_id,
        function_name: function_name,
        args: function_args
      )

    InvokeHostFunction.new(function: function)
  end

  @spec set_invoke_host_function_params(
          invoke_host_function :: invoke_host_function(),
          footprint :: String.t(),
          auth :: auth(),
          auth_account :: auth_account()
        ) :: invoke_host_function() | {:error, :required_auth}
  defp set_invoke_host_function_params(invoke_host_function_op, footprint, [auth], nil) do
    invoke_host_function_op
    |> InvokeHostFunction.set_footprint(footprint)
    |> InvokeHostFunction.set_contract_auth(auth)
  end

  defp set_invoke_host_function_params(invoke_host_function_op, footprint, [auth], auth_account) do
    authorization = ContractAuth.sign_xdr(auth, auth_account)

    invoke_host_function_op
    |> InvokeHostFunction.set_footprint(footprint)
    |> InvokeHostFunction.set_contract_auth(authorization)
  end

  defp set_invoke_host_function_params(invoke_host_function_op, footprint, nil, _auth_account),
    do: InvokeHostFunction.set_footprint(invoke_host_function_op, footprint)

  @spec convert_to_sc_val(function_args :: function_args()) :: {:ok, sc_val_list()}
  defp convert_to_sc_val(function_args) do
    sc_vals = Enum.map(function_args, fn %{__struct__: struct} = arg -> struct.to_sc_val(arg) end)
    {:ok, sc_vals}
  end
end