https://gitlab.com/tezos/tezos
Raw File
Tip revision: 6824bb6a0ebfd1e64166797bb985d7dc68898d46 authored by ovidiu deac on 26 October 2022, 13:48:48 UTC
when Initial or First_after return the fuel_left instead of initial fuel
Tip revision: 6824bb6
client_config.ml
(*****************************************************************************)
(*                                                                           *)
(* Open Source License                                                       *)
(* Copyright (c) 2018 Dynamic Ledger Solutions, Inc. <contact@tezos.com>     *)
(* Copyright (c) 2018-2020 Nomadic Labs, <contact@nomadic-labs.com>          *)
(*                                                                           *)
(* Permission is hereby granted, free of charge, to any person obtaining a   *)
(* copy of this software and associated documentation files (the "Software"),*)
(* to deal in the Software without restriction, including without limitation *)
(* the rights to use, copy, modify, merge, publish, distribute, sublicense,  *)
(* and/or sell copies of the Software, and to permit persons to whom the     *)
(* Software is furnished to do so, subject to the following conditions:      *)
(*                                                                           *)
(* The above copyright notice and this permission notice shall be included   *)
(* in all copies or substantial portions of the Software.                    *)
(*                                                                           *)
(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*)
(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,  *)
(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL   *)
(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*)
(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING   *)
(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER       *)
(* DEALINGS IN THE SOFTWARE.                                                 *)
(*                                                                           *)
(*****************************************************************************)

(* Tezos Command line interface - Configuration and Arguments Parsing *)

type cli_args = {
  chain : Chain_services.chain;
  block : Shell_services.block;
  confirmations : int option;
  sources : Tezos_proxy.Light.sources_config option;
  password_filename : string option;
  protocol : Protocol_hash.t option;
  print_timings : bool;
  log_requests : bool;
  better_errors : bool;
  client_mode : client_mode;
}

and client_mode = [`Mode_client | `Mode_light | `Mode_mockup | `Mode_proxy]

let all_modes = [`Mode_client; `Mode_light; `Mode_mockup; `Mode_proxy]

let client_mode_to_string = function
  | `Mode_client -> "client"
  | `Mode_light -> "light"
  | `Mode_mockup -> "mockup"
  | `Mode_proxy -> "proxy"

type error += Invalid_endpoint_arg of string

type error += Invalid_media_type_arg of string

type error += Suppressed_arg of {args : string list; by : string}

type error += Invalid_chain_argument of string

type error += Invalid_block_argument of string

type error += Invalid_protocol_argument of string

type error += Invalid_port_arg of string

type error += Invalid_remote_signer_argument of string

type error += Invalid_wait_arg of string

type error += Invalid_mode_arg of string

let () =
  register_error_kind
    `Branch
    ~id:"badMediaTypeArgument"
    ~title:"Bad Media Type Argument"
    ~description:"Media type argument could not be parsed"
    ~pp:(fun ppf s ->
      Format.fprintf
        ppf
        "media_type parameter must be 'json', 'binary' or 'any'. Got: %s"
        s)
    Data_encoding.(obj1 (req "value" string))
    (function Invalid_media_type_arg s -> Some s | _ -> None)
    (fun s -> Invalid_media_type_arg s) ;
  register_error_kind
    `Branch
    ~id:"badEndpointArgument"
    ~title:"Bad Endpoint Argument"
    ~description:"Endpoint argument could not be parsed"
    ~pp:(fun ppf s -> Format.pp_print_string ppf s)
    Data_encoding.(obj1 (req "value" string))
    (function Invalid_endpoint_arg s -> Some s | _ -> None)
    (fun s -> Invalid_endpoint_arg s) ;
  register_error_kind
    `Branch
    ~id:"suppressedArgument"
    ~title:"Suppressed Argument"
    ~description:"Certain arguments are conflicting with some other"
    ~pp:(fun ppf (args, by) ->
      Format.fprintf
        ppf
        (if List.compare_length_with args 1 = 0 then
         "Option %s is in conflict with %s"
        else "Options %s are in conflict with %s")
        (String.concat " and " args)
        by)
    Data_encoding.(obj2 (req "suppressed" (list string)) (req "by" string))
    (function Suppressed_arg e -> Some (e.args, e.by) | _ -> None)
    (fun (args, by) -> Suppressed_arg {args; by}) ;
  register_error_kind
    `Branch
    ~id:"badChainArgument"
    ~title:"Bad Chain Argument"
    ~description:"Chain argument could not be parsed"
    ~pp:(fun ppf s ->
      Format.fprintf ppf "Value %s is not a value chain reference." s)
    Data_encoding.(obj1 (req "value" string))
    (function Invalid_chain_argument s -> Some s | _ -> None)
    (fun s -> Invalid_chain_argument s) ;
  register_error_kind
    `Branch
    ~id:"badBlockArgument"
    ~title:"Bad Block Argument"
    ~description:"Block argument could not be parsed"
    ~pp:(fun ppf s ->
      Format.fprintf ppf "Value %s is not a value block reference." s)
    Data_encoding.(obj1 (req "value" string))
    (function Invalid_block_argument s -> Some s | _ -> None)
    (fun s -> Invalid_block_argument s) ;
  register_error_kind
    `Branch
    ~id:"badProtocolArgument"
    ~title:"Bad Protocol Argument"
    ~description:"Protocol argument could not be parsed"
    ~pp:(fun ppf s ->
      Format.fprintf ppf "Value %s does not correspond to any known protocol." s)
    Data_encoding.(obj1 (req "value" string))
    (function Invalid_protocol_argument s -> Some s | _ -> None)
    (fun s -> Invalid_protocol_argument s) ;
  register_error_kind
    `Branch
    ~id:"invalidPortArgument"
    ~title:"Bad Port Argument"
    ~description:"Port argument could not be parsed"
    ~pp:(fun ppf s -> Format.fprintf ppf "Value %s is not a valid TCP port." s)
    Data_encoding.(obj1 (req "value" string))
    (function Invalid_port_arg s -> Some s | _ -> None)
    (fun s -> Invalid_port_arg s) ;
  register_error_kind
    `Branch
    ~id:"invalid_remote_signer_argument"
    ~title:"Unexpected URI of remote signer"
    ~description:"The remote signer argument could not be parsed"
    ~pp:(fun ppf s -> Format.fprintf ppf "Value '%s' is not a valid URI." s)
    Data_encoding.(obj1 (req "value" string))
    (function Invalid_remote_signer_argument s -> Some s | _ -> None)
    (fun s -> Invalid_remote_signer_argument s) ;
  register_error_kind
    `Branch
    ~id:"invalidWaitArgument"
    ~title:"Bad Wait Argument"
    ~description:"Wait argument could not be parsed"
    ~pp:(fun ppf s ->
      Format.fprintf
        ppf
        "Value %s is not a valid number of confirmation, nor 'none'."
        s)
    Data_encoding.(obj1 (req "value" string))
    (function Invalid_wait_arg s -> Some s | _ -> None)
    (fun s -> Invalid_wait_arg s) ;
  register_error_kind
    `Branch
    ~id:"invalidModeArgument"
    ~title:"Invalid Mode Argument"
    ~description:"Mode argument could not be parsed"
    ~pp:(fun ppf s ->
      let enclose s = "\"" ^ s ^ "\"" in
      let pp_mode s = enclose @@ client_mode_to_string s in
      let mode_strings = List.map pp_mode all_modes in
      Format.fprintf
        ppf
        "Value \"%s\" is invalid. It should be one of: %s"
        s
        (String.concat " or " mode_strings))
    Data_encoding.(obj1 (req "value" string))
    (function Invalid_mode_arg s -> Some s | _ -> None)
    (fun s -> Invalid_mode_arg s)

let home = try Sys.getenv "HOME" with Not_found -> "/root"

let base_dir_env_name = "TEZOS_CLIENT_DIR"

let default_base_dir =
  try Sys.getenv base_dir_env_name
  with Not_found -> Filename.concat home ".tezos-client"

let default_chain = `Main

let default_block = `Head 0

let default_endpoint = Uri.of_string "http://localhost:8732"

let default_media_type = Media_type.Command_line.Any

open Filename.Infix

module Cfg_file = struct
  (* the fields [node_addr], [node_port], and [tls] are deprecated by
   * and should not coexist with [endpoint].
   * see [parse_config_args] for the exact handling *)
  type t = {
    base_dir : string;
    node_addr : string option;
    node_port : int option;
    tls : bool option;
    media_type : Media_type.Command_line.t option;
    endpoint : Uri.t option;
    web_port : int;
    remote_signer : Uri.t option;
    confirmations : int option;
    password_filename : string option;
  }

  let default =
    {
      base_dir = default_base_dir;
      media_type = None;
      endpoint = None;
      node_addr = None;
      node_port = None;
      tls = None;
      web_port = 8080;
      remote_signer = None;
      confirmations = Some 0;
      password_filename = None;
    }

  open Data_encoding

  let encoding =
    conv
      (fun {
             base_dir;
             node_addr;
             node_port;
             tls;
             media_type;
             endpoint;
             web_port;
             remote_signer;
             confirmations;
             password_filename;
           } ->
        ( base_dir,
          node_addr,
          node_port,
          tls,
          media_type,
          endpoint,
          Some web_port,
          remote_signer,
          confirmations,
          password_filename ))
      (fun ( base_dir,
             node_addr,
             node_port,
             tls,
             media_type,
             endpoint,
             web_port,
             remote_signer,
             confirmations,
             password_filename ) ->
        let web_port = Option.value ~default:default.web_port web_port in
        {
          base_dir;
          node_addr;
          node_port;
          tls;
          media_type;
          endpoint;
          web_port;
          remote_signer;
          confirmations;
          password_filename;
        })
      (obj10
         (req "base_dir" string)
         (opt "node_addr" string)
         (opt "node_port" uint16)
         (opt "tls" bool)
         (opt "media_type" Media_type.Command_line.encoding)
         (opt "endpoint" RPC_encoding.uri_encoding)
         (opt "web_port" uint16)
         (opt "remote_signer" RPC_encoding.uri_encoding)
         (opt "confirmations" int8)
         (opt "password_filename" string))

  let from_json json = Data_encoding.Json.destruct encoding json

  let read fp =
    let open Lwt_result_syntax in
    let* json = Lwt_utils_unix.Json.read_file fp in
    return (from_json json)

  let write out cfg =
    Lwt_utils_unix.Json.write_file
      out
      (Data_encoding.Json.construct encoding cfg)
end

let default_cli_args =
  {
    chain = default_chain;
    block = default_block;
    confirmations = Some 0;
    sources = None;
    password_filename = None;
    protocol = None;
    print_timings = false;
    log_requests = false;
    better_errors = false;
    client_mode = `Mode_client;
  }

let string_parameter () : (string, #Client_context.full) Tezos_clic.parameter =
  Tezos_clic.parameter (fun _ x -> Lwt.return_ok x)

let media_type_parameter () :
    (Media_type.Command_line.t, #Client_context.full) Tezos_clic.parameter =
  let open Lwt_result_syntax in
  Tezos_clic.parameter (fun _ x ->
      match Media_type.Command_line.parse_cli_parameter x with
      | Some v -> return v
      | None -> tzfail (Invalid_media_type_arg x))

let endpoint_parameter () =
  let open Lwt_result_syntax in
  Tezos_clic.parameter (fun _ x ->
      let parsed = Uri.of_string x in
      let* _ =
        match Uri.scheme parsed with
        | Some "http" | Some "https" -> return ()
        | _ ->
            tzfail
              (Invalid_endpoint_arg
                 ("only http and https endpoints are supported: " ^ x))
      in
      match (Uri.query parsed, Uri.fragment parsed) with
      | [], None -> return parsed
      | _ ->
          tzfail
            (Invalid_endpoint_arg
               ("endpoint uri should not have query string or fragment: " ^ x)))

let sources_parameter () =
  let open Lwt_result_syntax in
  Tezos_clic.parameter (fun _ path ->
      let*! r = Lwt_utils_unix.Json.read_file path in
      match r with
      | Error errs ->
          failwith
            "Can't parse the file specified by --sources as JSON: %s@,%a"
            path
            pp_print_trace
            errs
      | Ok json -> (
          try
            match Tezos_proxy.Light.destruct_sources_config json with
            | Ok sources_cfg -> return sources_cfg
            | Error msg -> failwith "%s" msg
          with exn ->
            failwith
              "Can't parse the file specified by --sources: %s@,%a"
              path
              (fun ppf exn -> Json_encoding.print_error ppf exn)
              exn))

let chain_parameter () =
  Tezos_clic.parameter (fun _ chain ->
      let open Lwt_result_syntax in
      match Chain_services.parse_chain chain with
      | Error _ -> tzfail (Invalid_chain_argument chain)
      | Ok chain -> return chain)

let block_parameter () =
  Tezos_clic.parameter (fun _ block ->
      let open Lwt_result_syntax in
      match Block_services.parse_block block with
      | Error _ -> tzfail (Invalid_block_argument block)
      | Ok block -> return block)

let wait_parameter () =
  Tezos_clic.parameter (fun _ wait ->
      let open Lwt_result_syntax in
      match wait with
      | "no" | "none" -> return_none
      | _ -> (
          match int_of_string_opt wait with
          | Some w when 0 <= w -> return_some w
          | None | Some _ -> tzfail (Invalid_wait_arg wait)))

let protocol_parameter () =
  Tezos_clic.parameter (fun _ arg ->
      let open Lwt_result_syntax in
      match
        Seq.filter
          (fun (hash, _commands) ->
            String.has_prefix ~prefix:arg (Protocol_hash.to_b58check hash))
          (Client_commands.get_versions ())
        @@ ()
      with
      | Cons ((hash, _commands), _) -> return_some hash
      | Nil -> tzfail (Invalid_protocol_argument arg))

(* Command-line only args (not in config file) *)
let base_dir_arg () =
  Tezos_clic.arg
    ~long:"base-dir"
    ~short:'d'
    ~placeholder:"path"
    ~doc:
      (Format.asprintf
         "@[<v>@[<2>client data directory (absent: %s env)@,\
          The directory where the Tezos client will store all its data.@,\
          If absent, its value is the value of the %s@,\
          environment variable. If %s is itself not specified,@,\
          defaults to %s@]@]@."
         base_dir_env_name
         base_dir_env_name
         base_dir_env_name
         default_base_dir)
    (string_parameter ())

let config_file_arg () =
  Tezos_clic.arg
    ~long:"config-file"
    ~short:'c'
    ~placeholder:"path"
    ~doc:"configuration file"
    (string_parameter ())

let timings_switch () =
  Tezos_clic.switch ~long:"timings" ~short:'t' ~doc:"show RPC request times" ()

let chain_arg () =
  Tezos_clic.default_arg
    ~long:"chain"
    ~placeholder:"hash|tag"
    ~doc:
      "chain on which to apply contextual commands (commands dependent on the \
       context associated with the specified chain). Possible tags are 'main' \
       and 'test'."
    ~default:(Chain_services.to_string default_cli_args.chain)
    (chain_parameter ())

let block_arg () =
  Tezos_clic.default_arg
    ~long:"block"
    ~short:'b'
    ~placeholder:"hash|level|tag"
    ~doc:
      "block on which to apply contextual commands (commands dependent on the \
       context associated with the specified block). Possible tags include \
       'head' and 'genesis' +/- an optional offset (e.g. \"octez-client -b \
       head-1 get timestamp\"). Note that block queried must exist in node's \
       storage."
    ~default:(Block_services.to_string default_cli_args.block)
    (block_parameter ())

let wait_arg () =
  Tezos_clic.arg
    ~long:"wait"
    ~short:'w'
    ~placeholder:"none|<int>"
    ~doc:
      "how many confirmation blocks are needed before an operation is \
       considered included"
    (wait_parameter ())

let protocol_arg () =
  Tezos_clic.arg
    ~long:"protocol"
    ~short:'p'
    ~placeholder:"hash"
    ~doc:"use commands of a specific protocol"
    (protocol_parameter ())

let log_requests_switch () =
  Tezos_clic.switch
    ~long:"log-requests"
    ~short:'l'
    ~doc:"log all requests to the node"
    ()

let better_errors () =
  Tezos_clic.switch
    ~long:"better-errors"
    ~doc:
      "Error reporting is more detailed. Can be used if a call to an RPC fails \
       or if you don't know the input accepted by the RPC. It may happen that \
       the RPC calls take more time however."
    ()

(* Command-line args which can be set in config file as well *)
let addr_confdesc = "-A/--addr ('node_addr' in config file)"

let addr_arg () =
  Tezos_clic.arg
    ~long:"addr"
    ~short:'A'
    ~placeholder:"IP addr|host"
    ~doc:"[DEPRECATED: use --endpoint instead] IP address of the node"
    (string_parameter ())

let port_confdesc = "-P/--port ('node_port' in config file)"

let port_arg () =
  Tezos_clic.arg
    ~long:"port"
    ~short:'P'
    ~placeholder:"number"
    ~doc:"[DEPRECATED: use --endpoint instead] RPC port of the node"
    (Tezos_clic.parameter (fun _ x ->
         let open Lwt_result_syntax in
         match int_of_string_opt x with
         | Some i -> return i
         | None -> tzfail (Invalid_port_arg x)))

let tls_confdesc = "-S/--tls ('tls' in config file)"

let tls_switch () =
  Tezos_clic.switch
    ~long:"tls"
    ~short:'S'
    ~doc:"[DEPRECATED: use --endpoint instead] use TLS to connect to node."
    ()

let media_type_confdesc = "-m/--media-type"

let media_type_arg () =
  Tezos_clic.arg
    ~long:"media-type"
    ~short:'m'
    ~placeholder:"json, binary, any or default"
    ~doc:
      "Sets the \"media-type\" value for the \"accept\" header for RPC \
       requests to the node. The media accept header indicates to the node \
       which format of data serialisation is supported. Use the value \"json\" \
       for serialisation to the JSON format.\n\
      \          Use the value \"binary\" for faster but less human-readable \
       binary serialisation format."
    (media_type_parameter ())

let endpoint_confdesc = "-E/--endpoint ('endpoint' in config file)"

let endpoint_arg () =
  Tezos_clic.arg
    ~long:"endpoint"
    ~short:'E'
    ~placeholder:"uri"
    ~doc:
      "HTTP(S) endpoint of the node RPC interface; e.g. 'http://localhost:8732'"
    (endpoint_parameter ())

let sources_arg () =
  Tezos_clic.arg
    ~long:"sources"
    ~short:'s'
    ~placeholder:"path"
    ~doc:
      ("path to JSON file containing sources for --mode light. Example file \
        content: " ^ Tezos_proxy.Light.example_sources)
    (sources_parameter ())

let remote_signer_arg () =
  Tezos_clic.arg
    ~long:"remote-signer"
    ~short:'R'
    ~placeholder:"uri"
    ~doc:"URI of the remote signer"
    (Tezos_clic.parameter (fun _ x ->
         Tezos_signer_backends_unix.Remote.parse_base_uri x))

let password_filename_arg () =
  Tezos_clic.arg
    ~long:"password-filename"
    ~short:'f'
    ~placeholder:"filename"
    ~doc:"path to the password filename"
    (string_parameter ())

let client_mode_arg () =
  let mode_strings = List.map client_mode_to_string all_modes in
  let parse_client_mode (str : string) : client_mode tzresult =
    let open Result_syntax in
    let* modes_and_strings =
      List.combine
        ~when_different_lengths:(TzTrace.make @@ Exn (Failure __LOC__))
        mode_strings
        all_modes
    in
    match List.assoc_opt ~equal:String.equal str modes_and_strings with
    | None -> tzfail (Invalid_mode_arg str)
    | Some mode -> return mode
  in
  Tezos_clic.default_arg
    ~short:'M'
    ~long:"mode"
    ~placeholder:(String.concat "|" mode_strings)
    ~doc:"how to interact with the node"
    ~default:(client_mode_to_string `Mode_client)
    (Tezos_clic.parameter
       ~autocomplete:(fun _ -> Lwt.return_ok mode_strings)
       (fun _ param -> Lwt.return (parse_client_mode param)))

let read_config_file config_file =
  let open Lwt_result_syntax in
  let*! r = Lwt_utils_unix.Json.read_file config_file in
  match r with
  | Error errs ->
      failwith
        "Can't parse the configuration file as a JSON: %s@,%a"
        config_file
        pp_print_trace
        errs
  | Ok cfg_json -> (
      try return @@ Cfg_file.from_json cfg_json
      with exn ->
        failwith
          "Can't parse the configuration file: %s@,%a"
          config_file
          (fun ppf exn -> Json_encoding.print_error ppf exn)
          exn)

let fail_on_non_mockup_dir (cctxt : #Client_context.full) =
  let open Lwt_result_syntax in
  let base_dir = cctxt#get_base_dir in
  let open Tezos_mockup.Persistence in
  let* b = classify_base_dir base_dir in
  match b with
  | Base_dir_does_not_exist | Base_dir_is_file | Base_dir_is_nonempty
  | Base_dir_is_empty ->
      failwith
        "base directory at %s should be a mockup directory for this operation \
         to be allowed (it may contain sensitive data otherwise). What you \
         likely want is calling `octez-client --mode mockup --base-dir \
         /some/dir create mockup` where `/some/dir` is **fresh** and **empty** \
         and redo this operation, specifying `--base-dir /some/dir` this time."
        base_dir
  | Base_dir_is_mockup -> return_unit

let default_config_file_name = "config"

let mockup_bootstrap_accounts = "bootstrap-accounts"

let mockup_protocol_constants = "protocol-constants"

(* The implementation of ["config"; "show"] when --mode is "client" *)
let config_show_client (cctxt : #Client_context.full) (config_file : string) cfg
    =
  let open Lwt_syntax in
  let* () =
    if not @@ Sys.file_exists config_file then
      cctxt#warning
        "@[<v 2>Warning: no config file at %s,@,\
         displaying the default configuration.@]"
        config_file
    else Lwt.return_unit
  in
  let* () =
    cctxt#message
      "%a@,"
      Data_encoding.Json.pp
      (Data_encoding.Json.construct Cfg_file.encoding cfg)
  in
  return_ok_unit

(* The implementation of ["config"; "show"] when --mode is "mockup" *)
let config_show_mockup (cctxt : #Client_context.full)
    (protocol_hash_opt : Protocol_hash.t option) (base_dir : string) =
  let open Lwt_result_syntax in
  let* () = fail_on_non_mockup_dir cctxt in
  let* mockup, _ =
    Tezos_mockup.Persistence.get_mockup_context_from_disk
      ~base_dir
      ~protocol_hash:protocol_hash_opt
      cctxt
  in
  let (module Mockup) = mockup in
  let json_pp encoding ppf value =
    Data_encoding.Json.pp ppf (Data_encoding.Json.construct encoding value)
  in
  let* bootstrap_accounts_string = Mockup.default_bootstrap_accounts cctxt in
  let*! () =
    cctxt#message
      "@[<v>Default value of --%s:@,%s@]"
      mockup_bootstrap_accounts
      bootstrap_accounts_string
  in
  let* protocol_constants = Mockup.default_protocol_constants cctxt in
  let*! () =
    cctxt#message
      "@[<v>Default value of --%s:@,%a@]"
      mockup_protocol_constants
      (json_pp Mockup.protocol_constants_encoding)
      protocol_constants
  in
  return_unit

(* The implementation of ["config"; "init"] when --mode is "client" *)
let config_init_client config_file cfg =
  if not (Sys.file_exists config_file) then Cfg_file.(write config_file cfg)
    (* Should be default or command would have failed *)
  else failwith "Config file already exists at location: %s" config_file

(* The implementation of ["config"; "init"] when --mode is "mockup" *)
let config_init_mockup cctxt protocol_hash_opt bootstrap_accounts_file
    protocol_constants_file base_dir =
  let open Lwt_result_syntax in
  let* () = fail_on_non_mockup_dir cctxt in
  let* () =
    fail_when
      (Sys.file_exists bootstrap_accounts_file)
      (error_of_fmt
         "Config file to write value of --%s exists already: %s"
         mockup_bootstrap_accounts
         bootstrap_accounts_file)
  in
  let* () =
    fail_when
      (Sys.file_exists protocol_constants_file)
      (error_of_fmt
         "Config file to write value of --%s exists already: %s"
         mockup_protocol_constants
         protocol_constants_file)
  in
  let* mockup, _ =
    Tezos_mockup.Persistence.get_mockup_context_from_disk
      ~base_dir
      ~protocol_hash:protocol_hash_opt
      cctxt
  in
  let (module Mockup) = mockup in
  let* string_to_write = Mockup.default_bootstrap_accounts cctxt in
  let*! _ =
    Lwt_utils_unix.create_file bootstrap_accounts_file string_to_write
  in
  let*! () =
    cctxt#message
      "Written default --%s file: %s"
      mockup_bootstrap_accounts
      bootstrap_accounts_file
  in
  let* protocol_constants = Mockup.default_protocol_constants cctxt in
  let string_to_write =
    Data_encoding.Json.construct
      Mockup.protocol_constants_encoding
      protocol_constants
  in
  let* () =
    Lwt_utils_unix.Json.write_file protocol_constants_file string_to_write
  in
  let*! () =
    cctxt#message
      "Written default --%s file: %s"
      mockup_protocol_constants
      protocol_constants_file
  in
  return_unit

let commands config_file cfg (client_mode : client_mode)
    (protocol_hash_opt : Protocol_hash.t option) (base_dir : string) =
  let open Tezos_clic in
  let group =
    {
      name = "config";
      title = "Commands for editing and viewing the client's config file";
    }
  in
  [
    command
      ~group
      ~desc:
        "Show the current config (config file content + command line \
         arguments) or the mockup config files if `--mode mockup` is \
         specified."
      no_options
      (fixed ["config"; "show"])
      (fun () (cctxt : #Client_context.full) ->
        match client_mode with
        | `Mode_client | `Mode_light | `Mode_proxy ->
            config_show_client cctxt config_file cfg
        | `Mode_mockup -> config_show_mockup cctxt protocol_hash_opt base_dir);
    command
      ~group
      ~desc:"Reset the config file to the factory defaults."
      no_options
      (fixed ["config"; "reset"])
      (fun () _cctxt -> Cfg_file.(write config_file default));
    command
      ~group
      ~desc:
        "Update the config based on the current cli values.\n\
         Loads the current configuration (default or as specified with \
         `-config-file`), applies alterations from other command line \
         arguments (such as the node's address, etc.), and overwrites the \
         updated configuration file."
      no_options
      (fixed ["config"; "update"])
      (fun () _cctxt -> Cfg_file.(write config_file cfg));
    command
      ~group
      ~desc:
        "Create config file(s) based on the current CLI values.\n\
         If the `-file` option is not passed, this will initialize the default \
         config file, based on default parameters, altered by other command \
         line options (such as the node's address, etc.).\n\
         Otherwise, it will create a new config file, based on the default \
         parameters (or the the ones specified with `-config-file`), altered \
         by other command line options.\n\n\
         If `-mode mockup` is specified, this will initialize the mockup's \
         default files instead of the config file. Use `-bootstrap-accounts` \
         and `-protocol-constants` to specify custom paths.\n\n\
         The command will always fail if file(s) to create exist already"
      (args3
         (default_arg
            ~long:"output"
            ~short:'o'
            ~placeholder:"path"
            ~doc:"path at which to create the file"
            ~default:(cfg.base_dir // default_config_file_name)
            (parameter (fun _ctx str -> Lwt.return_ok str)))
         (default_arg
            ~long:mockup_bootstrap_accounts
            ~placeholder:"path"
            ~doc:"path at which to create the file"
            ~default:((cfg.base_dir // mockup_bootstrap_accounts) ^ ".json")
            (parameter (fun _ctx str -> Lwt.return_ok str)))
         (default_arg
            ~long:mockup_protocol_constants
            ~placeholder:"path"
            ~doc:"path at which to create the file"
            ~default:((cfg.base_dir // mockup_protocol_constants) ^ ".json")
            (parameter (fun _ctx str -> Lwt.return_ok str))))
      (fixed ["config"; "init"])
      (fun (config_file, bootstrap_accounts_file, protocol_constants_file) cctxt ->
        match client_mode with
        | `Mode_client | `Mode_light | `Mode_proxy ->
            config_init_client config_file cfg
        | `Mode_mockup ->
            config_init_mockup
              cctxt
              protocol_hash_opt
              bootstrap_accounts_file
              protocol_constants_file
              base_dir);
  ]

let global_options () =
  Tezos_clic.args18
    (base_dir_arg ())
    (config_file_arg ())
    (timings_switch ())
    (chain_arg ())
    (block_arg ())
    (wait_arg ())
    (protocol_arg ())
    (log_requests_switch ())
    (better_errors ())
    (addr_arg ())
    (port_arg ())
    (tls_switch ())
    (media_type_arg ())
    (endpoint_arg ())
    (sources_arg ())
    (remote_signer_arg ())
    (password_filename_arg ())
    (client_mode_arg ())

type parsed_config_args = {
  parsed_config_file : Cfg_file.t option;
  parsed_args : cli_args option;
  config_commands : Client_context.full Tezos_clic.command list;
  base_dir : string option;
  require_auth : bool;
}

let default_parsed_config_args =
  {
    parsed_config_file = None;
    parsed_args = None;
    config_commands = [];
    base_dir = None;
    require_auth = false;
  }

(* Check that the base directory is actually in the right configuration for
 * the mode used by the client.
 *
 * Depending on the criticality of the compatibility issue, this function fails
 * (when all/most commands will fail) or emits a warning (some commands may
 * fail).
 *)
let check_base_dir_for_mode (ctx : #Client_context.full) client_mode base_dir =
  let open Lwt_result_syntax in
  let open Tezos_mockup.Persistence in
  let* base_dir_class = classify_base_dir base_dir in
  match client_mode with
  | `Mode_client | `Mode_light | `Mode_proxy -> (
      match base_dir_class with
      | Base_dir_is_mockup ->
          failwith
            "Base directory %s is in mockup mode while operation is in %s mode"
            base_dir
          @@ client_mode_to_string client_mode
      (* You might be creating a mockup directory here *)
      | Base_dir_is_empty -> return_unit
      | Base_dir_is_file | Base_dir_does_not_exist ->
          (* This case is checked in by the caller so that it should not happen *)
          failwith
            "Error for base-dir %s should not have happened (this is due to %a)"
            base_dir
            pp_base_dir_class
            base_dir_class
      | _ -> return_unit)
  | `Mode_mockup -> (
      let warn_might_not_work explain =
        let*! () =
          ctx#warning
            "@[<hv>Base directory %s %a@ Some commands (e.g., transfer) might \
             not work correctly.@]"
            base_dir
            explain
            ()
        in
        return_unit
      in
      let show_cmd ppf () =
        Format.fprintf
          ppf
          "./octez-client --mode mockup --base-dir %s create mockup"
          base_dir
      in
      match base_dir_class with
      | Base_dir_is_empty ->
          warn_might_not_work (fun ppf () ->
              Format.fprintf
                ppf
                "is empty.@ Move directory %s away and create it anew with:@ \
                 %a@ or use another directory name."
                base_dir
                show_cmd
                ())
      | Base_dir_does_not_exist ->
          warn_might_not_work (fun ppf () ->
              Format.fprintf
                ppf
                "does not exist.@ Create it with:@ %a"
                show_cmd
                ())
      | Base_dir_is_nonempty ->
          warn_might_not_work (fun ppf () ->
              Format.fprintf
                ppf
                "is non empty.@ Move directory %s away and create it anew \
                 with:@ %a@ or use another directory name."
                base_dir
                show_cmd
                ())
      | Base_dir_is_file ->
          warn_might_not_work (fun ppf () ->
              Format.fprintf
                ppf
                "is a file.@ This is expected to be a directory.@ It can be \
                 created with:@ %a"
                show_cmd
                ())
      | Base_dir_is_mockup -> return_unit)

let build_endpoint addr port tls =
  let updatecomp updatef ov uri =
    match ov with Some x -> updatef uri (Some x) | None -> uri
  in
  let scheme = Option.map (function true -> "https" | false -> "http") tls in
  let url = default_endpoint in
  url
  |> updatecomp Uri.with_host addr
  |> updatecomp Uri.with_port port
  |> updatecomp Uri.with_scheme scheme

let light_mode_checks mode endpoint sources =
  let open Lwt_result_syntax in
  match (mode, sources) with
  | `Mode_client, None | `Mode_mockup, None | `Mode_proxy, None ->
      (* No --mode light, no --sources; good *)
      return_unit
  | `Mode_client, Some _ | `Mode_mockup, Some _ | `Mode_proxy, Some _ ->
      (* --sources without the light mode: wrong *)
      failwith
        "--sources is specified whereas mode is %s. --sources should only be \
         used with --mode light."
      @@ client_mode_to_string mode
  | `Mode_light, None ->
      (* --mode light without --sources: wrong *)
      failwith
        "--mode light requires passing --sources. Example --sources file: %s"
        Tezos_proxy.Light.example_sources
  | `Mode_light, Some sources ->
      let sources_uris = Tezos_proxy.Light.sources_config_to_uris sources in
      if List.mem ~equal:Uri.equal endpoint sources_uris then return_unit
      else
        let uri_to_json_string uri =
          Uri.to_string uri |> Printf.sprintf "\"%s\""
        in
        failwith
          "Value of --endpoint is %a. Therefore, this URI MUST be in field \
           'uris' of --sources (whose value is: [%s]). If you did not specify \
           --endpoint, it is being defaulted; you may hereby specify \
           --endpoint %a to fix this error."
          Uri.pp
          endpoint
          (String.concat ", " @@ List.map uri_to_json_string sources_uris)
          (* By the check done in Light.mk_sources_config, [sources_uris]
             cannot be empty, but we don't rely on this here, by using
             pp_print_option. *)
          (Format.pp_print_option Uri.pp)
          (List.hd sources_uris)

let parse_config_args (ctx : #Client_context.full) argv =
  let open Lwt_result_syntax in
  let* ( ( base_dir,
           config_file,
           timings,
           chain,
           block,
           confirmations,
           protocol,
           log_requests,
           better_errors,
           node_addr,
           node_port,
           tls,
           media_type,
           endpoint,
           sources,
           remote_signer,
           password_filename,
           client_mode ),
         remaining ) =
    Tezos_clic.parse_global_options (global_options ()) ctx argv
  in
  let* base_dir =
    match base_dir with
    | None ->
        let base_dir = default_base_dir in
        let* () =
          unless
            (* Mockup mode will create the base directory on need *)
            (client_mode = `Mode_mockup || Sys.file_exists base_dir)
            (fun () ->
              let*! () = Lwt_utils_unix.create_dir base_dir in
              return_unit)
        in
        return base_dir
    | Some dir -> (
        match client_mode with
        | `Mode_client | `Mode_light | `Mode_proxy ->
            if not (Sys.file_exists dir) then
              failwith
                "Specified --base-dir does not exist. Please create the \
                 directory and try again."
            else if Sys.is_directory dir then return dir
            else failwith "Specified --base-dir must be a directory"
        | `Mode_mockup ->
            (* In mockup mode base dir may be created automatically. *)
            return dir)
  in
  let* () = check_base_dir_for_mode ctx client_mode base_dir in
  let* () =
    when_
      (Option.is_some sources && client_mode <> `Mode_light)
      (fun () ->
        failwith
          "--sources is specific to --mode light, please do not specify it \
           with --mode %s."
        @@ client_mode_to_string client_mode)
  in
  let* config_file =
    match config_file with
    | None -> return @@ (base_dir // default_config_file_name)
    | Some config_file ->
        if Sys.file_exists config_file then return config_file
        else
          failwith
            "Config file specified in option does not exist. Use `client \
             config init` to create one."
  in
  let config_dir = Filename.dirname config_file in
  let protocol = match protocol with None -> None | Some p -> p in
  let* cfg =
    if not (Sys.file_exists config_file) then
      return {Cfg_file.default with base_dir}
    else read_config_file config_file
  in
  (* endpoint logic:
   *   1) when --endpoint provided as argument,
   *      use it but check no presence of --addr, --port, or --tls
   *   2) otherwise, merge --addr, --port, and --tls with config file; then
   *        2a) --endpoint exists in config file,
   *            use it but check no presence of merged --addr, --port, or --tls
   *        2b) synthesize --endpoint from --addr, --port, and --tls *)
  let check_absence addr port tls =
    let checkabs argdesc = function
      | None -> fun x -> x
      | _ -> fun x -> x @ [argdesc]
    in
    let superr =
      []
      |> checkabs addr_confdesc addr
      |> checkabs port_confdesc port
      |> checkabs tls_confdesc tls
    in
    if superr <> [] then
      tzfail (Suppressed_arg {args = superr; by = endpoint_confdesc})
    else return ()
  in
  let tls = if tls then Some true else None in
  let* endpoint =
    match endpoint with
    | Some endpt ->
        let* _ = check_absence node_addr node_port tls in
        return endpt
    | None -> (
        let node_addr = Option.either node_addr cfg.node_addr in
        let node_port = Option.either node_port cfg.node_port in
        let tls = Option.either tls cfg.tls in
        match cfg.endpoint with
        | Some endpt ->
            let* _ = check_absence node_addr node_port tls in
            return endpt
        | None -> return (build_endpoint node_addr node_port tls))
  in
  (* give a kind warning when any of -A -P -S exists *)
  (let got = function Some _ -> true | None -> false in
   let gotany =
     got node_addr || got node_port || got tls || got cfg.node_addr
     || got cfg.node_port || got cfg.tls
   in
   if gotany then (
     Format.(
       eprintf
         "@{<warning>Warning:@}  the --addr --port --tls options are now \
          deprecated; use --endpoint instead\n" ;
       pp_print_flush err_formatter ()))) ;
  let* () = light_mode_checks client_mode endpoint sources in
  let* remote_signer_env =
    Tezos_signer_backends_unix.Remote.read_base_uri_from_env ()
  in
  let remote_signer =
    Option.either remote_signer
    @@ Option.either remote_signer_env cfg.remote_signer
  in
  let confirmations = Option.value ~default:cfg.confirmations confirmations in
  (* --password-filename has precedence over --config-file's
     "password-filename" json field *)
  let password_filename =
    Option.either password_filename cfg.password_filename
  in
  let media_type = Option.either media_type cfg.media_type in
  let cfg =
    {
      cfg with
      node_addr = None;
      node_port = None;
      tls = None;
      media_type;
      endpoint = Some endpoint;
      remote_signer;
      confirmations;
      password_filename;
    }
  in
  if Sys.file_exists base_dir && not (Sys.is_directory base_dir) then (
    Format.eprintf "%s is not a directory.@." base_dir ;
    exit 1) ;
  if Sys.file_exists config_dir && not (Sys.is_directory config_dir) then (
    Format.eprintf "%s is not a directory.@." config_dir ;
    exit 1) ;
  let* () =
    unless (client_mode = `Mode_mockup) (fun () ->
        let*! () = Lwt_utils_unix.create_dir config_dir in
        return_unit)
  in
  let parsed_args =
    {
      chain;
      block;
      confirmations;
      sources;
      print_timings = timings;
      log_requests;
      better_errors;
      password_filename;
      protocol;
      client_mode;
    }
  in
  return
    ( {
        default_parsed_config_args with
        parsed_config_file = Some cfg;
        parsed_args = Some parsed_args;
        config_commands =
          commands config_file cfg client_mode parsed_args.protocol base_dir;
      },
      remaining )

type t =
  string option
  * string option
  * bool
  * Shell_services.chain
  * Shell_services.block
  * int option option
  * Protocol_hash.t option option
  * bool
  * bool
  * string option
  * int option
  * bool
  * Media_type.Command_line.t option
  * Uri.t option
  * Tezos_proxy.Light.sources_config option
  * Uri.t option
  * string option
  * client_mode

module type Remote_params = sig
  val authenticate :
    Signature.public_key_hash list -> Bytes.t -> Signature.t tzresult Lwt.t

  val logger : Tezos_rpc_http_client_unix.RPC_client_unix.logger
end

let other_registrations : (_ -> (module Remote_params) -> _) option =
  Some
    (fun parsed_config_file (module Remote_params) ->
      parsed_config_file.Cfg_file.remote_signer
      |> Option.iter (fun signer ->
             Client_keys.register_signer
               (module Tezos_signer_backends_unix.Remote.Make
                         (Tezos_rpc_http_client_unix.RPC_client_unix)
                         (struct
                           let default = signer

                           include Remote_params
                         end))))

let clic_commands ~base_dir:_ ~config_commands ~builtin_commands ~other_commands
    ~require_auth:_ =
  config_commands @ builtin_commands @ other_commands

let logger = None
back to top