lib/tx_build/sc_val.ex

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

  @behaviour Stellar.TxBuild.XDR

  alias StellarBase.XDR.SCContractInstance
  alias Stellar.TxBuild.{SCAddress, SCError, SCMapEntry}

  alias StellarBase.XDR.{
    Bool,
    Duration,
    Hash,
    Int128Parts,
    Int256Parts,
    Int32,
    Int64,
    SCBytes,
    ContractExecutable,
    ContractExecutableType,
    SCMap,
    SCNonceKey,
    SCString,
    SCSymbol,
    SCVal,
    SCValType,
    SCVec,
    OptionalSCMap,
    OptionalSCVec,
    TimePoint,
    UInt128Parts,
    UInt256Parts,
    UInt32,
    UInt64,
    Void
  }

  @type type ::
          :bool
          | :void
          | :error
          | :u32
          | :i32
          | :u64
          | :i64
          | :time_point
          | :duration
          | :u128
          | :i128
          | :u256
          | :i256
          | :bytes
          | :string
          | :symbol
          | :vec
          | :map
          | :address
          | :ledger_key_contract_instance
          | :ledger_key_nonce
          | :contract_instance

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

  @type map_entry :: StellarBase.XDR.SCMapEntry.t()

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

  defstruct [:type, :value]

  @allowed_types ~w(bool void error u32 i32 u64 i64 time_point duration u128 i128 u256 i256 bytes string symbol vec map address ledger_key_contract_instance ledger_key_nonce contract_instance)a

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

  def new([{type, value}], _opts)
      when type in @allowed_types 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: :error, value: value}) do
    val_type = SCValType.new(:SCV_ERROR)

    value
    |> SCError.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)
    hi = UInt64.new(hi)
    lo = UInt64.new(lo)

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

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

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

  def to_xdr(%__MODULE__{
        type: :u256,
        value: %{hi_hi: hi_hi, hi_lo: hi_lo, lo_hi: lo_hi, lo_lo: lo_lo}
      }) do
    type = SCValType.new(:SCV_U256)
    hi_hi = UInt64.new(hi_hi)
    hi_lo = UInt64.new(hi_lo)
    lo_hi = UInt64.new(lo_hi)
    lo_lo = UInt64.new(lo_lo)

    hi_hi
    |> UInt256Parts.new(hi_lo, lo_hi, lo_lo)
    |> SCVal.new(type)
  end

  def to_xdr(%__MODULE__{
        type: :i256,
        value: %{hi_hi: hi_hi, hi_lo: hi_lo, lo_hi: lo_hi, lo_lo: lo_lo}
      }) do
    type = SCValType.new(:SCV_I256)
    hi_hi = Int64.new(hi_hi)
    hi_lo = UInt64.new(hi_lo)
    lo_hi = UInt64.new(lo_hi)
    lo_lo = UInt64.new(lo_lo)

    hi_hi
    |> Int256Parts.new(hi_lo, lo_hi, lo_lo)
    |> 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: :ledger_key_contract_instance, value: nil}) do
    type = SCValType.new(:SCV_LEDGER_KEY_CONTRACT_INSTANCE)

    SCVal.new(Void.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: :contract_instance, value: {:wasm_ref, hash}}) do
    type = SCValType.new(:SCV_CONTRACT_INSTANCE)
    contract_code = ContractExecutableType.new(:CONTRACT_EXECUTABLE_WASM)

    hash
    |> Hash.new()
    |> ContractExecutable.new(contract_code)
    |> SCContractInstance.new(OptionalSCMap.new())
    |> SCVal.new(type)
  end

  def to_xdr(%__MODULE__{type: :contract_instance, value: :token}) do
    type = SCValType.new(:SCV_CONTRACT_INSTANCE)
    contract_code = ContractExecutableType.new(:CONTRACT_EXECUTABLE_STELLAR_ASSET)

    Void.new()
    |> ContractExecutable.new(contract_code)
    |> SCContractInstance.new(OptionalSCMap.new())
    |> SCVal.new(type)
  end

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

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

  @spec to_native_from_xdr(xdr :: String.t()) :: any
  def to_native_from_xdr(xdr) when is_binary(xdr) do
    with {:ok, {scval, _rest}} <- validate_xdr_decoding(xdr) do
      to_native(scval)
    end
  end

  def to_native_from_xdr(_xdr), do: {:error, :invalid_xdr}

  @spec to_native(SCVal.t()) :: any
  def to_native(%SCVal{
        value: %StellarBase.XDR.SCSymbol{value: value},
        type: %StellarBase.XDR.SCValType{identifier: :SCV_SYMBOL}
      }) do
    value
  end

  def to_native(%SCVal{
        type: %StellarBase.XDR.SCValType{identifier: :SCV_VEC},
        value: %StellarBase.XDR.OptionalSCVec{
          sc_vec: %StellarBase.XDR.SCVec{
            items: items
          }
        }
      }) do
    Enum.map(items, &to_native/1)
  end

  def to_native(%SCVal{
        type: %StellarBase.XDR.SCValType{identifier: :SCV_VEC},
        value: %StellarBase.XDR.OptionalSCVec{
          sc_vec: nil
        }
      }) do
    []
  end

  def to_native(%SCVal{
        type: %StellarBase.XDR.SCValType{identifier: :SCV_VOID},
        value: _value
      }) do
    nil
  end

  def to_native(%SCVal{
        type: %StellarBase.XDR.SCValType{identifier: :SCV_BOOL},
        value: %StellarBase.XDR.Bool{value: value}
      }) do
    value
  end

  def to_native(%SCVal{
        type: %StellarBase.XDR.SCValType{identifier: :SCV_U128},
        value: %StellarBase.XDR.UInt128Parts{
          hi: %StellarBase.XDR.UInt64{datum: hi_value},
          lo: %StellarBase.XDR.UInt64{datum: lo_value}
        }
      }) do
    (hi_value <<< 64) + lo_value
  end

  def to_native(%SCVal{
        type: %StellarBase.XDR.SCValType{identifier: :SCV_U64},
        value: %StellarBase.XDR.UInt64{datum: value}
      }) do
    value
  end

  def to_native(%SCVal{
        type: %StellarBase.XDR.SCValType{identifier: :SCV_I64},
        value: %StellarBase.XDR.Int64{datum: value}
      }) do
    value
  end

  def to_native(%SCVal{
        type: %StellarBase.XDR.SCValType{identifier: :SCV_U32},
        value: %StellarBase.XDR.UInt32{datum: value}
      }) do
    value
  end

  def to_native(%SCVal{
        type: %StellarBase.XDR.SCValType{identifier: :SCV_I32},
        value: %StellarBase.XDR.Int32{datum: value}
      }) do
    value
  end

  def to_native(%SCVal{
        type: %StellarBase.XDR.SCValType{identifier: :SCV_I128},
        value: %StellarBase.XDR.Int128Parts{
          hi: %StellarBase.XDR.Int64{datum: hi_value},
          lo: %StellarBase.XDR.UInt64{datum: lo_value}
        }
      }) do
    (hi_value <<< 64) + lo_value
  end

  def to_native(%SCVal{
        type: %StellarBase.XDR.SCValType{identifier: :SCV_U256},
        value: %StellarBase.XDR.UInt256Parts{
          hi_hi: %StellarBase.XDR.UInt64{datum: hi_hi_value},
          hi_lo: %StellarBase.XDR.UInt64{datum: hi_lo_value},
          lo_hi: %StellarBase.XDR.UInt64{datum: lo_hi_value},
          lo_lo: %StellarBase.XDR.UInt64{datum: lo_lo_value}
        }
      }) do
    (hi_hi_value <<< 192) + (hi_lo_value <<< 128) + (lo_hi_value <<< 64) + lo_lo_value
  end

  def to_native(%SCVal{
        type: %StellarBase.XDR.SCValType{identifier: :SCV_I256},
        value: %StellarBase.XDR.Int256Parts{
          hi_hi: %StellarBase.XDR.Int64{datum: hi_hi_value},
          hi_lo: %StellarBase.XDR.UInt64{datum: hi_lo_value},
          lo_hi: %StellarBase.XDR.UInt64{datum: lo_hi_value},
          lo_lo: %StellarBase.XDR.UInt64{datum: lo_lo_value}
        }
      }) do
    (hi_hi_value <<< 192) + (hi_lo_value <<< 128) + (lo_hi_value <<< 64) + lo_lo_value
  end

  def to_native(%SCVal{
        type: %StellarBase.XDR.SCValType{identifier: :SCV_BYTES},
        value: %StellarBase.XDR.SCBytes{value: value}
      }) do
    value
  end

  def to_native(%SCVal{
        type: %StellarBase.XDR.SCValType{identifier: :SCV_STRING},
        value: %StellarBase.XDR.SCString{value: value}
      }) do
    value
  end

  def to_native(%SCVal{
        type: %StellarBase.XDR.SCValType{identifier: :SCV_MAP},
        value: %StellarBase.XDR.OptionalSCMap{
          sc_map: %StellarBase.XDR.SCMap{
            items: items
          }
        }
      }) do
    Enum.reduce(items, %{}, fn entry, acc ->
      {key, val} = map_entry_to_native(entry)
      Map.put(acc, key, val)
    end)
  end

  def to_native(%SCVal{
        type: %StellarBase.XDR.SCValType{identifier: :SCV_MAP},
        value: %StellarBase.XDR.OptionalSCMap{
          sc_map: nil
        }
      }) do
    %{}
  end

  def to_native(%SCVal{
        type: %StellarBase.XDR.SCValType{identifier: :SCV_ADDRESS},
        value: %StellarBase.XDR.SCAddress{
          sc_address: %StellarBase.XDR.AccountID{
            account_id: %StellarBase.XDR.PublicKey{
              public_key: %StellarBase.XDR.UInt256{
                datum: public_key
              }
            }
          },
          type: %StellarBase.XDR.SCAddressType{identifier: :SC_ADDRESS_TYPE_ACCOUNT}
        }
      }) do
    StellarBase.StrKey.encode!(public_key, :ed25519_public_key)
  end

  def to_native(%SCVal{
        type: %StellarBase.XDR.SCValType{identifier: :SCV_ADDRESS},
        value: %StellarBase.XDR.SCAddress{
          sc_address: %StellarBase.XDR.Hash{value: hash},
          type: %StellarBase.XDR.SCAddressType{identifier: :SC_ADDRESS_TYPE_CONTRACT}
        }
      }) do
    StellarBase.StrKey.encode!(hash, :contract)
  end

  def to_native(_sc_val), do: {:error, :invalid_or_not_supported_sc_val}

  @spec validate_xdr_decoding(xdr :: String.t()) :: validation()
  defp validate_xdr_decoding(xdr) when is_binary(xdr) do
    case Base.decode64(xdr) do
      {:ok, decoded_xdr} -> SCVal.decode_xdr(decoded_xdr)
      _ -> {:error, :invalid_base64}
    end
  end

  @spec map_entry_to_native(map_entry :: map_entry()) :: tuple()
  defp map_entry_to_native(%StellarBase.XDR.SCMapEntry{key: key, val: val}),
    do: {to_native(key), to_native(val)}

  @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({:error, %SCError{} = 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, %{hi_hi: hi_hi, hi_lo: hi_lo, lo_hi: lo_hi, lo_lo: lo_lo} = value})
       when type in ~w(u256 i256)a and is_integer(hi_hi) and is_integer(hi_lo) and
              is_integer(lo_hi) and is_integer(lo_lo),
       do: {:ok, value}

  defp validate_sc_val({type, value})
       when type in ~w(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_instance, :token}), do: {:ok, :token}

  defp validate_sc_val({:contract_instance, {: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_instance, _value}), do: {:ok, nil}
  defp validate_sc_val({:ledger_key_nonce, value}) when is_integer(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