Revision 5fa6faa707c4eedc955a4f0562195703224a63fa authored by Thomas Letan on 14 August 2023, 09:49:56 UTC, committed by Thomas Letan on 14 August 2023, 09:55:15 UTC
Tztop was added at a time where the protocol couldn’t be loaded in utop.
This is no longer the case, and as a consequence, we can safely retire
tztop.
1 parent 5745a2e
Raw File
client_keys_commands.ml
(*****************************************************************************)
(*                                                                           *)
(* Open Source License                                                       *)
(* Copyright (c) 2018 Dynamic Ledger Solutions, Inc. <contact@tezos.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.                                                 *)
(*                                                                           *)
(*****************************************************************************)

open Client_keys

let group =
  {
    Tezos_clic.name = "keys";
    title = "Commands for managing the wallet of cryptographic keys";
  }

let algo_param () =
  let open Lwt_result_syntax in
  Tezos_clic.parameter
    ~autocomplete:(fun _ -> return ["ed25519"; "secp256k1"; "p256"; "bls"])
    (fun _ name ->
      match name with
      | "ed25519" -> return Signature.Ed25519
      | "secp256k1" -> return Signature.Secp256k1
      | "p256" -> return Signature.P256
      | "bls" -> return Signature.Bls
      | name ->
          failwith
            "Unknown signature algorithm (%s). Available: 'ed25519', \
             'secp256k1','p256' or 'bls'"
            name)

let sig_algo_arg =
  Tezos_clic.default_arg
    ~doc:"use custom signature algorithm"
    ~long:"sig"
    ~short:'s'
    ~placeholder:"ed25519|secp256k1|p256|bls"
    ~default:"ed25519"
    (algo_param ())

let gen_keys_containing ?(encrypted = false) ?(prefix = false)
    ?(ignore_case = false) ?(force = false) ~containing ~name
    (cctxt : #Client_context.io_wallet) =
  let open Lwt_result_syntax in
  let unrepresentable =
    List.filter
      (fun s ->
        not
        @@ Tezos_crypto.Base58.Alphabet.all_in_alphabet
             ~ignore_case
             Tezos_crypto.Base58.Alphabet.bitcoin
             s)
      containing
  in
  let good_initial_char = "KLMNPQRSTUVWXYZabcdefghi" in
  let bad_initial_char =
    if ignore_case then "123456789Jj" else "123456789ABCDEFGHJjkmnopqrstuvwxyz"
  in
  let containing =
    if ignore_case then List.map String.lowercase_ascii containing
    else containing
  in
  match unrepresentable with
  | _ :: _ ->
      cctxt#error
        "@[<v 0>The following words can't be written in the key alphabet: %a.@,\
         Valid characters: %a@,\
         Extra restriction for the first character: %s@]"
        (Format.pp_print_list
           ~pp_sep:(fun ppf () -> Format.fprintf ppf ", ")
           (fun ppf s -> Format.fprintf ppf "'%s'" s))
        unrepresentable
        Tezos_crypto.Base58.Alphabet.pp
        Tezos_crypto.Base58.Alphabet.bitcoin
        good_initial_char
  | [] -> (
      let unrepresentable =
        List.filter
          (fun s -> prefix && String.contains bad_initial_char s.[0])
          containing
      in
      match unrepresentable with
      | _ :: _ ->
          cctxt#error
            "@[<v 0>The following words don't respect the first character \
             restriction: %a.@,\
             Valid characters: %a@,\
             Extra restriction for the first character: %s@]"
            (Format.pp_print_list
               ~pp_sep:(fun ppf () -> Format.fprintf ppf ", ")
               (fun ppf s -> Format.fprintf ppf "'%s'" s))
            unrepresentable
            Tezos_crypto.Base58.Alphabet.pp
            Tezos_crypto.Base58.Alphabet.bitcoin
            good_initial_char
      | [] ->
          let* name_exists = Public_key_hash.mem cctxt name in
          if name_exists && not force then
            let*! () =
              cctxt#warning
                "Key for name '%s' already exists. Use --force to update."
                name
            in
            return_unit
          else
            let*! () =
              cctxt#warning
                "This process uses a brute force search and may take a long \
                 time to find a key."
            in
            let adjust_case =
              if ignore_case then String.lowercase_ascii else Fun.id
            in
            let matches =
              if prefix then
                let containing_tz1 = List.map (( ^ ) "tz1") containing in
                fun key ->
                  List.exists
                    (fun containing ->
                      String.sub (adjust_case key) 0 (String.length containing)
                      = containing)
                    containing_tz1
              else
                let re = Re.Str.regexp (String.concat "\\|" containing) in
                fun key ->
                  try
                    ignore (Re.Str.search_forward re (adjust_case key) 0) ;
                    true
                  with Not_found -> false
            in
            let rec loop attempts =
              let public_key_hash, public_key, secret_key =
                Signature.generate_key ()
              in
              let hash =
                Signature.Public_key_hash.to_b58check
                @@ Signature.Public_key.hash public_key
              in
              if matches hash then
                let*? pk_uri =
                  Tezos_signer_backends.Unencrypted.make_pk public_key
                in
                let* sk_uri =
                  if encrypted then
                    Tezos_signer_backends.Encrypted.prompt_twice_and_encrypt
                      cctxt
                      secret_key
                  else
                    Lwt.return
                      (Tezos_signer_backends.Unencrypted.make_sk secret_key)
                in
                let* () =
                  register_key
                    cctxt
                    ~force
                    (public_key_hash, pk_uri, sk_uri)
                    ~public_key
                    name
                in
                return hash
              else
                let*! () =
                  if attempts mod 25_000 = 0 then
                    cctxt#message
                      "Tried %d keys without finding a match"
                      attempts
                  else Lwt.return_unit
                in
                let*! () = Lwt.pause () in
                loop (attempts + 1)
            in
            let* key_hash = loop 1 in
            let*! () =
              cctxt#message "Generated '%s' under the name '%s'." key_hash name
            in
            return_unit)

let rec input_fundraiser_params (cctxt : #Client_context.io_wallet) =
  let open Lwt_result_syntax in
  let rec get_boolean_answer (cctxt : #Client_context.io_wallet) ~default ~msg =
    let prompt = if default then "(Y/n/q)" else "(y/N/q)" in
    let* gen = cctxt#prompt "%s %s: " msg prompt in
    match (default, String.lowercase_ascii gen) with
    | default, "" -> return default
    | _, "y" -> return_true
    | _, "n" -> return_false
    | _, "q" -> failwith "Exit by user request."
    | _ -> get_boolean_answer cctxt ~msg ~default
  in
  let* email = cctxt#prompt "Enter the e-mail used for the paper wallet: " in
  let rec loop_words acc i =
    if i > 14 then return (List.rev acc)
    else
      let* word = cctxt#prompt_password "Enter word %d: " i in
      match Bip39.index_of_word (Bytes.to_string word) with
      | None -> loop_words acc i
      | Some wordidx -> loop_words (wordidx :: acc) (succ i)
  in
  let* words = loop_words [] 0 in
  match Bip39.of_indices words with
  | None -> assert false
  | Some t -> (
      let* password =
        cctxt#prompt_password "Enter the password used for the paper wallet: "
      in
      (* TODO: unicode normalization (NFKD)... *)
      let passphrase = Bytes.(cat (of_string email) password) in
      let sk = Bip39.to_seed ~passphrase t in
      let sk = Bytes.sub sk 0 32 in
      let sk : Signature.Secret_key.t =
        Ed25519
          (Data_encoding.Binary.of_bytes_exn
             Signature.Ed25519.Secret_key.encoding
             sk)
      in
      let pk = Signature.Secret_key.to_public_key sk in
      let pkh = Signature.Public_key.hash pk in
      let msg =
        Format.asprintf
          "Your public Tezos address is %a is that correct?"
          Signature.Public_key_hash.pp
          pkh
      in
      let* b = get_boolean_answer cctxt ~msg ~default:true in
      match b with true -> return sk | false -> input_fundraiser_params cctxt)

let fail_if_already_registered cctxt force pk_uri name =
  let open Lwt_result_syntax in
  let* o = Public_key.find_opt cctxt name in
  match o with
  | None -> return_unit
  | Some (pk_uri_found, _) ->
      fail_unless
        (pk_uri = pk_uri_found || force)
        (error_of_fmt
           "public and secret keys '%s' don't correspond, please don't use \
            --force"
           name)

let keys_count_param =
  let open Tezos_clic in
  param
    ~name:"keys_count"
    ~desc:"How many keys to generate"
    (parameter (fun _ s ->
         let open Lwt_result_syntax in
         match int_of_string_opt s with
         | None -> failwith "number of keys must be an integer"
         | Some x ->
             if x < 0 then failwith "number of keys must be positive"
             else return x))

(** The kind of info that the [generate_test_keys] command outputs. *)
type source = {
  pkh : Signature.public_key_hash;
  pk : Signature.public_key;
  sk : Signature.secret_key;
}

let source_encoding =
  let open Data_encoding in
  conv
    (fun {pkh; pk; sk} -> (pkh, pk, sk))
    (fun (pkh, pk, sk) -> {pkh; pk; sk})
    (obj3
       (req "pkh" Signature.Public_key_hash.encoding)
       (req "pk" Signature.Public_key.encoding)
       (req "sk" Signature.Secret_key.encoding))

let source_list_encoding = Data_encoding.list source_encoding

(* Simple helpers used to manage wallet files a raw way. *)
module Wallet_helpers = struct
  let write_file path str =
    let open Lwt_syntax in
    let* fd = Lwt_unix.openfile path Unix.[O_CREAT; O_TRUNC; O_RDWR] 0o644 in
    let* _written_bytes =
      Lwt.catch
        (fun () -> Lwt_unix.write_string fd str 0 (String.length str))
        (fun exn ->
          let* () = Lwt_unix.close fd in
          Lwt.fail exn)
    in
    Lwt_unix.close fd

  module Aliases = struct
    let encoding = list (obj1 (req "alias" Data_encoding.string))

    let name = "aliases"
  end
end

(** Generate an array of accounts for testing purposes, store them
    into a wallet and output them to stdout in the JSON
    format.

    It is essential that this command lives here and not in the
    protocol-specific code because it should be available before a
    protocol is activated. *)
let generate_test_keys =
  let open Tezos_clic in
  let alias_prefix_param =
    arg
      ~long:"alias-prefix"
      ~placeholder:"PREFIX"
      ~doc:
        "use a custom alias prefix (default: bootstrap). Keys will be \
         generated with alias \"PREFIX<ID>\" where ID is unique for all key"
      (parameter (fun _ s -> Lwt_result_syntax.return s))
  in
  command
    ~group
    ~desc:"Generate an array of accounts for testing purposes."
    (args1 alias_prefix_param)
    (prefixes ["stresstest"; "gen"; "keys"] @@ keys_count_param @@ stop)
    (fun alias_prefix n (cctxt : Client_context.full) ->
      let open Lwt_result_syntax in
      (* By default, the alias prefix matches the bootstrap<idx>
         pattern used in sandboxed mode.*)
      let alias_prefix =
        match alias_prefix with
        | None -> fun i -> Format.sprintf "bootstrap%d" (i + 6)
        | Some alias_prefix -> Format.sprintf "%s%06d" alias_prefix
      in
      let* source_list =
        List.init_es ~when_negative_length:[] n (fun i ->
            let alias = alias_prefix i in
            let pkh, pk, sk =
              Signature.generate_key ~algo:Signature.Ed25519 ()
            in
            let*? pk_uri = Tezos_signer_backends.Unencrypted.make_pk pk in
            let*? sk_uri = Tezos_signer_backends.Unencrypted.make_sk sk in
            return ({pkh; pk; sk}, pk_uri, sk_uri, alias))
      in
      (* All keys are registered into a single wallet. *)
      let* () =
        register_keys
          cctxt
          (List.rev_map
             (fun (x, pk_uri, sk_uri, alias) ->
               (alias, x.pkh, x.pk, pk_uri, sk_uri))
             source_list)
      in
      (* Extract and write wallet aliases *)
      let aliases = List.rev_map (fun (_, _, _, alias) -> alias) source_list in
      let wallet_path = cctxt#get_base_dir in
      let*! () =
        Wallet_helpers.(
          write_file
            (Filename.concat wallet_path Aliases.name)
            Data_encoding.Json.(to_string (construct Aliases.encoding aliases)))
      in
      let json =
        Data_encoding.Json.construct
          source_list_encoding
          (List.map (fun (x, _, _, _) -> x) source_list)
      in
      let*! () = cctxt#message "%a@." Data_encoding.Json.pp json in
      return_unit)

let aggregate_fail_if_already_registered cctxt force pk_uri name =
  let open Lwt_result_syntax in
  let* pk_opt = Aggregate_alias.Public_key.find_opt cctxt name in
  match pk_opt with
  | None -> return_unit
  | Some (pk_uri_found, _) ->
      fail_unless
        (pk_uri = pk_uri_found || force)
        (error_of_fmt
           "public and secret keys '%s' don't correspond, please don't use \
            --force"
           name)

module Bls_commands = struct
  open Lwt_result_syntax

  let generate_keys ~force ~encrypted name (cctxt : #Client_context.io_wallet) =
    let* name = Aggregate_alias.Secret_key.of_fresh cctxt force name in
    let mnemonic = Mnemonic.new_random in
    let*! () =
      cctxt#message
        "It is important to save this mnemonic in a secure place:@\n\
         @\n\
         %a@\n\
         @\n\
         The mnemonic can be used to recover your spending key.@."
        Mnemonic.words_pp
        (Bip39.to_words mnemonic)
    in
    let seed = Mnemonic.to_32_bytes mnemonic in
    let pkh, pk, sk = Tezos_crypto.Aggregate_signature.generate_key ~seed () in
    let*? pk_uri = Tezos_signer_backends.Unencrypted.Aggregate.make_pk pk in
    let* sk_uri =
      if encrypted then
        Tezos_signer_backends.Encrypted.prompt_twice_and_encrypt_aggregate
          cctxt
          sk
      else Tezos_signer_backends.Unencrypted.Aggregate.make_sk sk |> Lwt.return
    in
    register_aggregate_key
      cctxt
      ~force
      (pkh, pk_uri, sk_uri)
      ~public_key:pk
      name

  let list_keys (cctxt : #Client_context.io_wallet) =
    let* aggregate_keys_list = list_aggregate_keys cctxt in
    List.iter_es
      (fun (name, pkh, pk, sk) ->
        let* pkh_str = Aggregate_alias.Public_key_hash.to_source pkh in
        let*! () =
          match (pk, sk) with
          | None, None -> cctxt#message "%s: %s" name pkh_str
          | _, Some uri ->
              let scheme =
                Option.value ~default:"aggregate_unencrypted"
                @@ Uri.scheme (uri : aggregate_sk_uri :> Uri.t)
              in
              cctxt#message "%s: %s (%s sk known)" name pkh_str scheme
          | Some _, _ -> cctxt#message "%s: %s (pk known)" name pkh_str
        in
        return_unit)
      aggregate_keys_list

  let show_address ~show_private name (cctxt : #Client_context.io_wallet) =
    let* keys_opt = alias_aggregate_keys cctxt name in
    match keys_opt with
    | None ->
        let*! () = cctxt#error "No keys found for address" in
        return_unit
    | Some (pkh, pk, skloc) -> (
        let*! () =
          cctxt#message
            "Hash: %a"
            Tezos_crypto.Aggregate_signature.Public_key_hash.pp
            pkh
        in
        match pk with
        | None -> return_unit
        | Some pk ->
            let*! () =
              cctxt#message
                "Public Key: %a"
                Tezos_crypto.Aggregate_signature.Public_key.pp
                pk
            in
            if show_private then
              Option.iter_es
                (fun skloc ->
                  let* skloc = Aggregate_alias.Secret_key.to_source skloc in
                  let*! () = cctxt#message "Secret Key: %s" skloc in
                  return_unit)
                skloc
            else return_unit)

  let import_secret_key ~force name sk_uri (cctxt : #Client_context.io_wallet) =
    let* name = Aggregate_alias.Secret_key.of_fresh cctxt false name in
    let* pk_uri = aggregate_neuterize sk_uri in
    let* () = aggregate_fail_if_already_registered cctxt force pk_uri name in
    let* pkh, public_key =
      import_aggregate_secret_key ~io:(cctxt :> Client_context.io_wallet) pk_uri
    in
    let*! () =
      cctxt#message
        "Bls address added: %a"
        Tezos_crypto.Aggregate_signature.Public_key_hash.pp
        pkh
    in
    register_aggregate_key cctxt (pkh, pk_uri, sk_uri) ?public_key name
end

let commands network : Client_context.full Tezos_clic.command list =
  let open Lwt_result_syntax in
  let open Tezos_clic in
  let encrypted_switch () =
    if
      List.exists
        (fun (scheme, _) -> scheme = Tezos_signer_backends.Unencrypted.scheme)
        (Client_keys.registered_signers ())
    then Tezos_clic.switch ~long:"encrypted" ~doc:"Encrypt the key on-disk" ()
    else Tezos_clic.constant true
  in
  let show_private_switch =
    switch ~long:"show-secret" ~short:'S' ~doc:"show the private key" ()
  in
  [
    generate_test_keys;
    command
      ~group
      ~desc:
        "List supported signing schemes.\n\
         Signing schemes are identifiers for signer modules: the built-in \
         signing routines, a hardware wallet, an external agent, etc.\n\
         Each signer has its own format for describing secret keys, such a raw \
         secret key for the default `unencrypted` scheme, the path on a \
         hardware security module, an alias for an external agent, etc.\n\
         This command gives the list of signer modules that this version of \
         the tezos client supports."
      no_options
      (fixed ["list"; "signing"; "schemes"])
      (fun () (cctxt : Client_context.full) ->
        let signers =
          List.sort
            (fun (ka, _) (kb, _) -> String.compare ka kb)
            (registered_signers ())
        in
        let*! () =
          List.iter_s
            (fun (n, signer) ->
              match signer with
              | Simple (module S : SIGNER) ->
                  cctxt#message
                    "@[<v 2>Scheme `%s`: %s@,@[<hov 0>%a@]@]"
                    n
                    S.title
                    Format.pp_print_text
                    S.description
              | Aggregate (module S : AGGREGATE_SIGNER) ->
                  cctxt#message
                    "@[<v 2>Aggregate scheme `%s`: %s@,@[<hov 0>%a@]@]"
                    n
                    S.title
                    Format.pp_print_text
                    S.description)
            signers
        in
        return_unit);
    (match network with
    | Some `Mainnet ->
        command
          ~group
          ~desc:"Generate a pair of keys."
          (args2 (Secret_key.force_switch ()) sig_algo_arg)
          (prefixes ["gen"; "keys"] @@ Secret_key.fresh_alias_param @@ stop)
          (fun (force, algo) name (cctxt : Client_context.full) ->
            let* name = Secret_key.of_fresh cctxt force name in
            let pkh, pk, sk = Signature.generate_key ~algo () in
            let*? pk_uri = Tezos_signer_backends.Unencrypted.make_pk pk in
            let* sk_uri =
              Tezos_signer_backends.Encrypted.prompt_twice_and_encrypt cctxt sk
            in
            register_key cctxt ~force (pkh, pk_uri, sk_uri) name)
    | Some `Testnet | None ->
        command
          ~group
          ~desc:"Generate a pair of keys."
          (args3
             (Secret_key.force_switch ())
             sig_algo_arg
             (encrypted_switch ()))
          (prefixes ["gen"; "keys"] @@ Secret_key.fresh_alias_param @@ stop)
          (fun (force, algo, encrypted) name (cctxt : Client_context.full) ->
            let* name = Secret_key.of_fresh cctxt force name in
            let pkh, pk, sk = Signature.generate_key ~algo () in
            let*? pk_uri = Tezos_signer_backends.Unencrypted.make_pk pk in
            let* sk_uri =
              if encrypted then
                Tezos_signer_backends.Encrypted.prompt_twice_and_encrypt
                  cctxt
                  sk
              else Lwt.return (Tezos_signer_backends.Unencrypted.make_sk sk)
            in
            register_key cctxt ~force (pkh, pk_uri, sk_uri) name));
    (match network with
    | Some `Mainnet ->
        command
          ~group
          ~desc:"Generate keys including the given string."
          (args3
             (switch
                ~long:"prefix"
                ~short:'P'
                ~doc:"the key must begin with tz1[word]"
                ())
             (switch
                ~long:"ignore-case"
                ~short:'I'
                ~doc:"make the pattern case-insensitive"
                ())
             (force_switch ()))
          (prefixes ["gen"; "vanity"; "keys"]
          @@ Public_key_hash.fresh_alias_param @@ prefix "matching"
          @@ seq_of_param
          @@ string
               ~name:"words"
               ~desc:"string key must contain one of these words")
          (fun (prefix, ignore_case, force)
               name
               containing
               (cctxt : Client_context.full) ->
            let* name = Public_key_hash.of_fresh cctxt force name in
            gen_keys_containing
              ~encrypted:true
              ~force
              ~prefix
              ~ignore_case
              ~containing
              ~name
              cctxt)
    | Some `Testnet | None ->
        command
          ~group
          ~desc:"Generate keys including the given string."
          (args4
             (switch
                ~long:"prefix"
                ~short:'P'
                ~doc:"the key must begin with tz1[word]"
                ())
             (switch
                ~long:"ignore-case"
                ~short:'I'
                ~doc:"make the pattern case-insensitive"
                ())
             (force_switch ())
             (encrypted_switch ()))
          (prefixes ["gen"; "vanity"; "keys"]
          @@ Public_key_hash.fresh_alias_param @@ prefix "matching"
          @@ seq_of_param
          @@ string
               ~name:"words"
               ~desc:"string key must contain one of these words")
          (fun (prefix, ignore_case, force, encrypted)
               name
               containing
               (cctxt : Client_context.full) ->
            let* name = Public_key_hash.of_fresh cctxt force name in
            gen_keys_containing
              ~encrypted
              ~force
              ~prefix
              ~ignore_case
              ~containing
              ~name
              cctxt));
    command
      ~group
      ~desc:"Encrypt an unencrypted secret key."
      no_options
      (prefixes ["encrypt"; "secret"; "key"] @@ stop)
      (fun () (cctxt : Client_context.full) ->
        let* sk_uri = cctxt#prompt_password "Enter unencrypted secret key: " in
        let sk_uri = Uri.of_string (Bytes.to_string sk_uri) in
        let* () =
          match Uri.scheme sk_uri with
          | None | Some "unencrypted" -> return_unit
          | _ ->
              failwith
                "This command can only be used with the \"unencrypted\" scheme"
        in
        let* sk =
          Lwt.return (Signature.Secret_key.of_b58check (Uri.path sk_uri))
        in
        let* sk_uri =
          Tezos_signer_backends.Encrypted.prompt_twice_and_encrypt cctxt sk
        in
        let*! () =
          cctxt#message "Encrypted secret key %a" Uri.pp_hum (sk_uri :> Uri.t)
        in
        return_unit);
    command
      ~group
      ~desc:"Add a secret key to the wallet."
      (args1 (Secret_key.force_switch ()))
      (prefix "import"
      @@ prefixes ["secret"; "key"]
      @@ Secret_key.fresh_alias_param @@ Client_keys.sk_uri_param @@ stop)
      (fun force name sk_uri (cctxt : Client_context.full) ->
        let* name = Secret_key.of_fresh cctxt force name in
        let* pk_uri = Client_keys.neuterize sk_uri in
        let* () = fail_if_already_registered cctxt force pk_uri name in
        let* pkh, public_key =
          Client_keys.import_secret_key
            ~io:(cctxt :> Client_context.io_wallet)
            pk_uri
        in
        let*! () =
          cctxt#message
            "Tezos address added: %a"
            Signature.Public_key_hash.pp
            pkh
        in
        register_key cctxt ~force (pkh, pk_uri, sk_uri) ?public_key name);
  ]
  @ (if network <> Some `Mainnet then []
    else
      [
        command
          ~group
          ~desc:"Add a fundraiser secret key to the wallet."
          (args1 (Secret_key.force_switch ()))
          (prefix "import"
          @@ prefixes ["fundraiser"; "secret"; "key"]
          @@ Secret_key.fresh_alias_param @@ stop)
          (fun force name (cctxt : Client_context.full) ->
            let* name = Secret_key.of_fresh cctxt force name in
            let* sk = input_fundraiser_params cctxt in
            let* sk_uri =
              Tezos_signer_backends.Encrypted.prompt_twice_and_encrypt cctxt sk
            in
            let* pk_uri = Client_keys.neuterize sk_uri in
            let* () = fail_if_already_registered cctxt force pk_uri name in
            let* pkh, _public_key = Client_keys.public_key_hash pk_uri in
            register_key cctxt ~force (pkh, pk_uri, sk_uri) name);
      ])
  @ [
      command
        ~group
        ~desc:"Add a public key to the wallet."
        (args1 (Public_key.force_switch ()))
        (prefix "import"
        @@ prefixes ["public"; "key"]
        @@ Public_key.fresh_alias_param @@ Client_keys.pk_uri_param @@ stop)
        (fun force name pk_uri (cctxt : Client_context.full) ->
          let* name = Public_key.of_fresh cctxt force name in
          let* pkh, public_key = Client_keys.public_key_hash pk_uri in
          let* () = Public_key_hash.add ~force cctxt name pkh in
          let*! () =
            cctxt#message
              "Tezos address added: %a"
              Signature.Public_key_hash.pp
              pkh
          in
          Public_key.add ~force cctxt name (pk_uri, public_key));
      command
        ~group
        ~desc:"Add an address to the wallet."
        (args1 (Public_key.force_switch ()))
        (prefixes ["add"; "address"]
        @@ Public_key_hash.fresh_alias_param @@ Public_key_hash.source_param
        @@ stop)
        (fun force name hash cctxt ->
          let* name = Public_key_hash.of_fresh cctxt force name in
          Public_key_hash.add ~force cctxt name hash);
      command
        ~group
        ~desc:"List all addresses and associated keys."
        no_options
        (fixed ["list"; "known"; "addresses"])
        (fun () (cctxt : #Client_context.full) ->
          let* l = list_keys cctxt in
          List.iter_es
            (fun (name, pkh, pk, sk) ->
              let* v = Public_key_hash.to_source pkh in
              let*! () =
                match (pk, sk) with
                | None, None -> cctxt#message "%s: %s" name v
                | _, Some uri ->
                    let scheme =
                      Option.value ~default:"unencrypted"
                      @@ Uri.scheme (uri : sk_uri :> Uri.t)
                    in
                    cctxt#message "%s: %s (%s sk known)" name v scheme
                | Some _, _ -> cctxt#message "%s: %s (pk known)" name v
              in
              return_unit)
            l);
      command
        ~group
        ~desc:"Show the keys associated with an implicit account."
        (args1 show_private_switch)
        (prefixes ["show"; "address"] @@ Public_key_hash.alias_param @@ stop)
        (fun show_private (name, _) (cctxt : #Client_context.full) ->
          let* key_info = alias_keys cctxt name in
          match key_info with
          | None ->
              let*! () = cctxt#error "No keys found for address" in
              return_unit
          | Some (pkh, pk, skloc) -> (
              let*! () =
                cctxt#message "Hash: %a" Signature.Public_key_hash.pp pkh
              in
              match pk with
              | None -> return_unit
              | Some pk ->
                  let*! () =
                    cctxt#message "Public Key: %a" Signature.Public_key.pp pk
                  in
                  if show_private then
                    match skloc with
                    | None -> return_unit
                    | Some skloc ->
                        let* skloc = Secret_key.to_source skloc in
                        let*! () = cctxt#message "Secret Key: %s" skloc in
                        return_unit
                  else return_unit));
      command
        ~group
        ~desc:"Forget one address."
        (args1
           (Tezos_clic.switch
              ~long:"force"
              ~short:'f'
              ~doc:"delete associated keys when present"
              ()))
        (prefixes ["forget"; "address"] @@ Public_key_hash.alias_param @@ stop)
        (fun force (name, _pkh) (cctxt : Client_context.full) ->
          let* has_secret_key = Secret_key.mem cctxt name in
          let* has_public_key = Public_key.mem cctxt name in
          let* () =
            fail_when
              ((not force) && (has_secret_key || has_public_key))
              (error_of_fmt
                 "secret or public key present for %s, use --force to delete"
                 name)
          in
          let* () = Secret_key.del cctxt name in
          let* () = Public_key.del cctxt name in
          Public_key_hash.del cctxt name);
      command
        ~group
        ~desc:"Forget the entire wallet of keys."
        (args1
           (Tezos_clic.switch
              ~long:"force"
              ~short:'f'
              ~doc:"you got to use the force for that"
              ()))
        (fixed ["forget"; "all"; "keys"])
        (fun force (cctxt : Client_context.full) ->
          let* () =
            fail_unless
              force
              (error_of_fmt "this can only be used with option --force")
          in
          let* () = Public_key.set cctxt [] in
          let* () = Secret_key.set cctxt [] in
          Public_key_hash.set cctxt []);
      command
        ~group
        ~desc:"Compute deterministic nonce."
        no_options
        (prefixes ["generate"; "nonce"; "for"]
        @@ Public_key_hash.alias_param @@ prefixes ["from"]
        @@ string
             ~name:"data"
             ~desc:"string from which to deterministically generate the nonce"
        @@ stop)
        (fun () (name, _pkh) data (cctxt : Client_context.full) ->
          let data = Bytes.of_string data in
          let* sk_present = Secret_key.mem cctxt name in
          let* () =
            fail_unless
              sk_present
              (error_of_fmt "secret key not present for %s" name)
          in
          let* sk_uri = Secret_key.find cctxt name in
          let* nonce = Client_keys.deterministic_nonce sk_uri data in
          let*! () = cctxt#message "%a" Hex.pp (Hex.of_bytes nonce) in
          return_unit);
      command
        ~group
        ~desc:"Compute deterministic nonce hash."
        no_options
        (prefixes ["generate"; "nonce"; "hash"; "for"]
        @@ Public_key_hash.alias_param @@ prefixes ["from"]
        @@ string
             ~name:"data"
             ~desc:
               "string from which to deterministically generate the nonce hash"
        @@ stop)
        (fun () (name, _pkh) data (cctxt : Client_context.full) ->
          let data = Bytes.of_string data in
          let* sk_present = Secret_key.mem cctxt name in
          let* () =
            fail_unless
              sk_present
              (error_of_fmt "secret key not present for %s" name)
          in
          let* sk_uri = Secret_key.find cctxt name in
          let* nonce_hash = Client_keys.deterministic_nonce_hash sk_uri data in
          let*! () = cctxt#message "%a" Hex.pp (Hex.of_bytes nonce_hash) in
          return_unit);
      command
        ~group
        ~desc:
          "Import a pair of keys to the wallet from a mnemonic phrase. This \
           command uses the BIP39 algorithm, and therefore imports \
           public/secret keys that may be different from a Ledger application, \
           depending on the BIP32 derivation path used in the Ledger. This \
           command also uses the Ed25519 algorithm, which means it generates \
           tz1 public key hashes."
        (args2
           (Secret_key.force_switch ())
           (switch ~doc:"encrypt the secret key" ~long:"encrypt" ()))
        (prefix "import"
        @@ prefixes ["keys"; "from"; "mnemonic"]
        @@ Secret_key.fresh_alias_param @@ stop)
        (fun (force, encrypt) name (cctxt : Client_context.full) ->
          let* name = Secret_key.of_fresh cctxt force name in
          let* mnemonic = cctxt#prompt "Enter your mnemonic: " in
          let mnemonic = String.trim mnemonic |> String.split_on_char ' ' in
          match Bip39.of_words mnemonic with
          | None ->
              failwith
                "\"%s\" is not a valid BIP39 mnemonic. Please ensure that your \
                 mnemonic is of correct length, and that each word is \
                 separated by a single space. For reference, a correct \
                 mnemonic is comprised of 12, 15, 18, 21, or 24 words where \
                 the last is a checksum. Do not try to write your own \
                 mnemonic."
                (String.concat " " mnemonic)
          | Some t ->
              let* passphrase =
                cctxt#prompt_password "Enter your passphrase: "
              in
              let sk = Bip39.to_seed ~passphrase t in
              let sk = Bytes.sub sk 0 32 in
              let sk : Signature.Secret_key.t =
                Ed25519
                  (Data_encoding.Binary.of_bytes_exn
                     Signature.Ed25519.Secret_key.encoding
                     sk)
              in
              let*? unencrypted_sk_uri =
                Tezos_signer_backends.Unencrypted.make_sk sk
              in
              let* sk_uri =
                match encrypt with
                | true ->
                    Tezos_signer_backends.Encrypted.prompt_twice_and_encrypt
                      cctxt
                      sk
                | false -> return unencrypted_sk_uri
              in
              let* pk_uri = neuterize unencrypted_sk_uri in
              let* () = fail_if_already_registered cctxt force pk_uri name in
              let* pkh, public_key =
                import_secret_key ~io:(cctxt :> Client_context.io_wallet) pk_uri
              in
              let* () =
                register_key cctxt ~force (pkh, pk_uri, sk_uri) ?public_key name
              in
              let*! () =
                cctxt#message
                  "Tezos address added: %a"
                  Signature.Public_key_hash.pp
                  pkh
              in
              return_unit);
      (let desc = "Generate a pair of BLS keys." in
       let force_switch = Aggregate_alias.Secret_key.force_switch in
       let cmd =
         prefixes ["bls"; "gen"; "keys"]
         @@ Aggregate_alias.Secret_key.fresh_alias_param @@ stop
       in
       match network with
       | Some `Mainnet ->
           command
             ~group
             ~desc
             (args1 (force_switch ()))
             cmd
             (fun force name (cctxt : #Client_context.full) ->
               Bls_commands.generate_keys ~force ~encrypted:true name cctxt)
       | Some `Testnet | None ->
           command
             ~group
             ~desc
             (args2 (force_switch ()) (encrypted_switch ()))
             cmd
             (fun (force, encrypted) name (cctxt : #Client_context.full) ->
               Bls_commands.generate_keys ~force ~encrypted name cctxt));
      command
        ~group
        ~desc:"List BlS keys."
        no_options
        (prefixes ["bls"; "list"; "keys"] @@ stop)
        (fun () cctxt -> Bls_commands.list_keys cctxt);
      command
        ~group
        ~desc:"Show the keys associated with an rollup account."
        (args1 show_private_switch)
        (prefixes ["bls"; "show"; "address"]
        @@ Aggregate_alias.Public_key_hash.alias_param @@ stop)
        (fun show_private (name, _pkh) (cctxt : #Client_context.full) ->
          Bls_commands.show_address ~show_private name cctxt);
      command
        ~group
        ~desc:"Add a secret key to the wallet."
        (args1 (Aggregate_alias.Secret_key.force_switch ()))
        (prefixes ["bls"; "import"; "secret"; "key"]
        @@ Aggregate_alias.Secret_key.fresh_alias_param
        @@ aggregate_sk_uri_param @@ stop)
        (fun force name sk_uri cctxt ->
          Bls_commands.import_secret_key ~force name sk_uri cctxt);
    ]
back to top