(*****************************************************************************) (* *) (* Open Source License *) (* Copyright (c) 2018 Dynamic Ledger Solutions, Inc. *) (* Copyright (c) 2021 Nomadic Labs, *) (* *) (* 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. *) (* *) (*****************************************************************************) type error = Overwrite_forbiden of string | File_not_found of string (* We need to exit Lwt + tzResult context from Yes_wallet. *) let run_load_bakers_public_keys ?staking_share_opt ?network_opt ?level base_dir ~active_bakers_only alias_pkh_pk_list = let open Yes_wallet_lib in let open Tezos_error_monad in match Lwt_main.run (load_bakers_public_keys ?staking_share_opt ?network_opt ?level base_dir ~active_bakers_only alias_pkh_pk_list) with | Ok alias_pkh_pk_list -> alias_pkh_pk_list | Error trace -> Format.eprintf "error:@.%a@." Error_monad.pp_print_trace trace ; exit 1 let run_load_contracts ?dump_contracts ?network_opt ?level base_dir = let open Yes_wallet_lib in let open Tezos_error_monad in match Lwt_main.run (load_contracts ?dump_contracts ?network_opt ?level base_dir) with | Ok l -> l | Error trace -> Format.eprintf "error:@.%a@." Error_monad.pp_print_trace trace ; exit 1 let run_build_yes_wallet ?staking_share_opt ?network_opt base_dir ~active_bakers_only ~aliases = let open Yes_wallet_lib in let open Tezos_error_monad in match Lwt_main.run (build_yes_wallet ?staking_share_opt ?network_opt base_dir ~active_bakers_only ~aliases) with | Ok alias_pkh_pk_list -> alias_pkh_pk_list | Error trace -> Format.eprintf "error:@.%a@." Error_monad.pp_print_trace trace ; exit 1 let try_copy ~replace source target = if source = target then Ok () else let tmp_target = Filename.temp_file target ".tmp" in if Sys.file_exists source then if (not (Sys.file_exists target)) || replace then ( let chin = open_in source and chout = open_out (if replace then tmp_target else target) in (try while true do let input = input_line chin in output_string chout input done with End_of_file -> ()) ; close_in chin ; close_out chout ; Ok (Sys.rename tmp_target target)) else Error (Overwrite_forbiden target) else Error (File_not_found source) let convert_wallet ~replace wallet_dir target_dir = let pkh_filename = "public_key_hashs" in let pk_filename = "public_keys" in let sk_filename = "secret_keys" in if not (Sys.file_exists wallet_dir) then failwith "wallet does not exists, cannot convert empty wallet." else let pkh_target = Filename.concat target_dir pkh_filename in let pk_target = Filename.concat target_dir pk_filename in let sk_target = Filename.concat target_dir sk_filename in let pkh_source = Filename.concat wallet_dir pkh_filename in let pk_source = Filename.concat wallet_dir pk_filename in if (Sys.file_exists pkh_target || Sys.file_exists pk_target || Sys.file_exists sk_target) && not replace then Format.eprintf "Warning: cannot write yes-wallet, at least one of the following files \ already exists: %s/{%s,%s,%s} (you can use --force option to \ overwrite files)@." target_dir pkh_filename pk_filename sk_filename else if Sys.file_exists pk_source then ( let sk_list = sk_list_of_pk_file pk_source in json_to_file sk_list sk_target ; (match try_copy ~replace pk_source pk_target with | Ok () -> () | Error (Overwrite_forbiden _) -> assert false (* [replace] is [true] in this branch *) | Error (File_not_found _) -> assert false (* [pk_source] file exists int this branch *)) ; match try_copy ~replace pkh_source pkh_target with | Ok () -> () | Error (Overwrite_forbiden _) -> assert false (* replace is true in this branch *) | Error (File_not_found _) -> ()) else Format.eprintf "Warning: cannot produce yes-wallet, the file %s does not exist@." pk_source let populate_wallet ~replace yes_wallet_dir alias_pkh_pk_list = let pkh_filename = Filename.concat yes_wallet_dir "public_key_hashs" in let pk_filename = Filename.concat yes_wallet_dir "public_keys" in let sk_filename = Filename.concat yes_wallet_dir "secret_keys" in if not (Sys.file_exists yes_wallet_dir) then Unix.mkdir yes_wallet_dir 0o750 ; if (Sys.file_exists pkh_filename || Sys.file_exists pk_filename || Sys.file_exists sk_filename) && not replace then ( Format.eprintf "Warning: cannot write wallet, at least one of the following files \ already exists: %s/{%s,%s,%s} (you can use --force option to overwrite \ files)@." yes_wallet_dir pkh_filename pk_filename sk_filename ; false) else ( Yes_wallet_lib.write_yes_wallet yes_wallet_dir alias_pkh_pk_list ; true) let alias_file_opt_name = "--aliases" let alias_file_extension = ".json" let active_bakers_only_opt_name = "--active-bakers-only" let force_opt_name = "--force" let staking_share_opt_name = "--staking-share" let network_opt_name = "--network" let level_opt_name = "--level" let supported_network = List.map fst Octez_node_config.Config_file.builtin_blockchain_networks let force = ref false let confirm_rewrite wallet = Format.printf "/!\\ Warning /!\\: You are about to rewrite all secret keys from wallet \ %s /!\\\n\n\ All SECRET KEYS of the wallet will be LOST.\n\n\ Are you sure you want to do that ? press Y or y to proceed, any other key \ to cancel.@." wallet ; String.uppercase_ascii (read_line ()) = "Y" let usage () = Format.printf "@[@[> convert wallet in @,\ creates a yes-wallet in with the public keys from \ @]@,\ @[@[> convert wallet inplace@,\ same as above but overwrite the file in the directory @]@,\ @[> create from context in [%s] [%s \ ] [%s <%a>]@,\ creates a yes-wallet with all delegates in the head block of the context \ in and store it in @,\ if %s is used the deactivated bakers are filtered out@,\ if %s is used, the first largest bakers that have an accumulated \ stake of at least percent of the total stake are kept@,\ if %s <%a> is used the store is opened using the right genesis parameter \ (default is mainnet) @]@]@,\ @[@[> compute total supply from [in ]@,\ computes the total supply form all contracts and commitments. result is \ printed in stantdard output, optionally informations on all read \ contracts can be dumped into csv_file@]@]@,\ @[> dump staking balances from in ]@,\ saves the staking balances of all delegates in the target csv file@,\ @[if %s is used, it will input aliases from an .json file.See \ README.md for the spec of this file and how to generate it.@],@[if %s \ is used existing files will be overwritten@]@." active_bakers_only_opt_name staking_share_opt_name network_opt_name Format.( pp_print_list ~pp_sep:(fun ppf () -> pp_print_string ppf "|") pp_print_string) supported_network active_bakers_only_opt_name staking_share_opt_name network_opt_name Format.( pp_print_list ~pp_sep:(fun ppf () -> pp_print_string ppf "|") pp_print_string) supported_network alias_file_opt_name force_opt_name let () = let argv = Array.to_list Sys.argv in let staking_share_opt = let rec aux argv = match argv with | [] -> None | str :: percentage :: _ when str = staking_share_opt_name -> let percentage = Int64.of_string percentage in assert (0L < percentage && percentage <= 100L) ; Some percentage | _ :: argv' -> aux argv' in aux argv in let network_opt = let rec aux argv = match argv with | [] -> None | str :: net :: _ when str = network_opt_name -> Some net | _ :: argv' -> aux argv' in aux argv in let level_opt = let rec aux argv = match argv with | [] -> None | str :: level :: _ when str = level_opt_name -> let level = Int32.of_string level in Some level | _ :: argv' -> aux argv' in aux argv in (* Take an alias file as input. *) let alias_file_opt = let rec aux argv = match argv with | [] -> None | str :: file :: _ when str = alias_file_opt_name -> if Sys.file_exists file then Some file else ( Format.eprintf "Warning: %a doesn't point to an existing alias file.\n" Format.pp_print_string file ; None) | _ :: argv' -> aux argv' in aux argv in let options, argv = List.partition (fun arg -> (String.length arg > 0 && String.get arg 0 = '-') || Str.string_match (Str.regexp "[0-9]+") arg 0 (* FIME this is an uggly hack, but hey -lets' force alias files to have a .json extension.*) || String.ends_with ~suffix:alias_file_extension arg || List.mem (String.lowercase_ascii arg) supported_network) argv in let active_bakers_only = List.exists (fun opt -> opt = active_bakers_only_opt_name) options in force := List.exists (fun opt -> opt = force_opt_name) options ; let unknown_options = let rec filter args = match args with | [] -> [] | opt :: t when opt = active_bakers_only_opt_name -> filter t | opt :: t when opt = force_opt_name -> filter t | opt :: num :: t when opt = staking_share_opt_name && Str.string_match (Str.regexp "[0-9]+") num 0 -> filter t | opt :: num :: t when opt = level_opt_name && Str.string_match (Str.regexp "[0-9]+") num 0 -> filter t | opt :: file :: t when opt = alias_file_opt_name && String.ends_with ~suffix:alias_file_extension file -> filter t | opt :: net :: t when opt = network_opt_name && List.mem (String.lowercase_ascii net) supported_network -> filter t | h :: t -> h :: filter t in filter options in (* load alias file *) let aliases = match alias_file_opt with | None -> [] | Some file -> Yes_wallet_lib.load_alias_file file in if unknown_options <> [] then Format.eprintf "Warning: unknown options %a@." (Format.pp_print_list Format.pp_print_string) unknown_options ; match argv with | [] -> assert false | [_] -> usage () ; exit 0 | [_; "create"; "from"; "context"; base_dir; "in"; yes_wallet_dir] -> if not (Sys.file_exists base_dir) then ( Format.eprintf "Invalid --data-dir provided: %s.\n" base_dir ; exit 1) else let yes_alias_list = run_build_yes_wallet ~staking_share_opt ?network_opt base_dir ~active_bakers_only ~aliases in Format.printf "@[Number of keys to export:@;<3 0>%d@]@." (List.length yes_alias_list) ; if populate_wallet ~replace:!force yes_wallet_dir yes_alias_list then Format.printf "@[Exported path:@;<14 0>%s@]@." yes_wallet_dir | [_; "convert"; "wallet"; base_dir; "in"; target_dir] -> convert_wallet ~replace:!force base_dir target_dir ; Format.printf "Wallet %s converted to a yes-wallet in %s@." base_dir target_dir | [_; "convert"; "wallet"; base_dir; "inplace"] -> if !force || confirm_rewrite base_dir then ( convert_wallet ~replace:true base_dir base_dir ; Format.printf "Converted wallet in %s@." base_dir) else Format.printf "I refuse to rewrite files in %s without confirmation or --force \ flag@." base_dir | _ :: "compute" :: "total" :: "supply" :: "from" :: base_dir :: tl -> ( let dump_contracts = match tl with ["in"; csv_file] -> Some csv_file | _ -> None in let contracts_list = run_load_contracts ~dump_contracts ?level:level_opt base_dir in match dump_contracts with | Some csv_file -> let flags = if !force then [Open_wronly; Open_creat; Open_trunc; Open_text] else [Open_wronly; Open_creat; Open_excl; Open_text] in Out_channel.with_open_gen flags 0o666 csv_file (fun oc -> let fmtr = Format.formatter_of_out_channel oc in Format.fprintf fmtr "address, balance, frozen_bonds, staked_balance, \ unstaked_frozen_balance, unstaked_finalizable_balance, @." ; List.iter (fun { address; balance; frozen_bonds; staked_balance; unstaked_frozen_balance; unstaked_finalizable_balance; } -> Format.fprintf fmtr "%s, %Ld, %Ld, %Ld, %Ld, %Ld@." address balance frozen_bonds staked_balance unstaked_frozen_balance unstaked_finalizable_balance) contracts_list) | None -> exit 0) | [_; "dump"; "staking"; "balances"; "from"; base_dir; "in"; csv_file] -> let alias_pkh_pk_list = run_load_bakers_public_keys ~staking_share_opt ?network_opt ?level:level_opt base_dir ~active_bakers_only aliases in let flags = if !force then [Open_wronly; Open_creat; Open_trunc; Open_text] else [Open_wronly; Open_creat; Open_excl; Open_text] in Out_channel.with_open_gen flags 0o666 csv_file (fun oc -> let fmtr = Format.formatter_of_out_channel oc in Format.fprintf fmtr "pkh, stake, spendable_balance, frozen_deposits, \ unstake_frozen_deposits\n" ; List.iter (fun ( _alias, pkh, _pk, stake, frozen_deposits, unstake_frozen_deposits ) -> Format.fprintf fmtr "%s, %Ld, %Ld, %Ld\n" pkh stake frozen_deposits unstake_frozen_deposits) alias_pkh_pk_list) | _ -> Format.eprintf "Invalid command. Usage:@." ; usage () ; exit 1