From 904b780b8090cff4f5607447059e76c50d4018cf Mon Sep 17 00:00:00 2001 From: Kristaps Kaupe Date: Thu, 22 Feb 2024 20:28:18 +0200 Subject: [PATCH] Unify cli user input code where limited range of answers are allowed --- scripts/add-utxo.py | 11 ++++++----- scripts/bumpfee.py | 5 +++-- scripts/sendpayment.py | 18 +++++++++--------- scripts/sendtomany.py | 4 ++-- scripts/tumbler.py | 4 ++-- src/jmbase/__init__.py | 3 ++- src/jmbase/support.py | 30 +++++++++++++++++++++++++++++- src/jmclient/cli_options.py | 35 ++++++++++++++--------------------- src/jmclient/taker_utils.py | 5 +++-- src/jmclient/wallet_utils.py | 30 +++++++++++++++--------------- 10 files changed, 85 insertions(+), 60 deletions(-) diff --git a/scripts/add-utxo.py b/scripts/add-utxo.py index 2c595e334..1d6ae83c3 100755 --- a/scripts/add-utxo.py +++ b/scripts/add-utxo.py @@ -16,7 +16,7 @@ PoDLE, get_podle_commitments, get_utxo_info, validate_utxo_data, quit,\ get_wallet_path, add_base_options, BTCEngine from jmbase.support import EXIT_SUCCESS, EXIT_FAILURE, \ - jmprint + jmprint, cli_prompt_user_yesno def add_ext_commitments(utxo_datas): @@ -152,14 +152,15 @@ def main(): if options.delete_ext: other = options.in_file or options.in_json or options.loadwallet if len(args) > 0 or other: - if input("You have chosen to delete commitments, other arguments " - "will be ignored; continue? (y/n)") != 'y': + if not cli_prompt_user_yesno( + "You have chosen to delete commitments, other arguments " + "will be ignored; continue?"): jmprint("Quitting", "warning") sys.exit(EXIT_SUCCESS) c, e = get_podle_commitments() jmprint(pformat(e), "info") - if input( - "You will remove the above commitments; are you sure? (y/n): ") != 'y': + if not cli_prompt_user_yesno( + "You will remove the above commitments; are you sure?"): jmprint("Quitting", "warning") sys.exit(EXIT_SUCCESS) update_commitments(external_to_remove=e) diff --git a/scripts/bumpfee.py b/scripts/bumpfee.py index 913af5046..9c2796163 100755 --- a/scripts/bumpfee.py +++ b/scripts/bumpfee.py @@ -2,7 +2,7 @@ from decimal import Decimal from jmbase import get_log, hextobin, bintohex -from jmbase.support import EXIT_SUCCESS, EXIT_FAILURE, EXIT_ARGERROR, jmprint +from jmbase.support import EXIT_SUCCESS, EXIT_FAILURE, EXIT_ARGERROR, jmprint, cli_prompt_user_yesno from jmclient import jm_single, load_program_config, open_test_wallet_maybe, get_wallet_path, WalletService from jmclient.cli_options import OptionParser, add_base_options import jmbitcoin as btc @@ -272,7 +272,8 @@ def create_bumped_tx(tx, fee_per_kb, wallet, output_index=-1): jlog.info(btc.human_readable_transaction(bumped_tx)) if not options.answeryes: - if input('Would you like to push to the network? (y/n):')[0] != 'y': + if not cli_prompt_user_yesno( + 'Would you like to push to the network?'): jlog.info("You chose not to broadcast the transaction, quitting.") sys.exit(EXIT_SUCCESS) diff --git a/scripts/sendpayment.py b/scripts/sendpayment.py index 00c47be0c..5425d38fb 100755 --- a/scripts/sendpayment.py +++ b/scripts/sendpayment.py @@ -21,7 +21,7 @@ EngineError, check_and_start_tor from twisted.python.log import startLogging from jmbase.support import get_log, jmprint, \ - EXIT_FAILURE, EXIT_ARGERROR + EXIT_FAILURE, EXIT_ARGERROR, cli_prompt_user_yesno import jmbitcoin as btc @@ -174,7 +174,7 @@ def main(): "above absurd value " f"{btc.fee_per_kb_to_str(absurd_fee)}.", "warning") - if input("Still continue? (y/n):")[0] != "y": + if not cli_prompt_user_yesno("Still continue?"): sys.exit("Aborted by user.") jm_single().config.set("POLICY", "absurd_fee_per_kb", str(max_potential_txfee)) @@ -227,8 +227,8 @@ def main(): if exp_tx_fees_ratio > 0.05: jmprint('WARNING: Expected bitcoin network miner fees for this coinjoin' ' amount are roughly {:.1%}'.format(exp_tx_fees_ratio), "warning") - if input('You might want to modify your tx_fee' - ' settings in joinmarket.cfg. Still continue? (y/n):')[0] != 'y': + print('You might want to modify your tx_fee settings in joinmarket.cfg.') + if not cli_prompt_user_yesno('Still continue?'): sys.exit('Aborted by user.') else: log.info("Estimated miner/tx fees for this coinjoin amount: {:.1%}" @@ -255,8 +255,8 @@ def main(): "with Payjoin. Please retry without a custom change address.") sys.exit(EXIT_ARGERROR) if options.makercount > 0: - if not options.answeryes and input( - general_custom_change_warning + " (y/n):")[0] != "y": + if not options.answeryes and \ + not cli_prompt_user_yesno(general_custom_change_warning): sys.exit(EXIT_ARGERROR) engine_recognized = True try: @@ -265,8 +265,8 @@ def main(): engine_recognized = False if (not engine_recognized) or ( change_addr_type != wallet_service.get_txtype()): - if not options.answeryes and input( - nonwallet_custom_change_warning + " (y/n):")[0] != "y": + if not options.answeryes and \ + not cli_prompt_user_yesno(nonwallet_custom_change_warning): sys.exit(EXIT_ARGERROR) if options.makercount == 0 and not bip78url: @@ -304,7 +304,7 @@ def filter_orders_callback(orders_fees, cjamount): log.info('WARNING ' * 6) log.info('\n'.join(['=' * 60] * 3)) if not options.answeryes: - if input('send with these orders? (y/n):')[0] != 'y': + if not cli_prompt_user_yesno('Send with these orders?'): return False return True diff --git a/scripts/sendtomany.py b/scripts/sendtomany.py index 8087b42cb..82c283bac 100755 --- a/scripts/sendtomany.py +++ b/scripts/sendtomany.py @@ -8,7 +8,7 @@ from optparse import OptionParser import jmbitcoin as btc from jmbase import (get_log, jmprint, bintohex, utxostr_to_utxo, - IndentedHelpFormatterWithNL) + IndentedHelpFormatterWithNL, cli_prompt_user_yesno) from jmclient import load_program_config, estimate_tx_fee, jm_single,\ validate_address, get_utxo_info, add_base_options,\ validate_utxo_data, quit, BTCEngine, compute_tx_locktime @@ -112,7 +112,7 @@ def main(): return log.info("Got signed transaction:\n" + bintohex(txsigned.serialize())) log.info(btc.human_readable_transaction(txsigned)) - if input('Would you like to push to the network? (y/n):')[0] != 'y': + if not cli_prompt_user_yesno('Would you like to push to the network?'): log.info("You chose not to broadcast the transaction, quitting.") return jm_single().bc_interface.pushtx(txsigned.serialize()) diff --git a/scripts/tumbler.py b/scripts/tumbler.py index 6340cb37c..59cc3ceda 100755 --- a/scripts/tumbler.py +++ b/scripts/tumbler.py @@ -16,7 +16,7 @@ from jmbase.support import get_log, jmprint, EXIT_SUCCESS, \ - EXIT_FAILURE, EXIT_ARGERROR + EXIT_FAILURE, EXIT_ARGERROR, cli_prompt_user_yesno log = get_log() @@ -95,7 +95,7 @@ def main(): jmprint("For restarts, destinations are taken from schedule file," " so passed destinations on the command line were ignored.", "important") - if input("OK? (y/n)") != "y": + if not cli_prompt_user_yesno("OK?"): sys.exit(EXIT_SUCCESS) destaddrs = [s[3] for s in schedule if s[3] not in ["INTERNAL", "addrask"]] jmprint("Remaining destination addresses in restart: " + ",".join(destaddrs), diff --git a/src/jmbase/__init__.py b/src/jmbase/__init__.py index abc8cdfb4..c0a186765 100644 --- a/src/jmbase/__init__.py +++ b/src/jmbase/__init__.py @@ -8,7 +8,8 @@ EXIT_SUCCESS, hexbin, dictchanger, listchanger, JM_WALLET_NAME_PREFIX, JM_APP_NAME, IndentedHelpFormatterWithNL, wrapped_urlparse, - bdict_sdict_convert, random_insert, dict_factory) + bdict_sdict_convert, random_insert, dict_factory, + cli_prompt_user_value, cli_prompt_user_yesno) from .proof_of_work import get_pow, verify_pow from .twisted_utils import (stop_reactor, is_hs_uri, get_tor_agent, get_nontor_agent, JMHiddenService, diff --git a/src/jmbase/support.py b/src/jmbase/support.py index 777501fdf..4124694da 100644 --- a/src/jmbase/support.py +++ b/src/jmbase/support.py @@ -8,7 +8,7 @@ from functools import wraps from optparse import IndentedHelpFormatter from sqlite3 import Cursor, Row -from typing import List +from typing import Callable, List, Optional import urllib.parse as urlparse # JoinMarket version @@ -361,3 +361,31 @@ def get_free_tcp_ports(num_ports: int) -> List[int]: def dict_factory(cursor: Cursor, row: Row) -> dict: fields = [column[0] for column in cursor.description] return {key: value for key, value in zip(fields, row)} + +def cli_prompt_user_value(message: str, + input_check_fn: Callable[[str], bool], + input_for_default: Optional[str] = None, + default_value: Optional[str] = None) -> str: + while True: + data = input(message) + if input_for_default is not None and data == input_for_default: + return default_value + if not input_check_fn(data): + continue + return data + +def cli_prompt_user_yesno(message: str) -> bool: + + def cli_prompt_yesno_check(value: str) -> bool: + if len(value) > 0: + value = value.upper() + res = value[0] == "Y" or value[0] == "N" + else: + res = False + if not res: + print("Bad answer, try again.") + return res + + data = cli_prompt_user_value(f"{message} (y/n): ", + cli_prompt_yesno_check) + return data[0] == "Y" or data[0] == "y" diff --git a/src/jmclient/cli_options.py b/src/jmclient/cli_options.py index 1ec51125f..42298ca84 100644 --- a/src/jmclient/cli_options.py +++ b/src/jmclient/cli_options.py @@ -6,7 +6,7 @@ import jmclient.support from jmbase import JM_APP_NAME from jmclient import jm_single, RegtestBitcoinCoreInterface, cryptoengine -from jmbase.support import print_jm_version +from jmbase.support import print_jm_version, cli_prompt_user_value """This exists as a separate module for two reasons: to reduce clutter in main scripts, and refactor out @@ -157,20 +157,6 @@ def prompt_user_for_cj_fee(rel_val, abs_val): significantly less; perhaps half that amount, depending on which counterparties are selected.""" - def prompt_user_value(m, val, check): - while True: - data = input(m) - if data == 'y': - return val - try: - val_user = float(data) - except ValueError: - print("Bad answer, try again.") - continue - if not check(val_user): - continue - return val_user - rel_prompt = False if rel_val is None: rel_prompt = True @@ -186,27 +172,34 @@ def prompt_user_value(m, val, check): msg = ("\nIf you want to keep this relative limit, enter 'y';" "\notherwise choose your own fraction (between 1 and 0): ") - def rel_check(val): - if val >= 1: + def rel_check(val: str) -> bool: + try: + val_float = float(val) + except ValueError: + print("Bad answer, try again.") + return False + if val_float >= 1: print("Choose a number below 1! Else you will spend all your " "bitcoins for fees!") return False return True - rel_val = prompt_user_value(msg, rel_val, rel_check) + rel_val = float(cli_prompt_user_value(msg, rel_check, "y", rel_val)) print("Success! Using relative fee limit of {:%}".format(rel_val)) if abs_prompt: msg = ("\nIf you want to keep this absolute limit, enter 'y';" "\notherwise choose your own limit in satoshi: ") - def abs_check(val): - if val % 1 != 0: + def abs_check(val: str) -> bool: + try: + val_int = int(val) + except ValueError: print("You must choose a full number!") return False return True - abs_val = int(prompt_user_value(msg, abs_val, abs_check)) + abs_val = int(cli_prompt_user_value(msg, abs_check, "y", abs_val)) print("Success! Using absolute fee limit of {}".format(abs_val)) print("""\nIf you don't want to see this message again, make an entry like diff --git a/src/jmclient/taker_utils.py b/src/jmclient/taker_utils.py index e6fc30ac2..afa69e781 100644 --- a/src/jmclient/taker_utils.py +++ b/src/jmclient/taker_utils.py @@ -6,7 +6,8 @@ import numbers from typing import Callable, Optional, Union -from jmbase import get_log, jmprint, bintohex, hextobin +from jmbase import get_log, jmprint, bintohex, hextobin, \ + cli_prompt_user_yesno from .configure import jm_single, validate_address, is_burn_destination from .schedule import human_readable_schedule_entry, tweak_tumble_schedule,\ schedule_to_text @@ -211,7 +212,7 @@ def direct_send(wallet_service: WalletService, amount: int, mixdepth: int, log.info(sending_info) if not answeryes: if not accept_callback: - if input('Would you like to push to the network? (y/n):')[0] != 'y': + if not cli_prompt_user_yesno('Would you like to push to the network?'): log.info("You chose not to broadcast the transaction, quitting.") return False else: diff --git a/src/jmclient/wallet_utils.py b/src/jmclient/wallet_utils.py index 2bbf6c70e..f00bfd083 100644 --- a/src/jmclient/wallet_utils.py +++ b/src/jmclient/wallet_utils.py @@ -20,7 +20,8 @@ from jmclient.wallet_service import WalletService from jmbase.support import (get_password, jmprint, EXIT_FAILURE, EXIT_ARGERROR, utxo_to_utxostr, hextobin, bintohex, - IndentedHelpFormatterWithNL, dict_factory) + IndentedHelpFormatterWithNL, dict_factory, + cli_prompt_user_yesno) from .cryptoengine import TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WPKH, \ TYPE_SEGWIT_WALLET_FIDELITY_BONDS @@ -696,28 +697,28 @@ def cli_user_mnemonic_entry(): mnemonic_extension = None return (mnemonic_phrase, mnemonic_extension) -def cli_do_use_mnemonic_extension(): - uin = input("Would you like to use a two-factor mnemonic recovery " - "phrase? write 'n' if you don't know what this is (y/n): ") - if len(uin) == 0 or uin[0] != 'y': +def cli_do_use_mnemonic_extension() -> bool: + if cli_prompt_user_yesno("Would you like to use a two-factor mnemonic " + "recovery phrase? " + "Write 'n' if you don't know what this is"): + return True + else: jmprint("Not using mnemonic extension", "info") return False #no mnemonic extension - else: - return True def cli_get_mnemonic_extension(): jmprint("Note: This will be stored in a reversible way. Do not reuse!", "info") return input("Enter mnemonic extension: ") -def cli_do_support_fidelity_bonds(): - uin = input("Would you like this wallet to support fidelity bonds? " - "write 'n' if you don't know what this is (y/n): ") - if len(uin) == 0 or uin[0] != 'y': +def cli_do_support_fidelity_bonds() -> bool: + if cli_prompt_user_yesno("Would you like this wallet to support " + "fidelity bonds? " + "Write 'n' if you don't know what this is"): + return True + else: jmprint("Not supporting fidelity bonds", "info") return False - else: - return True def wallet_generate_recover_bip39(method, walletspath, default_wallet_name, display_seed_callback, enter_seed_callback, enter_wallet_password_callback, @@ -1205,8 +1206,7 @@ def wallet_signpsbt(wallet_service, psbt): jmprint("Base64 of the above PSBT:") jmprint(signedpsbt.to_base64()) if signresult.is_final: - if input("Above PSBT is fully signed. Do you want to broadcast?" - "(y/n):") != "y": + if not cli_prompt_user_yesno("Above PSBT is fully signed. Do you want to broadcast?"): jmprint("Not broadcasting.") else: jmprint("Broadcasting...")