lib/tx_build/sc_val.ex

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

  @behaviour Stellar.TxBuild.XDR

  alias Stellar.TxBuild.{SCAddress, SCMapEntry, SCStatus}

  alias StellarBase.XDR.{
    Bool,
    Duration,
    Hash,
    Int128Parts,
    Int32,
    Int64,
    SCBytes,
    SCContractExecutable,
    SCContractExecutableType,
    SCMap,
    SCNonceKey,
    SCString,
    SCSymbol,
    SCVal,
    SCValType,
    SCVec,
    OptionalSCMap,
    OptionalSCVec,
    TimePoint,
    UInt32,
    UInt64,
    UInt256,
    Void
  }

  @type type ::
          :bool
          | :void
          | :status
          | :u32
          | :i32
          | :u64
          | :i64
          | :time_point
          | :duration
          | :u128
          | :i128
          | :u256
          | :i256
          | :bytes
          | :string
          | :symbol
          | :vec
          | :map
          | :contract
          | :address
          | :ledger_key_contract
          | :ledger_key_nonce

  @type validation :: {:ok, any()} | {:error, atom()}
  @type value ::
          nil
          | integer()
          | boolean()
          | SCStatus.t()
          | map()
          | binary()
          | list()
          | atom()
          | SCAddress.t()

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

  defstruct [:type, :value]

  @impl true
  def new(args, opts \\ nil)

  def new([{type, value}], _opts)
      when type in ~w(bool void status u32 i32 u64 i64 time_point duration u128 i128 u256 i256 bytes string symbol vec map contract address ledger_key_contract ledger_key_nonce)a do
    with {:ok, _value} <- validate_sc_val({type, value}) do
      %__MODULE__{
        type: type,
        value: value
      }
    end
  end

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

  @impl true
  def to_xdr(%__MODULE__{type: :bool, value: value}) do
    type = SCValType.new(:SCV_BOOL)

    value
    |> Bool.new()
    |> SCVal.new(type)
  end

  def to_xdr(%__MODULE__{type: :void, value: _value}) do
    type = SCValType.new(:SCV_VOID)

    Void.new()
    |> SCVal.new(type)
  end

  def to_xdr(%__MODULE__{type: :status, value: value}) do
    val_type = SCValType.new(:SCV_STATUS)

    value
    |> SCStatus.to_xdr()
    |> SCVal.new(val_type)
  end

  def to_xdr(%__MODULE__{type: :u32, value: value}) do
    type = SCValType.new(:SCV_U32)

    value
    |> UInt32.new()
    |> SCVal.new(type)
  end

  def to_xdr(%__MODULE__{type: :i32, value: value}) do
    type = SCValType.new(:SCV_I32)

    value
    |> Int32.new()
    |> SCVal.new(type)
  end

  def to_xdr(%__MODULE__{type: :u64, value: value}) do
    type = SCValType.new(:SCV_U64)

    value
    |> UInt64.new()
    |> SCVal.new(type)
  end

  def to_xdr(%__MODULE__{type: :i64, value: value}) do
    type = SCValType.new(:SCV_I64)

    value
    |> Int64.new()
    |> SCVal.new(type)
  end

  def to_xdr(%__MODULE__{type: :time_point, value: value}) do
    type = SCValType.new(:SCV_TIMEPOINT)

    value
    |> TimePoint.new()
    |> SCVal.new(type)
  end

  def to_xdr(%__MODULE__{type: :duration, value: value}) do
    type = SCValType.new(:SCV_DURATION)

    value
    |> Duration.new()
    |> SCVal.new(type)
  end

  def to_xdr(%__MODULE__{type: :u128, value: %{lo: lo, hi: hi}}) do
    type = SCValType.new(:SCV_U128)
    lo = UInt64.new(lo)
    hi = UInt64.new(hi)

    lo
    |> Int128Parts.new(hi)
    |> SCVal.new(type)
  end

  def to_xdr(%__MODULE__{type: :i128, value: %{lo: lo, hi: hi}}) do
    type = SCValType.new(:SCV_I128)
    lo = UInt64.new(lo)
    hi = UInt64.new(hi)

    lo
    |> Int128Parts.new(hi)
    |> SCVal.new(type)
  end

  def to_xdr(%__MODULE__{type: :u256, value: value}) do
    type = SCValType.new(:SCV_U256)

    value
    |> UInt256.new()
    |> SCVal.new(type)
  end

  def to_xdr(%__MODULE__{type: :i256, value: value}) do
    type = SCValType.new(:SCV_I256)

    value
    |> UInt256.new()
    |> SCVal.new(type)
  end

  def to_xdr(%__MODULE__{type: :bytes, value: value}) do
    type = SCValType.new(:SCV_BYTES)

    value
    |> SCBytes.new()
    |> SCVal.new(type)
  end

  def to_xdr(%__MODULE__{type: :string, value: value}) do
    type = SCValType.new(:SCV_STRING)

    value
    |> SCString.new()
    |> SCVal.new(type)
  end

  def to_xdr(%__MODULE__{type: :symbol, value: value}) do
    type = SCValType.new(:SCV_SYMBOL)

    value
    |> SCSymbol.new()
    |> SCVal.new(type)
  end

  def to_xdr(%__MODULE__{type: :vec, value: nil}) do
    type = SCValType.new(:SCV_VEC)

    nil
    |> OptionalSCVec.new()
    |> SCVal.new(type)
  end

  def to_xdr(%__MODULE__{type: :vec, value: value}) do
    type = SCValType.new(:SCV_VEC)

    value
    |> Enum.map(&to_xdr/1)
    |> SCVec.new()
    |> OptionalSCVec.new()
    |> SCVal.new(type)
  end

  def to_xdr(%__MODULE__{type: :map, value: nil}) do
    type = SCValType.new(:SCV_MAP)

    nil
    |> OptionalSCMap.new()
    |> SCVal.new(type)
  end

  def to_xdr(%__MODULE__{type: :map, value: value}) do
    type = SCValType.new(:SCV_MAP)

    value
    |> Enum.map(&SCMapEntry.to_xdr/1)
    |> SCMap.new()
    |> OptionalSCMap.new()
    |> SCVal.new(type)
  end

  def to_xdr(%__MODULE__{type: :contract, value: {:wasm_ref, hash}}) do
    type = SCValType.new(:SCV_CONTRACT_EXECUTABLE)
    contract_code = SCContractExecutableType.new(:SCCONTRACT_EXECUTABLE_WASM_REF)

    hash
    |> Hash.new()
    |> SCContractExecutable.new(contract_code)
    |> SCVal.new(type)
  end

  def to_xdr(%__MODULE__{type: :contract, value: :token}) do
    type = SCValType.new(:SCV_CONTRACT_EXECUTABLE)
    contract_code = SCContractExecutableType.new(:SCCONTRACT_EXECUTABLE_TOKEN)

    Void.new()
    |> SCContractExecutable.new(contract_code)
    |> SCVal.new(type)
  end

  def to_xdr(%__MODULE__{type: :address, value: value}) do
    type = SCValType.new(:SCV_ADDRESS)

    value
    |> SCAddress.to_xdr()
    |> SCVal.new(type)
  end

  def to_xdr(%__MODULE__{type: :ledger_key_contract, value: _value}) do
    type = SCValType.new(:SCV_LEDGER_KEY_CONTRACT_EXECUTABLE)

    Void.new()
    |> SCVal.new(type)
  end

  def to_xdr(%__MODULE__{type: :ledger_key_nonce, value: value}) do
    type = SCValType.new(:SCV_LEDGER_KEY_NONCE)

    value
    |> SCAddress.to_xdr()
    |> SCNonceKey.new()
    |> SCVal.new(type)
  end

  @spec validate_sc_val(tuple :: tuple()) :: validation()
  defp validate_sc_val({type, value})
       when type in ~w(u32 u64 time_point duration)a and is_integer(value) and value >= 0,
       do: {:ok, value}

  defp validate_sc_val({type, value})
       when type in ~w(i32 i64)a and is_integer(value),
       do: {:ok, value}

  defp validate_sc_val({:bool, value}) when is_boolean(value), do: {:ok, value}
  defp validate_sc_val({:void, _value}), do: {:ok, nil}
  defp validate_sc_val({:status, %SCStatus{} = value}), do: {:ok, value}

  defp validate_sc_val({type, %{lo: lo, hi: hi} = value})
       when type in ~w(u128 i128)a and is_integer(lo) and is_integer(hi),
       do: {:ok, value}

  defp validate_sc_val({type, value})
       when type in ~w(u256 i256 bytes string symbol)a and is_binary(value),
       do: {:ok, value}

  defp validate_sc_val({:vec, nil}), do: {:ok, nil}

  defp validate_sc_val({:vec, value}) when is_list(value) do
    if Enum.all?(value, &is_sc_val?/1),
      do: {:ok, value},
      else: {:error, :invalid_vec}
  end

  defp validate_sc_val({:map, nil}), do: {:ok, nil}

  defp validate_sc_val({:map, value}) when is_list(value) do
    if Enum.all?(value, &is_map_entry?/1),
      do: {:ok, value},
      else: {:error, :invalid_map}
  end

  defp validate_sc_val({:contract, :token}), do: {:ok, :token}

  defp validate_sc_val({:contract, {:wasm_ref, hash} = value}) when is_binary(hash),
    do: {:ok, value}

  defp validate_sc_val({:address, %SCAddress{} = value}), do: {:ok, value}
  defp validate_sc_val({:ledger_key_contract, _value}), do: {:ok, nil}
  defp validate_sc_val({:ledger_key_nonce, %SCAddress{} = value}), do: {:ok, value}
  defp validate_sc_val({type, _value}), do: {:error, :"invalid_#{type}"}

  @spec is_map_entry?(value :: any()) :: boolean()
  defp is_map_entry?(%SCMapEntry{}), do: true
  defp is_map_entry?(_), do: false

  @spec is_sc_val?(value :: any()) :: boolean()
  defp is_sc_val?(%__MODULE__{}), do: true
  defp is_sc_val?(_), do: false
end