lib/tx_build/host_function.ex

defmodule Stellar.TxBuild.HostFunction do
  @moduledoc """
    `HostFunction` struct definition.
  """

  import Stellar.TxBuild.Validations,
    only: [
      validate_sc_vals: 1,
      validate_contract_id: 1,
      validate_string: 1
    ]

  alias Stellar.TxBuild.{Asset, SCVal, SCContractExecutable}
  alias StellarBase.XDR.HostFunction, as: HostFunctionXDR

  alias StellarBase.XDR.{
    ContractID,
    ContractIDType,
    CreateContractArgs,
    InstallContractCodeArgs,
    SCVec,
    HostFunctionType,
    UInt256,
    VariableOpaque256000
  }

  @behaviour Stellar.TxBuild.XDR

  @type type :: :invoke | :install | :create
  @type contract_id :: String.t()
  @type function_name :: String.t()
  @type invoke_args :: list(SCVal.t())
  @type args :: invoke_args()
  @type wasm_id :: binary()
  @type asset :: Asset.t()
  @type error :: {:error, atom()}
  @type validation :: {:ok, any()} | error()

  @type t :: %__MODULE__{
          type: type(),
          contract_id: contract_id(),
          function_name: function_name(),
          args: args(),
          code: binary(),
          wasm_id: wasm_id(),
          salt: binary(),
          asset: asset()
        }

  defstruct [:type, :contract_id, :function_name, :args, :code, :asset, :wasm_id, :salt]

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

  def new(
        [
          {:type, :invoke},
          {:contract_id, contract_id},
          {:function_name, function_name},
          {:args, args}
        ],
        _opts
      )
      when is_list(args) do
    with {:ok, contract_id} <- validate_contract_id({:contract_id, contract_id}),
         {:ok, function_name} <- validate_string({:function_name, function_name}),
         {:ok, args} <- validate_sc_vals({:args, args}) do
      %__MODULE__{
        type: :invoke,
        contract_id: contract_id,
        function_name: function_name,
        args: args
      }
    end
  end

  def new(
        [
          {:type, :install},
          {:code, code}
        ],
        _opts
      )
      when is_binary(code) do
    %__MODULE__{
      type: :install,
      code: code
    }
  end

  def new(
        [
          {:type, :create},
          {:wasm_id, wasm_id}
        ],
        _opts
      ) do
    new(type: :create, wasm_id: wasm_id, salt: :crypto.strong_rand_bytes(32))
  end

  def new(
        [
          {:type, :create},
          {:wasm_id, wasm_id},
          {:salt, salt}
        ],
        _opts
      ) do
    with {:ok, wasm_id} <- validate_wasm_id(wasm_id),
         {:ok, salt} <- validate_salt(salt) do
      %__MODULE__{
        type: :create,
        wasm_id: wasm_id,
        salt: salt
      }
    end
  end

  def new(
        [
          {:type, :create},
          {:asset, asset}
        ],
        _opts
      ) do
    with {:ok, _asset} <- validate_asset(asset) do
      %__MODULE__{
        type: :create,
        asset: asset
      }
    end
  end

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

  @impl true
  def to_xdr(%__MODULE__{
        type: :invoke,
        contract_id: contract_id,
        function_name: function_name,
        args: args
      }) do
    contract_id_scval =
      contract_id
      |> Base.decode16!(case: :lower)
      |> (&SCVal.new(bytes: &1)).()
      |> SCVal.to_xdr()

    sc_symbol_scval =
      function_name
      |> (&SCVal.new(symbol: &1)).()
      |> SCVal.to_xdr()

    args_scvalxdr = Enum.map(args, &SCVal.to_xdr/1)

    sc_vec =
      [contract_id_scval, sc_symbol_scval]
      |> Kernel.++(args_scvalxdr)
      |> SCVec.new()

    host_function_type = HostFunctionType.new()
    HostFunctionXDR.new(sc_vec, host_function_type)
  end

  def to_xdr(%__MODULE__{
        type: :install,
        code: code
      }) do
    host_function_type = HostFunctionType.new(:HOST_FUNCTION_TYPE_INSTALL_CONTRACT_CODE)

    code
    |> VariableOpaque256000.new()
    |> InstallContractCodeArgs.new()
    |> HostFunctionXDR.new(host_function_type)
  end

  def to_xdr(%__MODULE__{
        type: :create,
        wasm_id: wasm_id,
        salt: salt,
        asset: nil
      }) do
    host_function_type = HostFunctionType.new(:HOST_FUNCTION_TYPE_CREATE_CONTRACT)

    contract_id_type = ContractIDType.new(:CONTRACT_ID_FROM_SOURCE_ACCOUNT)
    contract_id = salt |> UInt256.new() |> ContractID.new(contract_id_type)

    sc_contract_executable =
      [wasm_ref: wasm_id] |> SCContractExecutable.new() |> SCContractExecutable.to_xdr()

    contract_id
    |> CreateContractArgs.new(sc_contract_executable)
    |> HostFunctionXDR.new(host_function_type)
  end

  def to_xdr(%__MODULE__{
        type: :create,
        asset: asset
      }) do
    host_function_type = HostFunctionType.new(:HOST_FUNCTION_TYPE_CREATE_CONTRACT)

    contract_id_type = ContractIDType.new(:CONTRACT_ID_FROM_ASSET)
    contract_id = asset |> Asset.to_xdr() |> ContractID.new(contract_id_type)

    sc_contract_executable = :token |> SCContractExecutable.new() |> SCContractExecutable.to_xdr()

    contract_id
    |> CreateContractArgs.new(sc_contract_executable)
    |> HostFunctionXDR.new(host_function_type)
  end

  @spec validate_asset(asset :: asset()) :: validation()
  defp validate_asset(%Asset{} = asset), do: {:ok, asset}
  defp validate_asset(_asset), do: {:error, :invalid_asset}

  @spec validate_salt(salt :: binary()) :: validation()
  defp validate_salt(salt) when is_binary(salt) and byte_size(salt) == 32, do: {:ok, salt}
  defp validate_salt(_salt), do: {:error, :invalid_salt}

  @spec validate_wasm_id(wasm_id :: binary()) :: validation()
  defp validate_wasm_id(wasm_id) when is_binary(wasm_id), do: {:ok, wasm_id}
  defp validate_wasm_id(_wasm_id), do: {:error, :invalid_wasm_id}
end