client_main_run.ml
(*****************************************************************************)
(* *)
(* Open Source License *)
(* Copyright (c) 2018 Dynamic Ledger Solutions, Inc. <contact@tezos.com> *)
(* Copyright (c) 2018 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 - Main Program *)
open Client_context_unix
let builtin_commands =
let open Lwt_syntax in
let open Tezos_clic in
[
command
~desc:"List the protocol versions that this client understands."
no_options
(fixed ["list"; "understood"; "protocols"])
(fun () (cctxt : #Client_context.full) ->
let* () =
Seq.S.iter
(fun (ver, _) -> cctxt#message "%a" Protocol_hash.pp_short ver)
(Client_commands.get_versions ())
in
return_ok_unit);
]
module type M = sig
type t
val global_options :
unit -> (t, Client_context_unix.unix_full) Tezos_clic.options
val parse_config_args :
#Tezos_client_base.Client_context.full ->
string list ->
(Client_config.parsed_config_args * string list) tzresult Lwt.t
val default_chain : Chain_services.chain
val default_block : [> `Head of int]
val default_base_dir : string
val default_daily_logs_path : string option
val default_media_type : Media_type.Command_line.t
val other_registrations :
(Client_config.Cfg_file.t -> (module Client_config.Remote_params) -> unit)
option
val clic_commands :
base_dir:string ->
config_commands:
Tezos_client_base.Client_context.full Tezos_clic.command list ->
builtin_commands:
Tezos_client_base.Client_context.full Tezos_clic.command list ->
other_commands:Tezos_client_base.Client_context.full Tezos_clic.command list ->
require_auth:bool ->
Tezos_client_base.Client_context.full Tezos_clic.command list
val logger : RPC_client_unix.logger option
end
let setup_remote_signer (module C : M) client_config
(rpc_config : RPC_client_unix.config) parsed_config_file =
let module Remote_params = struct
let authenticate pkhs payload =
let open Lwt_result_syntax in
let* keys = Client_keys.list_keys client_config in
match
List.filter_map
(function
| _, known_pkh, _, Some known_sk_uri
when List.exists
(fun pkh ->
Tezos_crypto.Signature.Public_key_hash.equal
pkh
known_pkh)
pkhs ->
Some known_sk_uri
| _ -> None)
keys
with
| sk_uri :: _ -> Client_keys.sign client_config sk_uri payload
| [] ->
failwith
"remote signer expects authentication signature, but no authorized \
key was found in the wallet"
let logger =
(* overriding the logger we might already have with the one from
module C *)
match C.logger with Some logger -> logger | None -> rpc_config.logger
end in
let module Http =
Tezos_signer_backends.Http.Make (RPC_client_unix) (Remote_params)
in
let module Https =
Tezos_signer_backends.Https.Make (RPC_client_unix) (Remote_params)
in
let module Socket = Tezos_signer_backends_unix.Socket.Make (Remote_params) in
Client_keys.register_signer
(module Tezos_signer_backends.Encrypted.Make (struct
let cctxt = (client_config :> Client_context.io_wallet)
end)) ;
Client_keys.register_aggregate_signer
(module Tezos_signer_backends.Encrypted.Make_aggregate (struct
let cctxt = (client_config :> Client_context.io_wallet)
end)) ;
Client_keys.register_signer (module Tezos_signer_backends.Unencrypted) ;
Client_keys.register_aggregate_signer
(module Tezos_signer_backends.Unencrypted.Aggregate) ;
Client_keys.register_signer
(module Tezos_signer_backends_unix.Ledger.Signer_implementation) ;
Client_keys.register_signer (module Socket.Unix) ;
Client_keys.register_signer (module Socket.Tcp) ;
Client_keys.register_signer (module Http) ;
Client_keys.register_signer (module Https) ;
match parsed_config_file with
| None -> ()
| Some parsed_config_file -> (
match C.other_registrations with
| Some r -> r parsed_config_file (module Remote_params)
| None -> ())
(** Warn the user if there are duplicate URIs in the sources (may or may
not be a misconfiguration). *)
let warn_if_duplicates_light_sources (printer : unix_logger) uris =
let module UriMap = Map.Make (Uri) in
let uri_duplicates =
List.fold_left
(fun map uri ->
UriMap.update
uri
(fun nb -> Option.fold ~none:1 ~some:succ nb |> Option.some)
map)
UriMap.empty
uris
|> UriMap.filter (fun _uri nb -> nb > 1)
in
if not (UriMap.is_empty uri_duplicates) then
printer#warning
"The following URIs are duplicated in the light mode --sources argument \
(this will increase their weight in the consensus check). If this is \
not intentional, please fix your configuration: [%a]"
Format.(
pp_print_list
~pp_sep:(fun ppf () -> pp_print_string ppf "; ")
(fun ppf (uri, nb) -> fprintf ppf "%a (%i occurrences)" Uri.pp uri nb))
(UriMap.bindings uri_duplicates)
else Lwt.return_unit
let setup_default_proxy_client_config parsed_args base_dir rpc_config mode =
let open Lwt_result_syntax in
(* Make sure that base_dir is not a mockup. *)
let* () =
let* b = Tezos_mockup.Persistence.classify_base_dir base_dir in
match b with
| Base_dir_is_mockup ->
failwith
"%s is setup as a mockup, yet mockup mode is not active"
base_dir
| _ -> return_unit
in
let chain, block, confirmations, password_filename, protocol, sources =
match parsed_args with
| None ->
( Client_config.default_chain,
Client_config.default_block,
None,
None,
None,
None )
| Some p ->
( p.Client_config.chain,
p.Client_config.block,
p.Client_config.confirmations,
p.Client_config.password_filename,
p.Client_config.protocol,
p.Client_config.sources )
in
let verbose_rpc_error_diagnostics =
match parsed_args with
| None -> false
| Some parsed_args -> parsed_args.better_errors
in
match mode with
| `Mode_client ->
return
@@ new unix_full
~chain
~block
~confirmations
~password_filename
~base_dir
~rpc_config
~verbose_rpc_error_diagnostics
| (`Mode_light | `Mode_proxy) as mode ->
let printer = new unix_logger ~base_dir in
let get_mode () =
match (mode, sources) with
| `Mode_proxy, _ -> return Tezos_proxy.Proxy_services.Proxy_client
| `Mode_light, None ->
failwith
"--sources MUST be specified when --mode light is specified"
| `Mode_light, Some sources_config ->
let*! () =
warn_if_duplicates_light_sources printer sources_config.uris
in
let rpc_builder endpoint =
(new Tezos_rpc_http_client_unix.RPC_client_unix.http_ctxt
{rpc_config with endpoint}
(Media_type.Command_line.of_command_line rpc_config.media_type)
:> Tezos_rpc.Context.simple)
in
let sources =
Tezos_proxy.Light.sources_config_to_sources
rpc_builder
sources_config
in
return (Tezos_proxy.Proxy_services.Light_client sources)
in
let* mode = get_mode () in
return
@@ new unix_proxy
?protocol
~chain
~block
~confirmations
~password_filename
~base_dir
~rpc_config
~mode
()
let setup_mockup_rpc_client_config
(cctxt : Tezos_client_base.Client_context.printer)
(args : Client_config.cli_args) base_dir =
let open Lwt_result_syntax in
let in_memory_mockup (args : Client_config.cli_args) =
match args.protocol with
| None -> Tezos_mockup.Persistence.default_mockup_context cctxt
| Some protocol_hash ->
Tezos_mockup.Persistence.init_mockup_context_by_protocol_hash
~cctxt
~protocol_hash
~constants_overrides_json:None
~bootstrap_accounts_json:None
in
let* b = Tezos_mockup.Persistence.classify_base_dir base_dir in
let* (mockup_env, {chain = chain_id; rpc_context; protocol_data}), mem_only =
match b with
| Tezos_mockup.Persistence.Base_dir_is_empty
| Tezos_mockup.Persistence.Base_dir_is_file
| Tezos_mockup.Persistence.Base_dir_is_nonempty
| Tezos_mockup.Persistence.Base_dir_does_not_exist ->
let mem_only = true in
let* res = in_memory_mockup args in
return (res, mem_only)
| Tezos_mockup.Persistence.Base_dir_is_mockup ->
let mem_only = false in
let* res =
Tezos_mockup.Persistence.get_mockup_context_from_disk
~base_dir
~protocol_hash:args.protocol
cctxt
in
return (res, mem_only)
in
return
(new unix_mockup
~base_dir
~mem_only
~mockup_env
~chain_id
~rpc_context
~protocol_data)
let setup_client_config (cctxt : Tezos_client_base.Client_context.printer)
(parsed_args : Client_config.cli_args option) base_dir rpc_config =
let setup_non_mockup_rpc_client_config =
setup_default_proxy_client_config parsed_args base_dir rpc_config
in
match parsed_args with
| None -> setup_non_mockup_rpc_client_config `Mode_client
| Some args -> (
match args.Client_config.client_mode with
| (`Mode_client | `Mode_light | `Mode_proxy) as m ->
setup_non_mockup_rpc_client_config m
| `Mode_mockup -> setup_mockup_rpc_client_config cctxt args base_dir)
(* FIXME: https://gitlab.com/tezos/tezos/-/issues/4025
Remove backwards compatible Tezos symlinks. *)
let warn_if_argv0_name_not_octez () =
let executable_name =
(* example: tezos-tx-rollup-client-015-PtKathma *)
Filename.basename Sys.argv.(0)
in
let old_head = "tezos-" in
let new_head = "octez-" in
match TzString.has_prefix executable_name ~prefix:old_head with
| false -> ()
| true ->
let expected_name =
let len_prefix = String.length old_head in
let headless_name =
(* example: tx-rollup-client-015-PtKathma *)
String.sub
executable_name
len_prefix
(String.length executable_name - len_prefix)
in
let name_without_version name =
match headless_name |> String.starts_with ~prefix:name with
| false -> None
| true ->
let len_name = String.length name in
let version_proto =
(* example: -015-PtKathma *)
String.sub
headless_name
len_name
(String.length headless_name - len_name)
in
let num_hyphens =
(* example: 2 *)
version_proto
|> String.fold_left
(fun acc c -> match c with '-' -> acc + 1 | _ -> acc)
0
in
let proto =
(* example: -PtKathma *)
if num_hyphens = 2 then
String.sub version_proto 4 (String.length version_proto - 4)
else version_proto
in
Some (name ^ proto)
in
(* example: tx-rollup-client-PtKathma *)
let versionless_name =
["accuser"; "baker"; "tx-rollup-client"; "tx-rollup-node"]
|> List.map name_without_version
|> List.find Option.is_some |> Option.join
in
(* example: octez-tx-rollup-client-PtKathma *)
new_head
^
match versionless_name with
| None -> headless_name
| Some versionless_name -> versionless_name
in
Format.eprintf
"@[<v 2>@{<warning>@{<title>Warning@}@}@,\
The executable with name @{<kwd>%s@} has been renamed to @{<kwd>%s@}. \
The name @{<kwd>%s@} is now@,\
deprecated, and it will be removed in a future release. Please update@,\
your scripts to use the new name.@]@\n\
@."
executable_name
expected_name
executable_name
(* Main (lwt) entry *)
let main (module C : M) ~select_commands =
let open Lwt_result_syntax in
let global_options = C.global_options () in
let executable_name = Filename.basename Sys.executable_name in
let original_args, autocomplete =
(* for shell aliases *)
let rec move_autocomplete_token_upfront acc = function
| "bash_autocomplete" :: prev_arg :: cur_arg :: script :: args ->
let args = List.rev acc @ args in
(args, Some (prev_arg, cur_arg, script))
| x :: rest -> move_autocomplete_token_upfront (x :: acc) rest
| [] -> (List.rev acc, None)
in
match Array.to_list Sys.argv with
| _ :: args -> move_autocomplete_token_upfront [] args
| [] -> ([], None)
in
Random.self_init () ;
ignore
Tezos_clic.(
setup_formatter
Format.std_formatter
(if Unix.isatty Unix.stdout then Ansi else Plain)
Short) ;
ignore
Tezos_clic.(
setup_formatter
Format.err_formatter
(if Unix.isatty Unix.stderr then Ansi else Plain)
Short) ;
warn_if_argv0_name_not_octez () ;
let*! retcode =
Lwt.catch
(fun () ->
let full =
new unix_full
~chain:C.default_chain
~block:C.default_block
~confirmations:None
~password_filename:None
~base_dir:C.default_base_dir
~rpc_config:RPC_client_unix.default_config
~verbose_rpc_error_diagnostics:false
in
let*! r =
let* parsed, remaining = C.parse_config_args full original_args in
let parsed_config_file = parsed.Client_config.parsed_config_file in
let parsed_args = parsed.Client_config.parsed_args in
let config_commands = parsed.Client_config.config_commands in
let base_dir : string =
match parsed.Client_config.base_dir with
| Some p -> p
| None -> (
match parsed_config_file with
| None -> C.default_base_dir
| Some p -> p.Client_config.Cfg_file.base_dir)
in
let daily_logs_path =
C.default_daily_logs_path
|> Option.map
Filename.Infix.(fun logdir -> base_dir // "logs" // logdir)
in
let require_auth = parsed.Client_config.require_auth in
let*! () =
let open Tezos_base_unix.Internal_event_unix in
let config =
make_with_defaults
?enable_default_daily_logs_at:daily_logs_path
()
in
match parsed_config_file with
| None -> init ~config ()
| Some cf -> (
match cf.Client_config.Cfg_file.internal_events with
| None -> init ~config ()
| Some config -> init ~config ())
in
let rpc_config =
let rpc_config : RPC_client_unix.config =
match parsed_config_file with
| None -> RPC_client_unix.default_config
| Some cfg ->
{
RPC_client_unix.default_config with
media_type =
Option.value cfg.media_type ~default:C.default_media_type;
endpoint =
Option.value
cfg.endpoint
~default:Client_config.default_endpoint;
}
in
match parsed_args with
| Some parsed_args ->
if parsed_args.Client_config.print_timings then
let gettimeofday = Unix.gettimeofday in
{
rpc_config with
logger =
RPC_client_unix.timings_logger
~gettimeofday
Format.err_formatter;
}
else if parsed_args.Client_config.log_requests then
{
rpc_config with
logger = RPC_client_unix.full_logger Format.err_formatter;
}
else rpc_config
| None -> rpc_config
in
let* client_config =
setup_client_config
(full :> Client_context.printer)
parsed_args
base_dir
rpc_config
in
setup_remote_signer
(module C)
client_config
rpc_config
parsed_config_file ;
let* other_commands =
match parsed_args with
| Some parsed_args ->
select_commands
(client_config :> RPC_client_unix.http_ctxt)
parsed_args
| None -> return_nil
in
let commands =
Tezos_clic.add_manual
~executable_name
~global_options
(if Unix.isatty Unix.stdout then Tezos_clic.Ansi
else Tezos_clic.Plain)
Format.std_formatter
(C.clic_commands
~base_dir
~config_commands
~builtin_commands
~other_commands
~require_auth)
in
match autocomplete with
| Some (prev_arg, cur_arg, script) ->
let* completions =
Tezos_clic.autocompletion
~script
~cur_arg
~prev_arg
~args:original_args
~global_options
commands
client_config
in
List.iter print_endline completions ;
return_unit
| None -> Tezos_clic.dispatch commands client_config remaining
in
match r with
| Ok () -> Lwt.return 0
| Error [Tezos_clic.Version] ->
let version = Tezos_version_value.Bin_version.version_string in
Format.printf "%s\n" version ;
Lwt.return 0
| Error [Tezos_clic.Help command] ->
Tezos_clic.usage
Format.std_formatter
~executable_name
~global_options
(match command with None -> [] | Some c -> [c]) ;
Lwt.return 0
| Error errs ->
Tezos_clic.pp_cli_errors
Format.err_formatter
~executable_name
~global_options
~default:Error_monad.pp
errs ;
Lwt.return 1)
(function
| Client_commands.Version_not_found ->
Format.eprintf
"@{<error>@{<title>Fatal error@}@} unknown protocol version.@." ;
Lwt.return 1
| Failure message ->
Format.eprintf
"@{<error>@{<title>Fatal error@}@}@. @[<h 0>%a@]@."
Format.pp_print_text
message ;
Lwt.return 1
| exn ->
Format.printf
"@{<error>@{<title>Fatal error@}@}@. @[<h 0>%a@]@."
Format.pp_print_text
(Printexc.to_string exn) ;
Lwt.return 1)
in
Format.pp_print_flush Format.err_formatter () ;
Format.pp_print_flush Format.std_formatter () ;
let*! () = Tezos_base_unix.Internal_event_unix.close () in
Lwt.return retcode
(* Where all the user friendliness starts *)
let run (module M : M)
~(select_commands :
RPC_client_unix.http_ctxt ->
Client_config.cli_args ->
Client_context.full Tezos_clic.command list tzresult Lwt.t) =
Lwt.Exception_filter.(set handle_all_except_runtime) ;
Stdlib.exit @@ Lwt_main.run @@ Lwt_exit.wrap_and_forward
@@ main (module M) ~select_commands