lib/chainweb/pact/command_payload.ex

defmodule Kadena.Chainweb.Pact.CommandPayload do
  @moduledoc """
  `CommandPayload` struct definition.
  """

  alias Kadena.Utils.MapCase

  alias Kadena.Types.{
    Base16String,
    Cap,
    ChainID,
    ContPayload,
    EnvData,
    ExecPayload,
    MetaData,
    NetworkID,
    PactCode,
    PactDecimal,
    PactInt,
    PactPayload,
    PactTransactionHash,
    PactValue,
    Proof,
    Rollback,
    Signer,
    Step
  }

  @behaviour Kadena.Chainweb.Type

  @type network_id :: NetworkID.t() | nil
  @type payload :: PactPayload.t()
  @type signers :: list(Signer.t())
  @type meta :: MetaData.t()
  @type nonce :: String.t()
  @type value :: network_id() | payload() | signers() | meta() | nonce()
  @type validation :: {:ok, value()} | {:error, Keyword.t()}
  @type valid_map :: {:ok, map()}
  @type valid_string :: {:ok, String.t()}
  @type valid_list :: {:ok, list()}
  @type map_return :: map() | nil
  @type string_value :: String.t() | nil
  @type pact_values :: list(PactValue.t())
  @type scheme :: :ed25519 | nil
  @type scheme_return :: :ED25519 | nil
  @type cap :: Cap.t()
  @type data :: EnvData.t() | nil
  @type proof :: Proof.t() | nil
  @type signer :: Signer.t()
  @type raw_value :: integer() | string_value() | boolean() | Decimal.t()
  @type clist :: list(cap()) | nil
  @type addr :: Base16String.t() | nil
  @type pact_payload :: PactPayload.t()
  @type literal ::
          integer()
          | boolean()
          | String.t()
          | PactInt.t()
          | PactDecimal.t()
          | list(PactValue.t())

  @type t :: %__MODULE__{
          network_id: network_id(),
          payload: payload(),
          signers: signers(),
          meta: meta(),
          nonce: nonce()
        }

  defstruct [:network_id, :payload, :signers, :meta, :nonce]

  @impl true
  def new(args) do
    network_id = Keyword.get(args, :network_id)
    payload = Keyword.get(args, :payload)
    signers = Keyword.get(args, :signers, [])
    meta = Keyword.get(args, :meta)
    nonce = Keyword.get(args, :nonce)

    with {:ok, network_id} <- validate_network_id(network_id),
         {:ok, payload} <- validate_payload(payload),
         {:ok, signers} <- validate_signers(signers),
         {:ok, meta} <- validate_meta(meta),
         {:ok, nonce} <- validate_nonce(nonce) do
      %__MODULE__{
        network_id: network_id,
        payload: payload,
        signers: signers,
        meta: meta,
        nonce: nonce
      }
    end
  end

  @impl true
  def to_json!(%__MODULE__{
        network_id: network_id,
        payload: payload,
        signers: signers,
        meta: meta,
        nonce: nonce
      }) do
    with {:ok, payload} <- extract_payload(payload),
         {:ok, meta} <- extract_meta(meta),
         {:ok, network_id} <- extract_network_id(network_id),
         {:ok, signers} <- extract_signers_list(signers) do
      Jason.encode!(%{
        payload: payload,
        meta: meta,
        networkId: network_id,
        nonce: nonce,
        signers: signers
      })
    end
  end

  @spec validate_network_id(network_id :: network_id()) :: validation()
  defp validate_network_id(%NetworkID{} = network_id), do: {:ok, network_id}

  defp validate_network_id(network_id) do
    case NetworkID.new(network_id) do
      %NetworkID{} = network_id -> {:ok, network_id}
      _error -> {:error, [network_id: :invalid]}
    end
  end

  @spec validate_payload(payload :: payload()) :: validation()
  defp validate_payload(%PactPayload{} = payload), do: {:ok, payload}

  defp validate_payload(payload) do
    case PactPayload.new(payload) do
      %PactPayload{} = payload -> {:ok, payload}
      _error -> {:error, [payload: :invalid]}
    end
  end

  @spec validate_signers(signers :: signers()) :: validation()
  defp validate_signers([%Signer{} | _rest] = signers), do: {:ok, signers}
  defp validate_signers([] = signers), do: {:ok, signers}
  defp validate_signers(_signers), do: {:error, [signers: :invalid]}

  @spec validate_meta(meta :: meta()) :: validation()
  defp validate_meta(%MetaData{} = meta), do: {:ok, meta}
  defp validate_meta(nil), do: {:ok, MetaData.new([])}

  defp validate_meta(meta) do
    case MetaData.new(meta) do
      %MetaData{} = meta -> {:ok, meta}
      {:error, _reason} -> {:error, [meta: :invalid]}
    end
  end

  @spec validate_nonce(nonce :: nonce()) :: validation()
  defp validate_nonce(nonce), do: {:ok, to_string(nonce)}

  @spec extract_network_id(network_id()) :: valid_string()
  defp extract_network_id(%NetworkID{id: id}), do: {:ok, id}

  @spec extract_payload(pact_payload()) :: valid_map()
  defp extract_payload(%PactPayload{payload: %ExecPayload{} = exec_payload}) do
    %ExecPayload{code: %PactCode{code: code}, data: data} = exec_payload
    payload = %{exec: %{code: code, data: extract_data(data)}}
    {:ok, payload}
  end

  defp extract_payload(%PactPayload{
         payload: %ContPayload{
           data: data,
           pact_id: %PactTransactionHash{hash: hash},
           proof: proof,
           rollback: %Rollback{value: rollback},
           step: %Step{number: number}
         }
       }) do
    payload = %{
      cont: %{
        data: extract_data(data),
        pactId: hash,
        proof: extract_proof(proof),
        rollback: rollback,
        step: number
      }
    }

    {:ok, payload}
  end

  @spec extract_proof(proof()) :: string_value()
  defp extract_proof(nil), do: nil
  defp extract_proof(%Proof{value: proof}), do: proof

  @spec extract_data(data()) :: map_return()
  defp extract_data(nil), do: nil
  defp extract_data(%EnvData{data: data}), do: data

  @spec extract_meta(meta()) :: valid_map()
  defp extract_meta(%MetaData{
         creation_time: creation_time,
         ttl: ttl,
         gas_limit: gas_limit,
         gas_price: gas_price,
         sender: sender,
         chain_id: %ChainID{id: id}
       }) do
    %{
      chain_id: id,
      creation_time: creation_time,
      gas_limit: gas_limit,
      gas_price: gas_price,
      sender: sender,
      ttl: ttl
    }
    |> MapCase.to_camel!()
    |> (&{:ok, &1}).()
  end

  @spec extract_signers_list(signer_list :: signers()) :: valid_list()
  defp extract_signers_list(signer_list) do
    signers = Enum.map(signer_list, fn sig -> extract_signer_info(sig) end)
    {:ok, signers}
  end

  @spec extract_signer_info(signer()) :: map_return()
  defp extract_signer_info(%Signer{
         addr: addr,
         scheme: scheme,
         pub_key: %Base16String{value: pub_key},
         clist: clist
       }) do
    MapCase.to_camel!(%{
      addr: extract_addr(addr),
      scheme: extract_scheme(scheme),
      pub_key: pub_key,
      clist: extract_clist(clist)
    })
  end

  @spec extract_addr(addr()) :: string_value()
  defp extract_addr(nil), do: nil
  defp extract_addr(%Base16String{value: value}), do: value

  @spec extract_scheme(scheme()) :: scheme_return()
  defp extract_scheme(nil), do: nil
  defp extract_scheme(:ed25519), do: :ED25519

  @spec extract_clist(clist()) :: list()
  defp extract_clist(nil), do: []
  defp extract_clist(caps), do: Enum.map(caps, &extract_cap_info/1)

  @spec extract_cap_info(cap()) :: map_return()
  defp extract_cap_info(%Cap{name: name, args: args}),
    do: %{name: name, args: extract_values(args)}

  @spec extract_values(pact_values()) :: list()
  defp extract_values(pact_values),
    do: Enum.map(pact_values, fn %PactValue{literal: pact_value} -> extract_value(pact_value) end)

  @spec extract_value(literal()) :: raw_value()
  defp extract_value(value) when is_list(value), do: extract_values(value)
  defp extract_value(%PactInt{raw_value: value}), do: value
  defp extract_value(%PactDecimal{raw_value: value}), do: value
  defp extract_value(value), do: value
end