From d8410eff839d1bebbc9dee7eaeb08e67af224a18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 23 Feb 2021 11:23:56 +0100 Subject: [PATCH] Migrating CLI to Click (#1002) * Migrating CLI to Click * Adding more type annotations * Adding some required to flags * Some extra small fixes * Add click dependency to the setup.py file * Making callable from outside * Manually applying changes from commit f23c45 * Improving type annotations * Adding -h as --help option * Added feedback to add/remove commands for plots * Properly exiting with error code --- setup.py | 1 + src/cmds/chia.py | 95 +++++------ src/cmds/configure.py | 120 +++++-------- src/cmds/init.py | 80 +++++---- src/cmds/keys.py | 242 +++++++++----------------- src/cmds/netspace.py | 97 +++++------ src/cmds/plots.py | 323 +++++++++++++++++------------------ src/cmds/run_daemon.py | 11 -- src/cmds/show.py | 239 ++++++++++++-------------- src/cmds/start.py | 47 +++-- src/cmds/stop.py | 53 ++---- src/cmds/version.py | 10 -- src/cmds/wallet.py | 231 +++++++++++-------------- src/plotting/check_plots.py | 19 +-- src/plotting/create_plots.py | 5 +- src/wallet/wallet_node.py | 2 +- 16 files changed, 660 insertions(+), 915 deletions(-) delete mode 100644 src/cmds/run_daemon.py delete mode 100644 src/cmds/version.py diff --git a/setup.py b/setup.py index 869b77fa56..bd13d32811 100644 --- a/setup.py +++ b/setup.py @@ -23,6 +23,7 @@ dependencies = [ "setproctitle==1.2.2", # Gives the chia processes readable names "sortedcontainers==2.3.0", # For maintaining sorted mempools "websockets==8.1.0", # For use in wallet RPC and electron UI + "click==7.1.2", # For the CLI ] upnp_dependencies = [ diff --git a/src/cmds/chia.py b/src/cmds/chia.py index a67f965e53..014e664c79 100644 --- a/src/cmds/chia.py +++ b/src/cmds/chia.py @@ -1,62 +1,61 @@ -import importlib -import pathlib -from argparse import Namespace, ArgumentParser +import asyncio +import click +from pathlib import Path from src import __version__ from src.util.default_root import DEFAULT_ROOT_PATH +from src.daemon.server import async_run_daemon + +from src.cmds.init import init_cmd +from src.cmds.keys import keys_cmd +from src.cmds.plots import plots_cmd +from src.cmds.wallet import wallet_cmd +from src.cmds.configure import configure_cmd +from src.cmds.show import show_cmd +from src.cmds.start import start_cmd +from src.cmds.stop import stop_cmd +from src.cmds.netspace import netspace_cmd -SUBCOMMANDS = [ - "init", - "keys", - "show", - "start", - "stop", - "version", - "plots", - "netspace", - "run_daemon", - "wallet", - "configure", -] +CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) -def create_parser() -> ArgumentParser: - parser: ArgumentParser = ArgumentParser( - description="Manage chia blockchain infrastructure (%s)." % __version__, - epilog="Try 'chia start node', 'chia netspace -d 192', or 'chia show -s'.", - ) - - parser.add_argument( - "--root-path", - help="Config file root (defaults to %s)." % DEFAULT_ROOT_PATH, - type=pathlib.Path, - default=DEFAULT_ROOT_PATH, - ) - - subparsers = parser.add_subparsers() - - # this magic metaprogramming generalizes: - # from src.cmds import version - # new_parser = subparsers.add_parser(version) - # version.version_parser(new_parser) - - for subcommand in SUBCOMMANDS: - mod = importlib.import_module("src.cmds.%s" % subcommand) - mod.make_parser(subparsers.add_parser(subcommand)) # type: ignore - - parser.set_defaults(function=lambda args, parser: parser.print_help()) - return parser +@click.group( + help=f"\n Manage chia blockchain infrastructure ({__version__})\n", + epilog="Try 'chia start node', 'chia netspace -d 192', or 'chia show -s'.", + context_settings=CONTEXT_SETTINGS, +) +@click.option("--root-path", default=DEFAULT_ROOT_PATH, help="Config file root.", type=click.Path(), show_default=True) +@click.pass_context +def cli(ctx: click.Context, root_path: str) -> None: + ctx.ensure_object(dict) + ctx.obj["root_path"] = Path(root_path) -def chia(args: Namespace, parser: ArgumentParser): - return args.function(args, parser) +@cli.command("version", short_help="show version") +def version_cmd() -> None: + print(__version__) -def main(): - parser = create_parser() - args = parser.parse_args() - return chia(args, parser) +@cli.command("run_daemon", short_help="runs chia daemon") +@click.pass_context +def run_daemon_cmd(ctx: click.Context) -> None: + asyncio.get_event_loop().run_until_complete(async_run_daemon(ctx.obj["root_path"])) + + +cli.add_command(keys_cmd) +cli.add_command(plots_cmd) +cli.add_command(wallet_cmd) +cli.add_command(configure_cmd) +cli.add_command(init_cmd) +cli.add_command(show_cmd) +cli.add_command(start_cmd) +cli.add_command(stop_cmd) +cli.add_command(netspace_cmd) + + +def main() -> None: + cli() # pylint: disable=no-value-for-parameter if __name__ == "__main__": diff --git a/src/cmds/configure.py b/src/cmds/configure.py index 1ed29d973e..3aca4cb886 100644 --- a/src/cmds/configure.py +++ b/src/cmds/configure.py @@ -1,76 +1,24 @@ +import click +from pathlib import Path + from src.util.config import ( load_config, save_config, ) -from argparse import ArgumentParser from typing import Dict from src.util.default_root import DEFAULT_ROOT_PATH from src.util.config import str2bool -def make_parser(parser: ArgumentParser): - - parser.add_argument( - "--set-node-introducer", - help="Set the introducer for node - IP:Port", - type=str, - nargs="?", - default="", - ) - - parser.add_argument( - "--set-fullnode-port", - help="Set the port to use for the fullnode", - type=str, - nargs="?", - default="", - ) - - parser.add_argument( - "--set-log-level", - "--log-level", - "-log-level", - help="Set the instance log level, Can be CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET", - type=str, - nargs="?", - default="", - ) - - parser.add_argument( - "--enable-upnp", - "--upnp", - "-upnp", - help="Enable or disable uPnP. Can be True or False", - type=str, - nargs="?", - ) - - parser.set_defaults(function=configure) - - -def help_message(): - print("usage: chia configure -flag") - print( - """ - chia configure [arguments] [inputs] - --set-node-introducer [IP:Port] (Set the introducer for node), - --set-fullnode-port [Port] (Set the full node default port, useful for beta testing), - --set-log-level [LogLevel] (Can be CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET), - --enable-upnp, - --upnp {True,False} (Enable or disable uPnP. Can be True or False) - """ - ) - - -def configure(args, parser): +def configure(root_path: Path, set_node_introducer: str, set_fullnode_port: str, set_log_level: str, enable_upnp: str): config: Dict = load_config(DEFAULT_ROOT_PATH, "config.yaml") change_made = False - if args.set_node_introducer: + if set_node_introducer: try: - if args.set_node_introducer.index(":"): + if set_node_introducer.index(":"): host, port = ( - ":".join(args.set_node_introducer.split(":")[:-1]), - args.set_node_introducer.split(":")[-1], + ":".join(set_node_introducer.split(":")[:-1]), + set_node_introducer.split(":")[-1], ) config["full_node"]["introducer_peer"]["host"] = host config["full_node"]["introducer_peer"]["port"] = int(port) @@ -79,34 +27,52 @@ def configure(args, parser): change_made = True except ValueError: print("Node introducer address must be in format [IP:Port]") - if args.set_fullnode_port: - config["full_node"]["port"] = int(args.set_fullnode_port) - config["full_node"]["introducer_peer"]["port"] = int(args.set_fullnode_port) - config["farmer"]["full_node_peer"]["port"] = int(args.set_fullnode_port) - config["timelord"]["full_node_peer"]["port"] = int(args.set_fullnode_port) - config["wallet"]["full_node_peer"]["port"] = int(args.set_fullnode_port) - config["wallet"]["introducer_peer"]["port"] = int(args.set_fullnode_port) - config["introducer"]["port"] = int(args.set_fullnode_port) + if set_fullnode_port: + config["full_node"]["port"] = int(set_fullnode_port) + config["full_node"]["introducer_peer"]["port"] = int(set_fullnode_port) + config["farmer"]["full_node_peer"]["port"] = int(set_fullnode_port) + config["timelord"]["full_node_peer"]["port"] = int(set_fullnode_port) + config["wallet"]["full_node_peer"]["port"] = int(set_fullnode_port) + config["wallet"]["introducer_peer"]["port"] = int(set_fullnode_port) + config["introducer"]["port"] = int(set_fullnode_port) print("Default full node port updated.") change_made = True - if args.set_log_level: + if set_log_level: levels = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "NOTSET"] - if args.set_log_level in levels: - config["logging"]["log_level"] = args.set_log_level + if set_log_level in levels: + config["logging"]["log_level"] = set_log_level print(f"Logging level updated. Check {DEFAULT_ROOT_PATH}/log/debug.log") change_made = True else: print(f"Logging level not updated. Use one of: {levels}") - if args.enable_upnp is not None: - config["full_node"]["enable_upnp"] = str2bool(args.enable_upnp) - if str2bool(args.enable_upnp): + if enable_upnp is not None: + config["full_node"]["enable_upnp"] = str2bool(enable_upnp) + if str2bool(enable_upnp): print("uPnP enabled.") else: print("uPnP disabled.") change_made = True if change_made: print("Restart any running chia services for changes to take effect.") - save_config(args.root_path, "config.yaml", config) - else: - help_message() + save_config(root_path, "config.yaml", config) return 0 + + +@click.command("configure", short_help="modify configuration") +@click.option("--set-node-introducer", help="Set the introducer for node - IP:Port.", type=str) +@click.option( + "--set-fullnode-port", + help="Set the port to use for the fullnode, useful for beta testing.", + type=str, +) +@click.option( + "--set-log-level", + "--log-level", + "-log-level", + help="Set the instance log level.", + type=click.Choice(["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "NOTSET"]), +) +@click.option("--enable-upnp", "--upnp", "-upnp", help="Enable or disable uPnP.", type=click.Choice(["true", "false"])) +@click.pass_context +def configure_cmd(ctx, set_node_introducer, set_fullnode_port, set_log_level, enable_upnp): + configure(ctx.obj["root_path"], set_node_introducer, set_fullnode_port, set_log_level, enable_upnp) diff --git a/src/cmds/init.py b/src/cmds/init.py index ab8db1d43a..fdf6398633 100644 --- a/src/cmds/init.py +++ b/src/cmds/init.py @@ -1,15 +1,15 @@ +import click from src import __version__ +from pathlib import Path import os import shutil -from argparse import Namespace, ArgumentParser from typing import List, Dict, Any, Tuple from src.util.default_root import DEFAULT_ROOT_PATH from src.util.keychain import Keychain from src.util.config import unflatten_properties -from pathlib import Path from src.consensus.coinbase import create_puzzlehash_for_pk from src.util.ints import uint32 @@ -31,35 +31,6 @@ private_node_names = {"full_node", "wallet", "farmer", "harvester", "timelord", public_node_names = {"full_node", "wallet", "farmer", "introducer", "timelord"} -def help_message(): - print("usage: chia init") - print( - """ - chia init (migrate previous version configuration to current) - chia init -c [directory] (creates new TLS certificates signed by your CA in [directory]) - Follow these steps to create new certifcates for a remote harvester: - - Make a copy of your Farming Machine CA directory: ~/.chia/[version]/config/ssl/ca - - Shut down all chia daemon processes with `chia stop all -d` - - Run `chia init -c [directory]` on your remote harvester, - where [directory] is the the copy of your Farming Machine CA directory - - Get more details on remote harvester on Chia wiki: - https://github.com/Chia-Network/chia-blockchain/wiki/Farming-on-many-machines - """ - ) - - -def make_parser(parser): - parser.add_argument( - "-c", - "--create_certs", - help="Create new SSL certificates based on CA in [directory]", - type=Path, - default=None, - ) - parser.set_defaults(function=init) - parser.print_help = lambda self=parser: help_message() - - def dict_add_new_default(updated: Dict, default: Dict, do_not_migrate_keys: Dict[str, Any]): for k in do_not_migrate_keys: if k in updated and do_not_migrate_keys[k] == "": @@ -255,24 +226,24 @@ def generate_ssl_for_nodes(ssl_dir: Path, ca_crt: bytes, ca_key: bytes, private: generate_ca_signed_cert(ca_crt, ca_key, crt_path, key_path) -def init(args: Namespace, parser: ArgumentParser): - if args.create_certs is not None: - if args.root_path.exists(): - if os.path.isdir(args.create_certs): - ca_dir: Path = args.root_path / "config/ssl/ca" +def init(create_certs: Path, root_path: Path): + if create_certs is not None: + if root_path.exists(): + if os.path.isdir(create_certs): + ca_dir: Path = root_path / "config/ssl/ca" if ca_dir.exists(): print(f"Deleting your OLD CA in {ca_dir}") shutil.rmtree(ca_dir) - print(f"Copying your CA from {args.create_certs} to {ca_dir}") - copy_files_rec(args.create_certs, ca_dir) - create_all_ssl(args.root_path) + print(f"Copying your CA from {create_certs} to {ca_dir}") + copy_files_rec(create_certs, ca_dir) + create_all_ssl(root_path) else: - print(f"** Directory {args.create_certs} does not exist **") + print(f"** Directory {create_certs} does not exist **") else: - print(f"** {args.root_path} does not exist **") + print(f"** {root_path} does not exist **") print("** please run `chia init` to migrate or create new config files **") else: - return chia_init(args.root_path) + return chia_init(root_path) def chia_version_number() -> Tuple[str, str, str, str]: @@ -442,5 +413,30 @@ def chia_init(root_path: Path): return 0 +@click.command("init", short_help="create or migrate to current") +@click.option( + "--create-certs", + "-c", + default=None, + help="Create new SSL certificates based on CA in [directory]", + type=click.Path(), +) +@click.pass_context +def init_cmd(ctx: click.Context, create_certs: str): + """ + Create a new configuration or migrate from previous versions to current + + \b + Follow these steps to create new certifcates for a remote harvester: + - Make a copy of your Farming Machine CA directory: ~/.chia/[version]/config/ssl/ca + - Shut down all chia daemon processes with `chia stop all -d` + - Run `chia init -c [directory]` on your remote harvester, + where [directory] is the the copy of your Farming Machine CA directory + - Get more details on remote harvester on Chia wiki: + https://github.com/Chia-Network/chia-blockchain/wiki/Farming-on-many-machines + """ + init(Path(create_certs) if create_certs is not None else None, ctx.obj["root_path"]) + + if __name__ == "__main__": chia_init(DEFAULT_ROOT_PATH) diff --git a/src/cmds/keys.py b/src/cmds/keys.py index 72966e62f0..76447087a5 100644 --- a/src/cmds/keys.py +++ b/src/cmds/keys.py @@ -1,3 +1,5 @@ +import click + from pathlib import Path from typing import List @@ -18,97 +20,6 @@ from src.wallet.derive_keys import ( from src.util.ints import uint32 from src.consensus.coinbase import create_puzzlehash_for_pk -command_list = [ - "generate", - "generate_and_print", - "show", - "add", - "delete", - "delete_all", - "sign", - "verify", -] - - -def help_message(): - print("usage: chia keys command") - print(f"command can be any of {command_list}") - print("") - print("chia keys generate (generates and adds a key to keychain)") - print("chia keys generate_and_print (generates but does NOT add to keychain)") - print("chia keys show (displays all the keys in keychain)") - print("chia keys add -m [24 words] (add a private key through the mnemonic)") - print("chia keys delete -f [fingerprint] (delete a key by it's pk fingerprint in hex form)") - print("chia keys delete_all (delete all private keys in keychain)") - print("chia keys sign -f [fingerprint] -t [hd_path] -d [message] (sign a message with a private key)") - print("chia keys verify -p [public_key] -d [message] -s [signature] (verify a signature with a pk)") - - -def make_parser(parser): - parser.add_argument( - "-m", - "--mnemonic", - type=str, - nargs=24, - default=None, - help="Enter mnemonic you want to use", - ) - parser.add_argument( - "-k", - "--key", - type=str, - default=None, - help="Enter the raw private key in hex", - ) - parser.add_argument( - "-f", - "--fingerprint", - type=int, - default=None, - help="Enter the fingerprint of the key you want to use", - ) - - parser.add_argument( - "-t", - "--hd_path", - type=str, - default=None, - help="Enter the HD path in the form 'm/12381/8444/n/n'", - ) - - parser.add_argument( - "-d", - "--message", - type=str, - default=None, - help="Enter the message to sign in UTF-8", - ) - - parser.add_argument( - "-p", - "--public_key", - type=str, - default=None, - help="Enter the pk in hex", - ) - - parser.add_argument( - "-s", - "--signature", - type=str, - default=None, - help="Enter the signature in hex", - ) - - parser.add_argument( - "command", - help=f"Command can be any one of {command_list}", - type=str, - nargs="?", - ) - parser.set_defaults(function=handler) - parser.print_help = lambda self=parser: help_message() - keychain: Keychain = Keychain() @@ -135,7 +46,7 @@ def generate_and_add(): add_private_key_seed(mnemonic) -def add_private_key_seed(mnemonic): +def add_private_key_seed(mnemonic: str): """ Add a private key seed to the keyring, with the given mnemonic. """ @@ -186,39 +97,18 @@ def show_all_keys(): print(mnemonic) -def delete(args): +def delete(fingerprint: int): """ Delete a key by it's public key fingerprint (which is an int). """ - if args.fingerprint is None: - print("Please specify the fingerprint argument -f") - quit() - - fingerprint = args.fingerprint - assert fingerprint is not None print(f"Deleting private_key with fingerprint {fingerprint}") keychain.delete_key_by_fingerprint(fingerprint) -def sign(args): - if args.message is None: - print("Please specify the message argument -d") - quit() - - if args.fingerprint is None or args.hd_path is None: - print("Please specify the fingerprint argument -f and hd_path argument -t") - quit() - - message = args.message - assert message is not None - +def sign(message: str, fingerprint: int, hd_path: str): k = Keychain() private_keys = k.get_all_private_keys() - fingerprint = args.fingerprint - assert fingerprint is not None - hd_path = args.hd_path - assert hd_path is not None path: List[uint32] = [uint32(int(i)) for i in hd_path.split("/") if i != "m"] for sk, _ in private_keys: if sk.get_g1().get_fingerprint() == fingerprint: @@ -230,55 +120,85 @@ def sign(args): print(f"Fingerprint {fingerprint} not found in keychain") -def verify(args): - if args.message is None: - print("Please specify the message argument -d") - quit() - if args.public_key is None: - print("Please specify the public_key argument -p") - quit() - if args.signature is None: - print("Please specify the signature argument -s") - quit() - assert args.message is not None - assert args.public_key is not None - assert args.signature is not None - message = bytes(args.message, "utf-8") - public_key = G1Element.from_bytes(bytes.fromhex(args.public_key)) - signature = G2Element.from_bytes(bytes.fromhex(args.signature)) - print(AugSchemeMPL.verify(public_key, message, signature)) +def verify(message: str, public_key: str, signature: str): + messageBytes = bytes(message, "utf-8") + public_key = G1Element.from_bytes(bytes.fromhex(public_key)) + signature = G2Element.from_bytes(bytes.fromhex(signature)) + print(AugSchemeMPL.verify(public_key, messageBytes, signature)) -def handler(args, parser): - if args.command is None or len(args.command) < 1: - help_message() - parser.exit(1) - - root_path: Path = args.root_path +@click.group("keys", short_help="manage your keys") +@click.pass_context +def keys_cmd(ctx: click.Context): + """Create, delete, view and use your key pairs""" + root_path: Path = ctx.obj["root_path"] if not root_path.is_dir(): raise RuntimeError("Please initialize (or migrate) your config directory with chia init.") - command = args.command - if command not in command_list: - help_message() - parser.exit(1) - if command == "generate": - generate_and_add() - check_keys(root_path) - elif command == "show": - show_all_keys() - elif command == "add": - add_private_key_seed(" ".join(args.mnemonic)) - check_keys(root_path) - elif command == "delete": - delete(args) - check_keys(root_path) - elif command == "delete_all": - keychain.delete_all_keys() - if command == "generate_and_print": - generate_and_print() - if command == "sign": - sign(args) - if command == "verify": - verify(args) +@keys_cmd.command("generate", short_help="generates and adds a key to keychain") +@click.pass_context +def generate_cmd(ctx: click.Context): + generate_and_add() + check_keys(ctx.obj["root_path"]) + + +@keys_cmd.command("show", short_help="displays all the keys in keychain") +def show_cmd(): + show_all_keys() + + +@keys_cmd.command("add", short_help="add a private key through the mnemonic") +@click.option("--mnemonic", "-m", help="Enter mnemonic you want to use", type=str) +@click.pass_context +def add_cmd(ctx: click.Context, mnemonic: str): + add_private_key_seed(mnemonic) + check_keys(ctx.obj["root_path"]) + + +@keys_cmd.command("delete", short_help="delete a key by it's pk fingerprint in hex form") +@click.option( + "--fingerprint", + "-f", + default=None, + help="Enter the fingerprint of the key you want to use", + type=int, + required=True, +) +@click.pass_context +def delete_cmd(ctx: click.Context, fingerprint: int): + delete(fingerprint) + check_keys(ctx.obj["root_path"]) + + +@keys_cmd.command("delete_all", short_help="delete all private keys in keychain") +def delete_all_cmd(): + keychain.delete_all_keys() + + +@keys_cmd.command("generate_and_print", short_help="generates but does NOT add to keychain") +def generate_and_print_cmd(): + generate_and_print() + + +@keys_cmd.command("sign", short_help="sign a message with a private key") +@click.option("--message", "-d", default=None, help="Enter the message to sign in UTF-8", type=str, required=True) +@click.option( + "--fingerprint", + "-f", + default=None, + help="Enter the fingerprint of the key you want to use", + type=int, + required=True, +) +@click.option("--hd_path", "-t", help="Enter the HD path in the form 'm/12381/8444/n/n'", type=str, required=True) +def sing_cmd(message: str, fingerprint: int, hd_path: str): + sign(message, fingerprint, hd_path) + + +@keys_cmd.command("verify", short_help="verify a signature with a pk") +@click.option("--message", "-d", default=None, help="Enter the message to sign in UTF-8", type=str, required=True) +@click.option("--public_key", "-p", default=None, help="Enter the pk in hex", type=str, required=True) +@click.option("--signature", "-s", default=None, help="Enter the signature in hex", type=str, required=True) +def verify_cmd(message: str, public_key: str, signature: str): + verify(message, public_key, signature) diff --git a/src/cmds/netspace.py b/src/cmds/netspace.py index 6a04c16c51..75f929ffa7 100644 --- a/src/cmds/netspace.py +++ b/src/cmds/netspace.py @@ -1,64 +1,27 @@ +import click import aiohttp import asyncio -import time -from time import struct_time, localtime from src.util.config import load_config from src.util.default_root import DEFAULT_ROOT_PATH from src.util.byte_types import hexstr_to_bytes +from src.util.ints import uint16 from src.rpc.full_node_rpc_client import FullNodeRpcClient -def make_parser(parser): - - parser.add_argument( - "-d", - "--delta-block-height", - help="Compare a block X blocks older to estimate total network space. " - + "Defaults to 192 blocks (~1 hour) and Peak block as the starting block. " - + "Use --start BLOCK_HEIGHT to specify starting block. " - + "Use 1000 blocks to replicate the GUI estimate.", - type=str, - default="192", - ) - parser.add_argument( - "-s", - "--start", - help="Newest block used to calculate estimated total network space. Defaults to Peak block.", - type=str, - default="", - ) - parser.add_argument( - "-p", - "--rpc-port", - help="Set the port where the Full Node is hosting the RPC interface." - + "See the rpc_port under full_node in config.yaml. Defaults to 8555.", - type=int, - ) - parser.set_defaults(function=netspace) - - -def human_local_time(timestamp): - time_local = struct_time(localtime(timestamp)) - return time.strftime("%a %b %d %Y %T %Z", time_local) - - -async def netstorge_async(args, parser): +async def netstorge_async(rpc_port: int, delta_block_height: str, start: str) -> None: """ Calculates the estimated space on the network given two block header hases - # TODO: add help on failure/no args """ try: config = load_config(DEFAULT_ROOT_PATH, "config.yaml") self_hostname = config["self_hostname"] - if "rpc_port" not in args or args.rpc_port is None: + if rpc_port is None: rpc_port = config["full_node"]["rpc_port"] - else: - rpc_port = args.rpc_port - client = await FullNodeRpcClient.create(self_hostname, rpc_port, DEFAULT_ROOT_PATH, config) + client = await FullNodeRpcClient.create(self_hostname, uint16(rpc_port), DEFAULT_ROOT_PATH, config) - if args.delta_block_height: - if args.start == "": + if delta_block_height: + if start == "": blockchain_state = await client.get_blockchain_state() if blockchain_state["peak"] is None: print("No blocks in blockchain") @@ -68,9 +31,9 @@ async def netstorge_async(args, parser): newer_block_height = blockchain_state["peak"].height else: - newer_block = await client.get_block_record(hexstr_to_bytes(args.start)) + newer_block = await client.get_block_record(hexstr_to_bytes(start)) if newer_block is None: - print("Block header hash", args.start, "not found.") + print("Block header hash", start, "not found.") client.close() await client.await_closed() return None @@ -79,7 +42,7 @@ async def netstorge_async(args, parser): newer_block_height = newer_block.height newer_block_header = await client.get_block_record_by_height(newer_block_height) - older_block_height = max(0, newer_block_height - int(args.delta_block_height)) + older_block_height = max(0, newer_block_height - int(delta_block_height)) older_block_header = await client.get_block_record_by_height(older_block_height) network_space_bytes_estimate = await client.get_network_space( newer_block_header.header_hash, older_block_header.header_hash @@ -106,7 +69,7 @@ async def netstorge_async(args, parser): except Exception as e: if isinstance(e, aiohttp.client_exceptions.ClientConnectorError): - print(f"Connection error. Check if full node rpc is running at {args.rpc_port}") + print(f"Connection error. Check if full node rpc is running at {rpc_port}") else: print(f"Exception {e}") @@ -114,5 +77,39 @@ async def netstorge_async(args, parser): await client.await_closed() -def netspace(args, parser): - return asyncio.run(netstorge_async(args, parser)) +@click.command("netspace", short_help="estimate space on the network") +@click.option( + "-p", + "--rpc-port", + help=( + "Set the port where the Full Node is hosting the RPC interface. " + "See the rpc_port under full_node in config.yaml. " + "[default: 8555]" + ), + type=int, + show_default=True, +) +@click.option( + "-d", + "--delta-block-height", + help=( + "Compare a block X blocks older to estimate total network space. " + "Defaults to 192 blocks (~1 hour) and Peak block as the starting block. " + "Use --start BLOCK_HEIGHT to specify starting block. " + "Use 1000 blocks to replicate the GUI estimate." + ), + type=str, + default="192", +) +@click.option( + "-s", + "--start", + help="Newest block used to calculate estimated total network space. Defaults to Peak block.", + type=str, + default="", +) +def netspace_cmd(rpc_port: int, delta_block_height: str, start: str) -> None: + """ + Calculates the estimated space on the network given two block header hases. + """ + asyncio.run(netstorge_async(rpc_port, delta_block_height, start)) diff --git a/src/cmds/plots.py b/src/cmds/plots.py index dce918e654..dcfa5aebf4 100644 --- a/src/cmds/plots.py +++ b/src/cmds/plots.py @@ -1,3 +1,5 @@ +import click + from pathlib import Path import logging from src.plotting.plot_tools import ( @@ -13,154 +15,7 @@ from src.util.logging import initialize_logging log = logging.getLogger(__name__) -command_list = ["create", "check", "add", "remove", "show"] - - -def help_message(): - print("usage: chia plots command") - print(f"command can be any of {command_list}") - print("") - print( - "chia plots create -k [size] -n [number of plots] -b [memory buffer size MiB]" - + " -r [number of threads] -u [number of buckets] -s [stripe size]" - + " -a [fingerprint] -f [farmer public key] -p [pool public key]" - + " -t [tmp dir] -2 [tmp dir 2] -d [final dir] (creates plots)" - ) - print("-e disables bitfield plotting") - print("-x skips adding [final dir] to harvester for farming") - print("-i [plotid] -m [memo] are available for debugging") - print("chia plots check -n [challenges] -g [string] -l (checks plots)") - print(" Default: check all plots in every directory with 30 challenges") - print(" -n: number of challenges; 0 = skip opening plot files; can be used with -l") - print(" -g: checks plots with file or directory name containing [string]") - print(" -l: list plots with duplicate IDs") - print(" Debugging options for chia plots check") - print(" --debug-show-memo: shows memo to recreate the same exact plot") - print(" --challenge-start [start]: begins at a different [start] for -n [challenges]") - print("chia plots add -d [directory] (adds a directory of plots)") - print("chia plots remove -d [directory] (removes a directory of plots from config)") - print("chia plots show (shows the directory of current plots)") - - -def make_parser(parser): - parser.add_argument("-k", "--size", help="Plot size", type=int, default=32) - parser.add_argument("-n", "--num", help="Number of plots or challenges", type=int, default=None) - parser.add_argument("-b", "--buffer", help="Mebibytes for sort/plot buffer", type=int, default=4608) - parser.add_argument("-r", "--num_threads", help="Number of threads to use", type=int, default=2) - parser.add_argument("-u", "--buckets", help="Number of buckets", type=int, default=0) - parser.add_argument("-s", "--stripe_size", help="Stripe size", type=int, default=0) - parser.add_argument( - "-a", - "--alt_fingerprint", - type=int, - default=None, - help="Enter the alternative fingerprint of the key you want to use", - ) - parser.add_argument( - "-c", - "--pool_contract_address", - type=str, - default=None, - help=( - "Address of where the pool reward will be sent to. Only used " - "if alt_fingerprint and pool public key are None" - ), - ) - parser.add_argument( - "-f", - "--farmer_public_key", - help="Hex farmer public key", - type=str, - default=None, - ) - parser.add_argument("-p", "--pool_public_key", help="Hex public key of pool", type=str, default=None) - parser.add_argument( - "-g", - "--grep_string", - help="Shows only plots that contain the string in the filename or directory name", - type=str, - default=None, - ) - parser.add_argument( - "-t", - "--tmp_dir", - help="Temporary directory for plotting files", - type=Path, - default=Path("."), - ) - parser.add_argument( - "-2", - "--tmp2_dir", - help="Second temporary directory for plotting files", - type=Path, - default=None, - ) - parser.add_argument( - "-d", - "--final_dir", - help="Final directory for plots (relative or absolute)", - type=Path, - default=Path("."), - ) - parser.add_argument( - "-i", - "--plotid", - help="PlotID in hex for reproducing plots (debugging only)", - type=str, - default=None, - ) - parser.add_argument( - "-m", - "--memo", - help="Memo in hex for reproducing plots (debugging only)", - type=str, - default=None, - ) - parser.add_argument( - "-e", - "--nobitfield", - help="Disable bitfield", - default=False, - action="store_true", - ) - parser.add_argument( - "-x", - "--exclude_final_dir", - help="Skips adding [final dir] to harvester for farming", - default=False, - action="store_true", - ) - parser.add_argument( - "-l", - "--list_duplicates", - help="List plots with duplicate IDs", - default=False, - action="store_true", - ) - parser.add_argument( - "--debug-show-memo", - help="Shows memo to recreate the same exact plot", - default=False, - action="store_true", - ) - parser.add_argument( - "--challenge-start", - help="Begins at a different [start] for -n [challenges]", - type=int, - default=None, - ) - parser.add_argument( - "command", - help=f"Command can be any one of {command_list}", - type=str, - nargs="?", - ) - - parser.set_defaults(function=handler) - parser.print_help = lambda self=parser: help_message() - - -def show(root_path): +def show_plots(root_path: Path): print("Directories where plots are being searched for:") print("Note that subdirectories must be added manually.") print( @@ -173,30 +28,156 @@ def show(root_path): print(f"{str_path}") -def handler(args, parser): - if args.command is None or len(args.command) < 1: - help_message() - parser.exit(1) - - root_path: Path = args.root_path +@click.group("plots", short_help="manage your plots") +@click.pass_context +def plots_cmd(ctx: click.Context): + """Create, add, remove and check your plots""" + root_path: Path = ctx.obj["root_path"] if not root_path.is_dir(): raise RuntimeError("Please initialize (or migrate) your config directory with chia init.") - initialize_logging("", {"log_stdout": True}, root_path) - command = args.command - if command not in command_list: - help_message() - parser.exit(1) - if command == "create": - create_plots(args, root_path) - elif command == "check": - check_plots(args, root_path) - elif command == "add": - str_path = args.final_dir - add_plot_directory(str_path, root_path) - elif command == "remove": - str_path = args.final_dir - remove_plot_directory(str_path, root_path) - elif command == "show": - show(root_path) + +@plots_cmd.command("create", short_help="creates plots") +@click.option("-k", "--size", help="Plot size", type=int, default=32, show_default=True) +@click.option("-n", "--num", help="Number of plots or challenges", type=int, default=1, show_default=True) +@click.option("-b", "--buffer", help="Megabytes for sort/plot buffer", type=int, default=4608, show_default=True) +@click.option("-r", "--num_threads", help="Number of threads to use", type=int, default=2, show_default=True) +@click.option("-u", "--buckets", help="Number of buckets", type=int, default=0) +@click.option("-s", "--stripe_size", help="Stripe size", type=int, default=0) +@click.option( + "-a", + "--alt_fingerprint", + type=int, + default=None, + help="Enter the alternative fingerprint of the key you want to use", +) +@click.option( + "-c", + "--pool_contract_address", + type=str, + default=None, + help="Address of where the pool reward will be sent to. Only used if alt_fingerprint and pool public key are None", +) +@click.option("-f", "--farmer_public_key", help="Hex farmer public key", type=str, default=None) +@click.option("-p", "--pool_public_key", help="Hex public key of pool", type=str, default=None) +@click.option( + "-t", + "--tmp_dir", + help="Temporary directory for plotting files", + type=click.Path(), + default=Path("."), + show_default=True, +) +@click.option("-2", "--tmp2_dir", help="Second temporary directory for plotting files", type=click.Path(), default=None) +@click.option( + "-d", + "--final_dir", + help="Final directory for plots (relative or absolute)", + type=click.Path(), + default=Path("."), + show_default=True, +) +@click.option("-i", "--plotid", help="PlotID in hex for reproducing plots (debugging only)", type=str, default=None) +@click.option("-m", "--memo", help="Memo in hex for reproducing plots (debugging only)", type=str, default=None) +@click.option("-e", "--nobitfield", help="Disable bitfield", default=False, is_flag=True) +@click.option( + "-x", "--exclude_final_dir", help="Skips adding [final dir] to harvester for farming", default=False, is_flag=True +) +@click.pass_context +def create_cmd( + ctx: click.Context, + size: int, + num: int, + buffer: int, + num_threads: int, + buckets: int, + stripe_size: int, + alt_fingerprint: int, + pool_contract_address: str, + farmer_public_key: str, + pool_public_key: str, + tmp_dir: str, + tmp2_dir: str, + final_dir: str, + plotid: str, + memo: str, + nobitfield: bool, + exclude_final_dir: bool, +): + class Params(object): + def __init__(self): + self.size = size + self.num = num + self.buffer = buffer + self.num_threads = num_threads + self.buckets = buckets + self.stripe_size = stripe_size + self.alt_fingerprint = alt_fingerprint + self.pool_contract_address = pool_contract_address + self.farmer_public_key = farmer_public_key + self.pool_public_key = pool_public_key + self.tmp_dir = Path(tmp_dir) + self.tmp2_dir = Path(tmp2_dir) if tmp2_dir else None + self.final_dir = Path(final_dir) + self.plotid = plotid + self.memo = memo + self.nobitfield = nobitfield + self.exclude_final_dir = exclude_final_dir + + create_plots(Params(), ctx.obj["root_path"]) + + +@plots_cmd.command("check", short_help="checks plots") +@click.option("-n", "--num", help="Number of plots or challenges", type=int, default=None) +@click.option( + "-g", + "--grep_string", + help="Shows only plots that contain the string in the filename or directory name", + type=str, + default=None, +) +@click.option("-l", "--list_duplicates", help="List plots with duplicate IDs", default=False, is_flag=True) +@click.option("--debug-show-memo", help="Shows memo to recreate the same exact plot", default=False, is_flag=True) +@click.option("--challenge-start", help="Begins at a different [start] for -n [challenges]", type=int, default=None) +@click.pass_context +def check_cmd( + ctx: click.Context, num: int, grep_string: str, list_duplicates: bool, debug_show_memo: bool, challenge_start: int +): + check_plots(ctx.obj["root_path"], num, challenge_start, grep_string, list_duplicates, debug_show_memo) + + +@plots_cmd.command("add", short_help="adds a directory of plots") +@click.option( + "-d", + "--final_dir", + help="Final directory for plots (relative or absolute)", + type=click.Path(), + default=".", + show_default=True, +) +@click.pass_context +def add_cmd(ctx: click.Context, final_dir: str): + add_plot_directory(Path(final_dir), ctx.obj["root_path"]) + print(f'Added plot directory "{final_dir}".') + + +@plots_cmd.command("remove", short_help="removes a directory of plots from config") +@click.option( + "-d", + "--final_dir", + help="Final directory for plots (relative or absolute)", + type=click.Path(), + default=".", + show_default=True, +) +@click.pass_context +def remove_cmd(ctx: click.Context, final_dir: str): + remove_plot_directory(Path(final_dir), ctx.obj["root_path"]) + print(f'Removed plot directory "{final_dir}".') + + +@plots_cmd.command("show", short_help="shows the directory of current plots") +@click.pass_context +def show_cmd(ctx: click.Context): + show_plots(ctx.obj["root_path"]) diff --git a/src/cmds/run_daemon.py b/src/cmds/run_daemon.py deleted file mode 100644 index 9b9d263511..0000000000 --- a/src/cmds/run_daemon.py +++ /dev/null @@ -1,11 +0,0 @@ -import asyncio - -from src.daemon.server import async_run_daemon - - -def make_parser(parser): - parser.set_defaults(function=run_daemon) - - -def run_daemon(args, parser): - return asyncio.get_event_loop().run_until_complete(async_run_daemon(args.root_path)) diff --git a/src/cmds/show.py b/src/cmds/show.py index 8fd7f48553..54f07af0f2 100644 --- a/src/cmds/show.py +++ b/src/cmds/show.py @@ -1,3 +1,4 @@ +import click import traceback import aiohttp @@ -12,115 +13,37 @@ from src.server.outbound_message import NodeType from src.types.full_block import FullBlock from src.rpc.full_node_rpc_client import FullNodeRpcClient from src.util.byte_types import hexstr_to_bytes -from src.util.config import str2bool from src.util.config import load_config from src.util.default_root import DEFAULT_ROOT_PATH from src.util.bech32m import encode_puzzle_hash +from src.util.ints import uint16 -def make_parser(parser): - - parser.add_argument( - "-s", - "--state", - help="Show the current state of the blockchain.", - type=str2bool, - nargs="?", - const=True, - default=False, - ) - - parser.add_argument( - "-c", - "--connections", - help="List nodes connected to this Full Node.", - type=str2bool, - nargs="?", - const=True, - default=False, - ) - - parser.add_argument( - "-b", - "--block-by-header-hash", - help="Look up a block by block header hash.", - type=str, - default="", - ) - - parser.add_argument( - "-bh", - "--block-header-hash-by-height", - help="Look up a block header hash by block height.", - type=str, - default="", - ) - - parser.add_argument( - "-a", - "--add-connection", - help="Connect to another Full Node by ip:port", - type=str, - default="", - ) - - parser.add_argument( - "-r", - "--remove-connection", - help="Remove a Node by the first 8 characters of NodeID", - type=str, - default="", - ) - - parser.add_argument( - "-e", - "--exit-node", - help="Shut down the running Full Node", - nargs="?", - const=True, - default=False, - ) - - parser.add_argument( - "-p", - "--rpc-port", - help="Set the port where the Full Node is hosting the RPC interface." - + " See the rpc_port under full_node in config.yaml." - + "Defaults to 8555", - type=int, - default=8555, - ) - - parser.add_argument( - "-wp", - "--wallet-rpc-port", - help="Set the port where the Wallet is hosting the RPC interface." - + " See the rpc_port under wallet in config.yaml." - + "Defaults to 9256", - type=int, - default=9256, - ) - - parser.set_defaults(function=show) - - -async def show_async(args, parser): +async def show_async( + rpc_port: int, + state: bool, + show_connections: bool, + exit_node: bool, + add_connection: str, + remove_connection: str, + block_header_hash_by_height: str, + block_by_header_hash: str, +) -> None: # TODO read configuration for rpc_port instead of assuming default try: config = load_config(DEFAULT_ROOT_PATH, "config.yaml") self_hostname = config["self_hostname"] - if "rpc_port" not in args or args.rpc_port is None: + if rpc_port is None: rpc_port = config["full_node"]["rpc_port"] - else: - rpc_port = args.rpc_port - client = await FullNodeRpcClient.create(self_hostname, rpc_port, DEFAULT_ROOT_PATH, config) + client = await FullNodeRpcClient.create(self_hostname, uint16(rpc_port), DEFAULT_ROOT_PATH, config) - if args.state: + if state: blockchain_state = await client.get_blockchain_state() if blockchain_state is None: - return "There is no blockchain found yet. Try again shortly." + print("There is no blockchain found yet. Try again shortly.") + return peak: Optional[BlockRecord] = blockchain_state["peak"] difficulty = blockchain_state["difficulty"] sub_slot_iters = blockchain_state["sub_slot_iters"] @@ -140,7 +63,7 @@ async def show_async(args, parser): ) if synced: print("Current Blockchain Status: Full Node Synced") - print("\nPeak: Hash:", peak.header_hash) + print("\nPeak: Hash:", peak.header_hash if peak is not None else "") elif peak is not None: print(f"Current Blockchain Status: Not Synced. Peak height: {peak.height}") else: @@ -156,11 +79,11 @@ async def show_async(args, parser): while curr is not None and not curr.is_transaction_block: curr = await client.get_block_record(curr.prev_hash) peak_time = curr.timestamp - peak_time = struct_time(localtime(peak_time)) + peak_time_struct = struct_time(localtime(peak_time)) print( " Time:", - f"{time.strftime('%a %b %d %Y %T %Z', peak_time)}", + f"{time.strftime('%a %b %d %Y %T %Z', peak_time_struct)}", f" Height: {peak.height:>10}\n", ) @@ -188,10 +111,10 @@ async def show_async(args, parser): else: print("Blockchain has no blocks yet") - # if called together with connections, leave a blank line - if args.connections: + # if called together with show_connections, leave a blank line + if show_connections: print("") - if args.connections: + if show_connections: connections = await client.get_connections() print("Connections:") print( @@ -235,18 +158,18 @@ async def show_async(args, parser): ) print(con_str) # if called together with state, leave a blank line - if args.state: + if state: print("") - if args.exit_node: + if exit_node: node_stop = await client.stop_node() print(node_stop, "Node stopped.") - if args.add_connection: - if ":" not in args.add_connection: + if add_connection: + if ":" not in add_connection: print("Enter a valid IP and port in the following format: 10.5.4.3:8000") else: ip, port = ( - ":".join(args.add_connection.split(":")[:-1]), - args.add_connection.split(":")[-1], + ":".join(add_connection.split(":")[:-1]), + add_connection.split(":")[-1], ) print(f"Connecting to {ip}, {port}") try: @@ -254,34 +177,34 @@ async def show_async(args, parser): except Exception: # TODO: catch right exception print(f"Failed to connect to {ip}:{port}") - if args.remove_connection: + if remove_connection: result_txt = "" - if len(args.remove_connection) != 8: + if len(remove_connection) != 8: result_txt = "Invalid NodeID. Do not include '.'." else: connections = await client.get_connections() for con in connections: - if args.remove_connection == con["node_id"].hex()[:8]: - print("Attempting to disconnect", "NodeID", args.remove_connection) + if remove_connection == con["node_id"].hex()[:8]: + print("Attempting to disconnect", "NodeID", remove_connection) try: await client.close_connection(con["node_id"]) except Exception: - result_txt = f"Failed to disconnect NodeID {args.remove_connection}" + result_txt = f"Failed to disconnect NodeID {remove_connection}" else: - result_txt = f"NodeID {args.remove_connection}... {NodeType(con['type']).name} " + result_txt = f"NodeID {remove_connection}... {NodeType(con['type']).name} " f"{con['peer_host']} disconnected." elif result_txt == "": - result_txt = f"NodeID {args.remove_connection}... not found." + result_txt = f"NodeID {remove_connection}... not found." print(result_txt) - if args.block_header_hash_by_height != "": - block_header = await client.get_block_record_by_height(args.block_header_hash_by_height) + if block_header_hash_by_height != "": + block_header = await client.get_block_record_by_height(block_header_hash_by_height) if block_header is not None: - print(f"Header hash of block {args.block_header_hash_by_height}: " f"{block_header.header_hash.hex()}") + print(f"Header hash of block {block_header_hash_by_height}: " f"{block_header.header_hash.hex()}") else: - print("Block height", args.block_header_hash_by_height, "not found.") - if args.block_by_header_hash != "": - block: Optional[BlockRecord] = await client.get_block_record(hexstr_to_bytes(args.block_by_header_hash)) - full_block: Optional[FullBlock] = await client.get_block(hexstr_to_bytes(args.block_by_header_hash)) + print("Block height", block_header_hash_by_height, "not found.") + if block_by_header_hash != "": + block: Optional[BlockRecord] = await client.get_block_record(hexstr_to_bytes(block_by_header_hash)) + full_block: Optional[FullBlock] = await client.get_block(hexstr_to_bytes(block_by_header_hash)) # Would like to have a verbose flag for this if block is not None: assert full_block is not None @@ -292,10 +215,18 @@ async def show_async(args, parser): difficulty = block.weight if block.is_transaction_block: assert full_block.transactions_info is not None - block_time = struct_time(localtime(full_block.foliage_transaction_block.timestamp)) + block_time = struct_time( + localtime( + full_block.foliage_transaction_block.timestamp + if full_block.foliage_transaction_block + else None + ) + ) block_time_string = time.strftime("%a %b %d %Y %T %Z", block_time) - cost = full_block.transactions_info.cost - tx_filter_hash = full_block.foliage_transaction_block.filter_hash + cost = str(full_block.transactions_info.cost) + tx_filter_hash = "Not a transaction block" + if full_block.foliage_transaction_block: + tx_filter_hash = full_block.foliage_transaction_block.filter_hash else: block_time_string = "Not a transaction block" cost = "Not a transaction block" @@ -325,11 +256,11 @@ async def show_async(args, parser): f"Fees Amount {block.fees}\n" ) else: - print("Block with header hash", args.block_header_hash_by_height, "not found.") + print("Block with header hash", block_header_hash_by_height, "not found.") except Exception as e: if isinstance(e, aiohttp.client_exceptions.ClientConnectorError): - print(f"Connection error. Check if full node rpc is running at {args.rpc_port}") + print(f"Connection error. Check if full node rpc is running at {rpc_port}") print("This is normal if full node is still starting up.") else: tb = traceback.format_exc() @@ -339,5 +270,59 @@ async def show_async(args, parser): await client.await_closed() -def show(args, parser): - return asyncio.run(show_async(args, parser)) +@click.command("show", short_help="show node information") +@click.option( + "-p", + "--rpc-port", + help=( + "Set the port where the Full Node is hosting the RPC interface. " + "See the rpc_port under full_node in config.yaml." + ), + type=int, + default=8555, + show_default=True, +) +@click.option( + "-wp", + "--wallet-rpc-port", + help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml.", + type=int, + default=9256, + show_default=True, +) +@click.option("-s", "--state", help="Show the current state of the blockchain.", is_flag=True, type=bool, default=False) +@click.option( + "-c", "--connections", help="List nodes connected to this Full Node.", is_flag=True, type=bool, default=False +) +@click.option("-e", "--exit-node", help="Shut down the running Full Node", is_flag=True, default=False) +@click.option("-a", "--add-connection", help="Connect to another Full Node by ip:port", type=str, default="") +@click.option( + "-r", "--remove-connection", help="Remove a Node by the first 8 characters of NodeID", type=str, default="" +) +@click.option( + "-bh", "--block-header-hash-by-height", help="Look up a block header hash by block height.", type=str, default="" +) +@click.option("-b", "--block-by-header-hash", help="Look up a block by block header hash.", type=str, default="") +def show_cmd( + rpc_port: int, + wallet_rpc_port: int, + state: bool, + connections: bool, + exit_node: bool, + add_connection: str, + remove_connection: str, + block_header_hash_by_height: str, + block_by_header_hash: str, +) -> None: + asyncio.run( + show_async( + rpc_port, + state, + connections, + exit_node, + add_connection, + remove_connection, + block_header_hash_by_height, + block_by_header_hash, + ) + ) diff --git a/src/cmds/start.py b/src/cmds/start.py index efa93560bb..b02878f5e0 100644 --- a/src/cmds/start.py +++ b/src/cmds/start.py @@ -1,43 +1,30 @@ +import click import asyncio import os import subprocess +from pathlib import Path +from typing import Optional -from src.daemon.client import connect_to_daemon_and_validate +from src.daemon.client import connect_to_daemon_and_validate, DaemonProxy from src.util.service_groups import all_groups, services_for_groups -def make_parser(parser): - - parser.add_argument( - "-r", - "--restart", - action="store_true", - help="Restart of running processes", - ) - parser.add_argument( - "group", - choices=all_groups(), - type=str, - nargs="+", - ) - parser.set_defaults(function=start) - - -def launch_start_daemon(root_path): +def launch_start_daemon(root_path: Path) -> subprocess.Popen: os.environ["CHIA_ROOT"] = str(root_path) # TODO: use startupinfo=subprocess.DETACHED_PROCESS on windows process = subprocess.Popen("chia run_daemon".split(), stdout=subprocess.PIPE) return process -async def create_start_daemon_connection(root_path): +async def create_start_daemon_connection(root_path: Path) -> Optional[DaemonProxy]: connection = await connect_to_daemon_and_validate(root_path) if connection is None: print("Starting daemon") # launch a daemon process = launch_start_daemon(root_path) # give the daemon a chance to start up - process.stdout.readline() + if process.stdout: + process.stdout.readline() await asyncio.sleep(1) # it prints "daemon: listening" connection = await connect_to_daemon_and_validate(root_path) @@ -46,16 +33,16 @@ async def create_start_daemon_connection(root_path): return None -async def async_start(args, parser): - daemon = await create_start_daemon_connection(args.root_path) +async def async_start(root_path: Path, group: str, restart: bool) -> None: + daemon = await create_start_daemon_connection(root_path) if daemon is None: print("failed to create the chia start daemon") - return 1 + return - for service in services_for_groups(args.group): + for service in services_for_groups(group): if await daemon.is_running(service_name=service): print(f"{service}: ", end="", flush=True) - if args.restart: + if restart: if not await daemon.is_running(service_name=service): print("not running") elif await daemon.stop_service(service_name=service): @@ -77,5 +64,9 @@ async def async_start(args, parser): await daemon.close() -def start(args, parser): - return asyncio.get_event_loop().run_until_complete(async_start(args, parser)) +@click.command("start", short_help="start service groups") +@click.option("-r", "--restart", is_flag=True, type=bool, help="Restart of running processes") +@click.argument("group", type=click.Choice(all_groups()), nargs=-1, required=True) +@click.pass_context +def start_cmd(ctx: click.Context, restart: bool, group: str) -> None: + asyncio.get_event_loop().run_until_complete(async_start(ctx.obj["root_path"], group, restart)) diff --git a/src/cmds/stop.py b/src/cmds/stop.py index e22ef0b135..7a5c86fe2f 100644 --- a/src/cmds/stop.py +++ b/src/cmds/stop.py @@ -1,50 +1,19 @@ +import click +import sys import asyncio +from pathlib import Path from src.daemon.client import connect_to_daemon_and_validate from src.util.service_groups import all_groups, services_for_groups -def make_parser(parser): - - parser.add_argument( - "-d", - "--daemon", - action="store_true", - help="Stop daemon", - ) - parser.add_argument( - "group", - choices=all_groups(), - type=str, - nargs="+", - default=None, - ) - - parser.set_defaults(function=stop) - - -async def stop_daemon(daemon): - service = "daemon" - print(f"{service}: ", end="", flush=True) - if daemon is None: - print("not running") - return - r = await daemon.exit() - exited = r["data"]["success"] - if exited: - print("Daemon stopped") - else: - error = r["data"]["error"] - print(f"error: {error}") - - -async def async_stop(args, parser): - daemon = await connect_to_daemon_and_validate(args.root_path) +async def async_stop(root_path: Path, group: str, stop_daemon: bool) -> int: + daemon = await connect_to_daemon_and_validate(root_path) if daemon is None: print("couldn't connect to chia daemon") return 1 - if args.daemon: + if stop_daemon: r = await daemon.exit() await daemon.close() print(f"daemon: {r}") @@ -52,7 +21,7 @@ async def async_stop(args, parser): return_val = 0 - for service in services_for_groups(args.group): + for service in services_for_groups(group): print(f"{service}: ", end="", flush=True) if not await daemon.is_running(service_name=service): print("not running") @@ -66,5 +35,9 @@ async def async_stop(args, parser): return return_val -def stop(args, parser): - return asyncio.get_event_loop().run_until_complete(async_stop(args, parser)) +@click.command("stop", short_help="stop service groups") +@click.option("-d", "--daemon", is_flag=True, type=bool, help="Stop daemon") +@click.argument("group", type=click.Choice(all_groups()), nargs=-1, required=True) +@click.pass_context +def stop_cmd(ctx: click.Context, daemon: bool, group: str) -> None: + sys.exit(asyncio.get_event_loop().run_until_complete(async_stop(ctx.obj["root_path"], group, daemon))) diff --git a/src/cmds/version.py b/src/cmds/version.py deleted file mode 100644 index 553ea58f36..0000000000 --- a/src/cmds/version.py +++ /dev/null @@ -1,10 +0,0 @@ -from src import __version__ - - -def version(args, parser): - print(__version__) - - -def make_parser(parser): - parser.set_defaults(function=version) - return parser diff --git a/src/cmds/wallet.py b/src/cmds/wallet.py index 0a9cfe90c6..198336c416 100644 --- a/src/cmds/wallet.py +++ b/src/cmds/wallet.py @@ -1,3 +1,4 @@ +import click import sys import time from datetime import datetime @@ -11,77 +12,14 @@ from src.util.bech32m import encode_puzzle_hash from src.util.byte_types import hexstr_to_bytes from src.util.config import load_config from src.util.default_root import DEFAULT_ROOT_PATH -from src.util.ints import uint64 +from src.util.ints import uint64, uint16 from src.wallet.transaction_record import TransactionRecord from src.wallet.util.wallet_types import WalletType from src.cmds.units import units from decimal import Decimal -command_list = ["send", "show", "get_transaction", "get_transactions"] - - -def help_message(): - print("usage: chia wallet command") - print(f"command can be any of {command_list}") - print("") - print( - "chia wallet send -f [optional fingerprint] -i [optional wallet_id] -a [(T)XCH amount] -m [(T)XCH fee] " - "-t [target address]" - ) - print("chia wallet show -f [optional fingerprint] -i [optional wallet_id]") - print("chia wallet get_transaction -f [optional fingerprint] -i [optional wallet_id] -tx [transaction id]") - print("chia wallet get_transactions -f [optional fingerprint] -i [optional wallet_id]") - - -def make_parser(parser): - parser.add_argument( - "-wp", - "--wallet-rpc-port", - help="Set the port where the Wallet is hosting the RPC interface." - + " See the rpc_port under wallet in config.yaml." - + "Defaults to 9256", - type=int, - default=9256, - ) - parser.add_argument( - "-f", - "--fingerprint", - help="Set the fingerprint to specify which wallet to use.", - type=int, - ) - parser.add_argument("-i", "--id", help="Id of the wallet to use.", type=int, default=1) - parser.add_argument( - "-a", - "--amount", - help="How much chia to send, in TXCH/XCH", - type=str, - ) - parser.add_argument("-m", "--fee", help="Set the fees for the transaction.", type=str, default="0") - parser.add_argument( - "-t", - "--address", - help="Address to send the TXCH/XCH", - type=str, - ) - parser.add_argument( - "-tx", - "--tx_id", - help="transaction id to search for", - type=str, - ) - parser.add_argument("--verbose", "-v", action="count", default=0) - parser.add_argument( - "command", - help=f"Command can be any one of {command_list}", - type=str, - nargs="?", - ) - parser.set_defaults(function=handler) - parser.print_help = lambda self=parser: help_message() - - -def print_transaction(tx: TransactionRecord, verbose: bool): +def print_transaction(tx: TransactionRecord, verbose: bool) -> None: if verbose: print(tx) else: @@ -95,33 +33,21 @@ def print_transaction(tx: TransactionRecord, verbose: bool): print("") -async def get_transaction(args, wallet_client, fingerprint: int): - if args.id is None: - print("Please specify a wallet id with -i") - return - else: - wallet_id = args.id - if args.tx_id is None: - print("Please specify a transaction id -tx") - return - else: - transaction_id = hexstr_to_bytes(args.tx_id) +async def get_transaction(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: + wallet_id = args["id"] + transaction_id = hexstr_to_bytes(args["tx_id"]) tx: TransactionRecord = await wallet_client.get_transaction(wallet_id, transaction_id=transaction_id) - print_transaction(tx, verbose=(args.verbose > 0)) + print_transaction(tx, verbose=(args["verbose"] > 0)) -async def get_transactions(args, wallet_client, fingerprint: int): - if args.id is None: - print("Please specify a wallet id with -i") - return - else: - wallet_id = args.id +async def get_transactions(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: + wallet_id = args["id"] txs: List[TransactionRecord] = await wallet_client.get_transactions(wallet_id) if len(txs) == 0: print("There are no transactions to this address") for i in range(0, len(txs), 5): for j in range(0, 5): - print_transaction(txs[i + j], verbose=(args.verbose > 0)) + print_transaction(txs[i + j], verbose=(args["verbose"] > 0)) print("Press q to quit, or c to continue") while True: entered_key = sys.stdin.read(1) @@ -131,27 +57,11 @@ async def get_transactions(args, wallet_client, fingerprint: int): break -async def send(args, wallet_client, fingerprint: int): - if args.id is None: - print("Please specify a wallet id with -i") - return - else: - wallet_id = args.id - if args.amount is None: - print("Please specify an amount with -a") - return - else: - amount = Decimal(args.amount) - if args.amount is None: - print("Please specify the transaction fees with -m") - return - else: - fee = Decimal(args.fee) - if args.address is None: - print("Please specify a target address with -t") - return - else: - address = args.address +async def send(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: + wallet_id = args["id"] + amount = Decimal(args["amount"]) + fee = Decimal(args["fee"]) + address = args["address"] print("Submitting transaction...") final_amount = uint64(int(amount * units["chia"])) @@ -171,7 +81,7 @@ async def send(args, wallet_client, fingerprint: int): print(f"Do 'chia wallet get_transaction -f {fingerprint} -tx 0x{tx_id}' to get status") -async def print_balances(args, wallet_client, fingerprint: int): +async def print_balances(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: summaries_response = await wallet_client.get_wallets() print(f"Wallet height: {await wallet_client.get_height_info()}") @@ -206,7 +116,7 @@ async def print_balances(args, wallet_client, fingerprint: int): ) -async def get_wallet(wallet_client, fingerprint=None) -> Optional[Tuple[WalletRpcClient, int]]: +async def get_wallet(wallet_client: WalletRpcClient, fingerprint: int = None) -> Optional[Tuple[WalletRpcClient, int]]: fingerprints = await wallet_client.get_public_keys() if len(fingerprints) == 0: print("No keys loaded. Run 'chia keys generate' or import a key.") @@ -238,7 +148,9 @@ async def get_wallet(wallet_client, fingerprint=None) -> Optional[Tuple[WalletRp continue else: fingerprint = fingerprints[index] + assert fingerprint is not None log_in_response = await wallet_client.log_in(fingerprint) + if log_in_response["success"] is False: if log_in_response["error"] == "not_initialized": use_cloud = True @@ -277,31 +189,24 @@ async def get_wallet(wallet_client, fingerprint=None) -> Optional[Tuple[WalletRp return wallet_client, fingerprint -async def execute_with_wallet(args, parser, function: Callable): - if args.fingerprint is None: - fingerprint = None - else: - fingerprint = args.fingerprint - +async def execute_with_wallet(wallet_rpc_port: int, fingerprint: int, extra_params: dict, function: Callable) -> None: try: config = load_config(DEFAULT_ROOT_PATH, "config.yaml") self_hostname = config["self_hostname"] - if "wallet_rpc_port" not in args or args.wallet_rpc_port is None: + if wallet_rpc_port is None: wallet_rpc_port = config["wallet"]["rpc_port"] - else: - wallet_rpc_port = args.wallet_rpc_port - wallet_client = await WalletRpcClient.create(self_hostname, wallet_rpc_port, DEFAULT_ROOT_PATH, config) + wallet_client = await WalletRpcClient.create(self_hostname, uint16(wallet_rpc_port), DEFAULT_ROOT_PATH, config) wallet_client_f = await get_wallet(wallet_client, fingerprint=fingerprint) if wallet_client_f is None: wallet_client.close() await wallet_client.await_closed() return wallet_client, fingerprint = wallet_client_f - await function(args, wallet_client, fingerprint) + await function(extra_params, wallet_client, fingerprint) except Exception as e: if isinstance(e, aiohttp.client_exceptions.ClientConnectorError): - print(f"Connection error. Check if wallet is running at {args.wallet_rpc_port}") + print(f"Connection error. Check if wallet is running at {wallet_rpc_port}") else: print(f"Exception from 'wallet' {e}") @@ -309,20 +214,76 @@ async def execute_with_wallet(args, parser, function: Callable): await wallet_client.await_closed() -def handler(args, parser): - if args.command is None or len(args.command) < 1: - help_message() - parser.exit(1) - command = args.command - if command not in command_list: - help_message() - parser.exit(1) +@click.group("wallet", short_help="manage your wallet") +def wallet_cmd() -> None: + pass - if command == "get_transaction": - return asyncio.run(execute_with_wallet(args, parser, get_transaction)) - if command == "get_transactions": - return asyncio.run(execute_with_wallet(args, parser, get_transactions)) - if command == "send": - return asyncio.run(execute_with_wallet(args, parser, send)) - elif command == "show": - return asyncio.run(execute_with_wallet(args, parser, print_balances)) + +@wallet_cmd.command("get_transaction", short_help="get transaction") +@click.option( + "-wp", + "--wallet-rpc-port", + help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml.", + type=int, + default=9256, + show_default=True, +) +@click.option("-f", "--fingerprint", help="Set the fingerprint to specify which wallet to use.", type=int) +@click.option("-i", "--id", help="Id of the wallet to use.", type=int, default=1, show_default=True, required=True) +@click.option("-tx", "--tx_id", help="transaction id to search for", type=str, required=True) +@click.option("--verbose", "-v", count=True, type=int) +def get_transaction_cmd(wallet_rpc_port: int, fingerprint: int, id: int, tx_id: str, verbose: int) -> None: + extra_params = {"id": id, "tx_id": tx_id, "verbose": verbose} + asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, get_transaction)) + + +@wallet_cmd.command("get_transactions", short_help="get all transactions") +@click.option( + "-wp", + "--wallet-rpc-port", + help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml.", + type=int, + default=9256, + show_default=True, +) +@click.option("-f", "--fingerprint", help="Set the fingerprint to specify which wallet to use.", type=int) +@click.option("-i", "--id", help="Id of the wallet to use.", type=int, default=1, show_default=True, required=True) +@click.option("--verbose", "-v", count=True, type=int) +def get_transactions_cmd(wallet_rpc_port: int, fingerprint: int, id: int, verbose: bool) -> None: + extra_params = {"id": id, "verbose": verbose} + asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, get_transactions)) + + +@wallet_cmd.command("send", short_help="send chia to other wallet") +@click.option( + "-wp", + "--wallet-rpc-port", + help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml.", + type=int, + default=9256, + show_default=True, +) +@click.option("-f", "--fingerprint", help="Set the fingerprint to specify which wallet to use.", type=int) +@click.option("-i", "--id", help="Id of the wallet to use.", type=int, default=1, show_default=True, required=True) +@click.option("-a", "--amount", help="How much chia to send, in TXCH/XCH", type=str, required=True) +@click.option( + "-m", "--fee", help="Set the fees for the transaction.", type=str, default="0", show_default=True, required=True +) +@click.option("-t", "--address", help="Address to send the TXCH/XCH", type=str, required=True) +def send_cmd(wallet_rpc_port: int, fingerprint: int, id: int, amount: str, fee: str, address: str) -> None: + extra_params = {"id": id, "amount": amount, "fee": fee, "address": address} + asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, send)) + + +@wallet_cmd.command("show", short_help="show wallet information") +@click.option( + "-wp", + "--wallet-rpc-port", + help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml.", + type=int, + default=9256, + show_default=True, +) +@click.option("-f", "--fingerprint", help="Set the fingerprint to specify which wallet to use.", type=int) +def show_cmd(wallet_rpc_port: int, fingerprint: int) -> None: + asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, {}, print_balances)) diff --git a/src/plotting/check_plots.py b/src/plotting/check_plots.py index ccc6e9dbe8..5a4a538454 100644 --- a/src/plotting/check_plots.py +++ b/src/plotting/check_plots.py @@ -13,10 +13,9 @@ from src.wallet.derive_keys import master_sk_to_farmer_sk log = logging.getLogger(__name__) -def check_plots(args, root_path): +def check_plots(root_path, num, challenge_start, grep_string, list_duplicates, debug_show_memo): config = load_config(root_path, "config.yaml") - if args.num is not None: - num = args.num + if num is not None: if num == 0: log.warning("Not opening plot files") else: @@ -28,25 +27,25 @@ def check_plots(args, root_path): else: num = 30 - if args.challenge_start is not None: - num_start = args.challenge_start + if challenge_start is not None: + num_start = challenge_start num_end = num_start + num else: num_start = 0 num_end = num challenges = num_end - num_start - if args.grep_string is not None: - match_str = args.grep_string + if grep_string is not None: + match_str = grep_string else: match_str = None - if args.list_duplicates: + if list_duplicates: log.warning("Checking for duplicate Plot IDs") log.info("Plot filenames expected to end with -[64 char plot ID].plot") - show_memo: bool = args.debug_show_memo + show_memo: bool = debug_show_memo - if args.list_duplicates: + if list_duplicates: plot_filenames: Dict[Path, List[Path]] = get_plot_filenames(config["harvester"]) all_filenames: List[Path] = [] for paths in plot_filenames.values(): diff --git a/src/plotting/create_plots.py b/src/plotting/create_plots.py index c6535cc379..2e3f0c3d83 100644 --- a/src/plotting/create_plots.py +++ b/src/plotting/create_plots.py @@ -78,10 +78,7 @@ def create_plots(args, root_path, use_datetime=True, test_private_keys: Optional pool_contract_puzzle_hash = decode_puzzle_hash(args.pool_contract_address) assert (pool_public_key is None) != (pool_contract_puzzle_hash is None) - if args.num is not None: - num = args.num - else: - num = 1 + num = args.num if args.size < config["min_mainnet_k_size"] and test_private_keys is None: log.warning(f"Creating plots with size k={args.size}, which is less than the minimum required for mainnet") diff --git a/src/wallet/wallet_node.py b/src/wallet/wallet_node.py index 2dbe8caadb..ada21c7103 100644 --- a/src/wallet/wallet_node.py +++ b/src/wallet/wallet_node.py @@ -112,7 +112,7 @@ class WalletNode: self.logged_in_fingerprint: Optional[int] = None self.peer_task = None - def get_key_for_fingerprint(self, fingerprint): + def get_key_for_fingerprint(self, fingerprint: Optional[int]): private_keys = self.keychain.get_all_private_keys() if len(private_keys) == 0: self.log.warning("No keys present. Create keys with the UI, or with the 'chia keys' program.")