lib/pact/command/exec_command.ex

defmodule Kadena.Pact.ExecCommand do
  @moduledoc """
    Specifies functions to build PACT execution command requests.
  """
  @behaviour Kadena.Pact.Command

  alias Kadena.Chainweb.Pact.CommandPayload
  alias Kadena.Cryptography.{Sign, Utils}
  alias Kadena.Pact.Command.{Hash, YamlReader}

  alias Kadena.Types.{
    Command,
    EnvData,
    ExecPayload,
    KeyPair,
    MetaData,
    NetworkID,
    PactPayload,
    Signature,
    SignCommand,
    Signer
  }

  @type cmd :: String.t()
  @type code :: String.t()
  @type command :: Command.t()
  @type data :: EnvData.t() | nil
  @type hash :: String.t()
  @type json_string_payload :: String.t()
  @type keypair :: KeyPair.t()
  @type keypairs :: list(keypair())
  @type meta_data :: MetaData.t()
  @type network_id :: NetworkID.t()
  @type nonce :: String.t()
  @type pact_payload :: PactPayload.t()
  @type signers :: list(Signer.t())
  @type signatures :: list(Signature.t())
  @type sign_command :: SignCommand.t()
  @type sign_commands :: list(sign_command())
  @type valid_command_json_string :: {:ok, json_string_payload()}
  @type valid_command :: {:ok, command()}
  @type valid_payload :: {:ok, pact_payload()}
  @type valid_signatures :: {:ok, signatures()}
  @type valid_sign_commands :: {:ok, sign_commands()}

  @type t :: %__MODULE__{
          network_id: network_id(),
          code: code(),
          data: data(),
          nonce: nonce(),
          meta_data: meta_data(),
          keypairs: keypairs(),
          signers: signers()
        }

  defstruct [
    :network_id,
    :meta_data,
    :code,
    :nonce,
    :data,
    keypairs: [],
    signers: []
  ]

  @impl true
  def new(opts \\ nil)

  def new(opts) when is_list(opts) do
    network_id = Keyword.get(opts, :network_id)
    code = Keyword.get(opts, :code, "")
    data = Keyword.get(opts, :data)
    nonce = Keyword.get(opts, :nonce, "")
    meta_data = Keyword.get(opts, :meta_data, MetaData.new())
    keypairs = Keyword.get(opts, :keypairs, [])
    signers = Keyword.get(opts, :signers, [])

    %__MODULE__{}
    |> set_network(network_id)
    |> set_data(data)
    |> set_code(code)
    |> set_nonce(nonce)
    |> set_metadata(meta_data)
    |> add_signers(signers)
    |> add_keypairs(keypairs)
  end

  def new(_opts), do: %__MODULE__{}

  @impl true
  def from_yaml(path) when is_binary(path) do
    with {:ok, map_result} <- YamlReader.read(path) do
      network_id = Map.get(map_result, "networkId")
      code = Map.get(map_result, "code", "")
      data = Map.get(map_result, "data")
      nonce = Map.get(map_result, "nonce", "")
      meta_data = Map.get(map_result, "publicMeta", MetaData.new())
      keypairs = Map.get(map_result, "keyPairs", [])
      signers = Map.get(map_result, "signers", [])

      %__MODULE__{}
      |> process_metadata(meta_data)
      |> process_keypairs(keypairs)
      |> process_signers(signers)
      |> set_network(network_id)
      |> set_data(data)
      |> set_code(code)
      |> set_nonce(nonce)
    end
  end

  def from_yaml(_path), do: {:error, [path: :invalid]}

  @impl true
  def set_network(%__MODULE__{} = cmd_request, network) do
    case NetworkID.new(network) do
      %NetworkID{} = network_id -> %{cmd_request | network_id: network_id}
      {:error, reason} -> {:error, [network_id: :invalid] ++ reason}
    end
  end

  def set_network({:error, reason}, _network), do: {:error, reason}

  @impl true
  def set_data(%__MODULE__{} = cmd_request, %EnvData{} = data),
    do: %{cmd_request | data: data}

  def set_data(%__MODULE__{} = cmd_request, nil), do: cmd_request

  def set_data(%__MODULE__{} = cmd_request, data) do
    case EnvData.new(data) do
      %EnvData{} -> %{cmd_request | data: data}
      error -> error
    end
  end

  def set_data({:error, reason}, _data), do: {:error, reason}

  @impl true
  def set_code(%__MODULE__{} = cmd_request, code) when is_binary(code),
    do: %{cmd_request | code: code}

  def set_code(%__MODULE__{}, _code), do: {:error, [code: :not_a_string]}
  def set_code({:error, reason}, _code), do: {:error, reason}

  @impl true
  def set_nonce(%__MODULE__{} = cmd_request, nonce) when is_binary(nonce),
    do: %{cmd_request | nonce: nonce}

  def set_nonce(%__MODULE__{}, _nonce), do: {:error, [nonce: :not_a_string]}
  def set_nonce({:error, reason}, _nonce), do: {:error, reason}

  @impl true
  def set_metadata(%__MODULE__{} = cmd_request, %MetaData{} = meta_data),
    do: %{cmd_request | meta_data: meta_data}

  def set_metadata(%__MODULE__{}, _metadata), do: {:error, [metadata: :invalid]}
  def set_metadata({:error, reason}, _metadata), do: {:error, reason}

  @impl true
  def add_keypair(%__MODULE__{keypairs: keypairs} = cmd_request, %KeyPair{} = keypair) do
    cmd_request = %{cmd_request | keypairs: Enum.uniq(keypairs ++ [keypair])}
    set_signers_from_keypair(cmd_request, keypair)
  end

  def add_keypair(%__MODULE__{}, _keypair), do: {:error, [keypair: :invalid]}
  def add_keypair({:error, reason}, _keypair), do: {:error, reason}

  @impl true
  def add_keypairs(%__MODULE__{} = cmd_request, []), do: cmd_request

  def add_keypairs(%__MODULE__{} = cmd_request, [keypair | keypairs]) do
    cmd_request
    |> add_keypair(keypair)
    |> add_keypairs(keypairs)
  end

  def add_keypairs(%__MODULE__{}, _keypairs), do: {:error, [keypairs: :not_a_list]}
  def add_keypairs({:error, reason}, _keypairs), do: {:error, reason}

  @impl true
  def add_signer(%__MODULE__{signers: signers} = cmd_request, %Signer{} = signer),
    do: %{cmd_request | signers: Enum.uniq(signers ++ [signer])}

  def add_signer(%__MODULE__{}, _signer), do: {:error, [signer: :invalid]}
  def add_signer({:error, reason}, _signer), do: {:error, reason}

  @impl true
  def add_signers(%__MODULE__{} = cmd_request, []), do: cmd_request

  def add_signers(%__MODULE__{} = cmd_request, [signer | signers]) do
    cmd_request
    |> add_signer(signer)
    |> add_signers(signers)
  end

  def add_signers(%__MODULE__{}, _signers), do: {:error, [signers: :not_a_signer_list]}
  def add_signers({:error, reason}, _signers), do: {:error, reason}

  @impl true
  def build(
        %__MODULE__{
          keypairs: keypairs,
          code: code,
          data: data
        } = cmd_request
      ) do
    with {:ok, payload} <- create_payload(code, data),
         {:ok, cmd} <- command_to_json_string(payload, cmd_request),
         {:ok, sig_commands} <- sign_commands([], cmd, keypairs),
         {:ok, hash} <- Hash.pull_unique(sig_commands),
         {:ok, signatures} <- build_signatures(sig_commands, []) do
      create_command(hash, signatures, cmd)
    end
  end

  def build(_module), do: {:error, [exec_command_request: :invalid_payload]}

  @spec set_signers_from_keypair(t(), keypair()) :: t()
  defp set_signers_from_keypair(cmd_request, %KeyPair{pub_key: pub_key, clist: clist}) do
    signer = Signer.new(pub_key: pub_key, clist: clist, scheme: :ed25519)
    add_signer(cmd_request, signer)
  end

  @spec create_payload(code :: code(), data :: data()) :: valid_payload()
  defp create_payload(code, data) do
    [code: code, data: data]
    |> ExecPayload.new()
    |> PactPayload.new()
    |> (&{:ok, &1}).()
  end

  @spec command_to_json_string(payload :: pact_payload(), t()) :: valid_command_json_string()
  defp command_to_json_string(payload, %__MODULE__{
         network_id: network_id,
         meta_data: meta_data,
         signers: signers,
         nonce: nonce
       }) do
    [
      network_id: network_id,
      payload: payload,
      meta: meta_data,
      signers: signers,
      nonce: nonce
    ]
    |> CommandPayload.new()
    |> CommandPayload.to_json!()
    |> (&{:ok, &1}).()
  end

  @spec create_command(
          hash :: hash(),
          sigs :: signatures(),
          cmd :: cmd()
        ) :: valid_command()
  defp create_command(hash, sigs, cmd) do
    case Command.new(hash: hash, sigs: sigs, cmd: cmd) do
      %Command{} = command -> {:ok, command}
    end
  end

  @spec sign_commands(signs :: list(), cmd :: json_string_payload(), keypairs()) ::
          valid_sign_commands()
  defp sign_commands([], cmd, []) do
    cmd
    |> Utils.blake2b_hash(byte_size: 32)
    |> Utils.url_encode64()
    |> (&SignCommand.new(hash: &1)).()
    |> (&{:ok, [&1]}).()
  end

  defp sign_commands(signs, _cmd, []), do: {:ok, signs}

  defp sign_commands(signs, cmd, [%KeyPair{} = keypair | keypairs]) do
    signs
    |> sign_command(cmd, keypair)
    |> sign_commands(cmd, keypairs)
  end

  @spec sign_command(signs :: list(), cmd :: json_string_payload(), keypair()) ::
          list()
  defp sign_command(signs, cmd, %KeyPair{} = keypair) do
    {:ok, sign_command} = Sign.sign(cmd, keypair)
    signs ++ [sign_command]
  end

  @spec build_signatures(sign_commands :: sign_commands(), result :: list()) :: valid_signatures()
  defp build_signatures([%SignCommand{sig: nil}], []), do: {:ok, []}
  defp build_signatures([], result), do: {:ok, result}

  defp build_signatures([%SignCommand{sig: sig} | rest], result),
    do: build_signatures(rest, result ++ [Signature.new(sig)])

  defp process_metadata(%__MODULE__{} = cmd_request, %MetaData{} = metadata),
    do: set_metadata(cmd_request, metadata)

  defp process_metadata(%__MODULE__{} = cmd_request, %{} = metadata) do
    case MetaData.new(metadata) do
      %MetaData{} = result -> %{cmd_request | meta_data: result}
      {:error, reason} -> {:error, [meta_data: :invalid] ++ reason}
    end
  end

  defp process_metadata(%__MODULE__{}, _metadata), do: {:error, [metadata: :invalid]}

  defp process_keypairs(%__MODULE__{} = cmd_request, [%{} = keypair_data | rest]) do
    case KeyPair.new(keypair_data) do
      %KeyPair{} = result ->
        cmd_request
        |> add_keypair(result)
        |> process_keypairs(rest)

      {:error, reason} ->
        {:error, [keypair: :invalid] ++ reason}
    end
  end

  defp process_keypairs(%__MODULE__{} = cmd_request, []), do: cmd_request
  defp process_keypairs(%__MODULE__{}, _keypair), do: {:error, [keypair: :invalid]}
  defp process_keypairs({:error, reason}, _keypairs), do: {:error, reason}

  defp process_signers(%__MODULE__{} = cmd_request, [%{} = signers_data | rest]) do
    case Signer.new(signers_data) do
      %Signer{} = result ->
        cmd_request
        |> add_signer(result)
        |> process_signers(rest)

      {:error, reason} ->
        {:error, [signers: :invalid] ++ reason}
    end
  end

  defp process_signers(%__MODULE__{} = cmd_request, []), do: cmd_request
  defp process_signers({:error, reason}, _signers), do: {:error, reason}
  defp process_signers(%__MODULE__{}, _signers), do: {:error, [signers: :invalid]}
end